欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

网易云音乐 PC 项目实践

最编程 2024-04-21 08:53:19
...

一、 项目简介

1. 项目介绍

  • 项目使用到的技术栈

    • CSS使用Flex进行布局
    • 配置路径别名使用: carco
    • 项目路由使用: react-router来管理
    • 使用react-router-config集中式路径映射表管理
    • 使用styled-components+普通的css编写样式
    • 使用axios发送网络请求
    • 项目全面拥抱React Hooks
    • 项目组件库使用: ant design
    • 使用immutable对项目reducerstate进行管理
    • 使用redux-immtable对根目录reducerstate进行管理
    • 项目使用redux-thunk中间件
    • 使用propType校验props类型及默认值
    • 使用react-transition-group添加过渡动画效果
    • 项目中的优化: 函数式组件全部采用memo、路由懒加载、函数防抖
  • 项目的目标

    • 使用React全家桶开发网易云音乐PC网站

2. 适合人群及收获

  • 适合人群:

    • 适合想了解一个项目的大致流程

    • 或者是学习了React 全家桶但是缺乏React项目经验

      • (如果有的知识点不了解,可以参考以前React系列文章
      • (最好了解一点Node
  • 收获:

    • 如何设计音乐播放器组件,歌词解析等
    • 项目目录的结构划分,大型项目的state管理
    • 项目的大致流程,如何进行性能优化等等

3. 页面效果和功能展示

推荐/新碟上架/榜单

路由切换

歌曲评论

排行榜

播放器

歌曲切换(随机、顺序、单曲循环)

歌曲搜索

  • 新增:键盘事件↓ & 函数防抖

    • ctrl+k 搜索框获取焦点 & 唤醒搜索下拉框
    • esc 取消焦点 & 下拉框
    • enter 进入歌曲搜索详情

歌曲搜索详情列表

  • 在搜索框中按下回车即可,搜索列表基本功能实现

4. 项目源码及API接口

  • ????项目Github地址

  • ???? API接口文档 如果觉得项目还不错的话 ????,就给个 ⭐ 鼓励一下吧

5. 项目规范

  • 项目规范:项目中有一些开发规范和代码风格 (也可以按照自己的习惯)

    1. src下文件夹名称统一小写、多个单词以连接符(-)连接;组件文件及文件夹采用驼峰命名

    2. JavaScript变量名称采用小驼峰标识,常量全部使用大写字母,组件采用大驼峰;

    3. CSS采用普通CSSstyled-component结合来编写

      • 全局采用普通CSS、局部采用styled-component
    4. 整个项目不再使用class组件,统一使用函数式组件,并且全面拥抱Hooks

    5. 所有的函数式组件,为了避免不必要的渲染,全部使用memo进行包裹;

    6. 组件内部的状态,使用useStateuseReducer;业务数据全部放在redux中管理;

    7. 函数组件内部基本按照如下顺序编写代码:

      • 组件内部state管理;
      • reduxhooks代码;
      • 其他组件hooks代码;
      • 其他逻辑代码;
      • 返回JSX代码;
    8. redux代码规范如下:

      • 每个模块有自己独立的reducer,通过combineReducer进行合并;
      • 异步请求代码使用redux-thunk,并且写在actionCreators中;
      • redux直接采用redux hooks方式编写,不再使用connect
  • 其他规范在项目中根据实际情况决定和编写;

6. React devtools图标隐藏(了解即可)

  • 在开发项目实战之前: 我们打开网易云音乐官网,会发现网易云官网为什么看不到react devtools插件的标记 (了解即可,不重要)
  • React devtools图标隐藏

//index.js最上面--在生产环境
// disable react-dev-tools for this project 
if (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === "object") {
	for (let [key, value] of Object.entries(window.__REACT_DEVTOOLS_GLOBAL_HOOK__)) {
		window.__REACT_DEVTOOLS_GLOBAL_HOOK__[key] = typeof value == "function" ? ()=>{} : null;
	}
}

二、 项目初始化

1. 前言-vscode&chrome插件(可选)

  • 如果已经安装过了可以选择跳过,以下都是可选的,当然不安装也没问题

  • 为了更便捷的开发项目,推荐安装以下vscode插件

    • ESLint: 代码风格检查工具,帮助我们规范代码书写
    • vscode-styled-components: 在编写styled-components中语法高亮显示和样式组件的
    • path-alias: 别名路径有对应的智能提示
    • ES7 React/Redux/GraphQL/React-Native snippets: 代码片段
  • chrome插件

    • Redux DevTools: 方便调试redux数据
    • FeHelper: 对服务器返回的json数据进行美化

2. 项目目录划分

  • 使用create-react-app脚手架初始化项目结构: create-react-app music163_xxx
  • 目录结构也可以按照自己习惯的结构来划分
│─src
  ├─assets 存放公共资源css和图片
    ├─css  全局css
    ├─img  
  ├─common  公共的一些常量、数据
  ├─components 公共组件
  ├─pages   路由映射组件
  ├─router  前端路由配置
  ├─service 网络配置和请求
  └─store   全局的store配置
  └─utils   封装的工具函数
  └─hooks   自定义hook

3. 项目初始化

1. css重置及公共css抽取

  • 安装: yarn add normalize.css

  • 在assets/css/reset.css文件中引入 @import "~normalize.css";

并编写自己需要的全局css覆盖normalize中不需要的css

@import "~normalize.css";

/* 样式的重置 */
body, html, h1, h2, h3, h4, h5, h6, ul, ol, li, dl, dt, dd, header, menu, section, p, input, td, th, ins {
  padding: 0;
  margin: 0;
}

ul, ol, li {
  list-style: none;
}

a {
  text-decoration: none;
  color: #666;
}

a:hover {
  color: #666;
  text-decoration: underline;
}

i, em {
  font-style: normal;
}

input, textarea, button, select, a {
  outline: none;
  border: none;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

img {
  border: none;
  vertical-align: middle;
}
//...更多的直接看项目
  • 在index.js中导出 import "./assets/css/reset.css";

2. 项目配置的修改

  • 安装:
`yarn add @craco/craco`
`yarn add babel-plugin-import`
`yarn add antd`
  • 根目录下新建:craco.config.js
const path = require("path");
//dir为当前文件(craco-根路径)路径,__dirname为下方resolve()传递过来的路径,然后拼接
const resolve = (dir) => path.resolve(__dirname, dir);

module.exports = {
//antd样式按需加载
  babel: {
    plugins: [
      [
        "import",
        {
          libraryName: "antd",
          libraryDirectory: "es",
          style: "css",
        },
      ],
    ],
  },
  //配置@别名,代替根目录下的src文件路径
  webpack: {
    alias: {
      "@": resolve("src"),
    },
  },
};

3. 项目路由配置

  • 安装:

yarn add react-router-dom@5.2.0

  • 路由统一配置: yarn add react-router-config
  • 在src的router文件夹下开始编写统一路由配置
import Discover from "@/pages/Discover";
import Friends from "@/pages/Friends";
import Mine from "@/pages/Mine";

const routes = [
  {
    path: "/",
    exact: true,
    component: Discover,
  },
  {
    path: "/discover",
    component: Discover,
  },
  {
    path: "/friends",
    component: Friends,
  },
  {
    path: "/mine",
    component: Mine,
  },
];
export default routes;

4. AppHeader组件

1. styled-components编写css

  • 安装: yarn add styled-components
  • 新建style.js-引入: import styled from "styled-components"
  • 然后开始编写css
  • 注意:Header选项卡数据放在公共组件common文件夹的local-data.js文件下,直接导入遍历即可
  • 注意路由用react-router-config,配置路由映射表

1641797497(1).png

5. AppFooter组件

1. styled-components编写css

  • 直接编写结构与样式即可,没啥好说的

1641797542(1).png

  • 在App.js文件中展示汇总展示组件,其中路由组件放在头部与尾部中间动态展示
// 导入的第三方库
import React, { memo } from "react";
import { renderRoutes } from "react-router-config";

// 导入自己定义的工具
import routes from "./router";

// 导入组件
import AppFooter from "@/components/AppFooter";
import AppHeader from "@/components/AppHeader";

export default memo(function App() {
  return (
    <div>
      <AppHeader />
      {renderRoutes(routes)}
      <AppFooter />
    </div>
  );
});

6. axios的使用

  • 需要用到后端数据,安装axios :yarn add axios
  • 在service文件夹下二次封装axios,然后将其导出到需要的文件下
  • 之后为每个需要请求的模块单独创建一个文件,在其中书写发起请求的方法,导出函数

7. redux的使用

  • 需要用到redux管理数据,安装: yarn add redux react-redux redux-thunk
  • 在src/store中建立reducer.js文件及index.js文件
    • reducer.js文件用于汇总各个reducer,,之后导出到store/index.js的文件中
    • index.js文件用于创建store及各项配置
  • 各个组件之中创建store文件夹用于管理各个组件之间的reducer并导出到src文件夹下的reducer文件中
  • 注意:别忘了在App.js文件中用provider包裹在最外层
import { Provider } from "react-redux";
import store from "./store/index";
export default memo(function App() {
  return (
    <div>
      <Provider store={store}>
        ...
      </Provider>
    </div>
  );
});

8. 使用 useSelector useDispatch 替代connect

import React, { memo, useEffect } from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";

import { getTopBannerAction } from "./store/actionCreators";

function Recommend(props) {
  // 使用hooks代替传统redux,获取数据和进行操作

  // 返回Redux store中对dispatch函数的引用。你可以根据需要使用它
  const dispatch = useDispatch();

  // 从redux的store对象中提取数据(state)。
  const { topBanners } = useSelector(
    (state) => ({
      topBanners: state.recommend.topBanners,
    }),
    shallowEqual
  );
  // console.log(banners);
  useEffect(() => {
    dispatch(getTopBannerAction());
  }, [dispatch]);

  return <div>Recommend:{topBanners.length}</div>;
}

export default memo(Recommend);

// 传统react-redux
// import React, { memo, useEffect } from "react";
// import { connect } from "react-redux";

// import { getTopBannerAction } from "./store/actionCreators";

// function Recommend(props) {
//   const { getBanners, topBanners } = props;

//   // 调用映射到props的getBanners方法
//   useEffect(() => {
//     getBanners();
//   }, [getBanners]);

//   return <div>Recommend:{topBanners.length}</div>;
// }

// // 将下方的state与方法映射到props中
// const mapStateToProps = (state) => {
//   return {
//     topBanners: state.recommend.topBanners,
//   };
// };
// const mapDispatchToProps = (dispatch) => {
//   return {
//     getBanners: () => {
//       dispatch(getTopBannerAction());
//     },
//   };
// };
// export default connect(mapStateToProps, mapDispatchToProps)(memo(Recommend));

9. 数据可变性问题--Immutable.js

  • Immutable.js中文文档

  • 在React开发中,我们总是会强调数据的不可变性:

    • 无论是类组件中的state,还是redux中管理的state;
    • 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的;
  • 数据的可变性引发的问题(案例):

    • 我们明明没有修改obj,只是修改了obj2,但是最终obj也被我们修改掉了;
    • 原因非常简单,对象是引用类型,它们指向同一块内存空间,两个引用都可以任意修改;
  • 有没有办法解决上面的问题呢?

    • 进行对象的拷贝即可:Object.assign或扩展运算符
   const info = {
        name: "why",
        age: 20,
      };
      const obj = { ...info };
      const obj2 = info;

      info.name = "kobe";

      console.log(obj.name); // why
      console.log(obj2.name); // kobe
  • 这种对象的浅拷贝有没有问题呢?
    • 从代码的角度来说,没有问题,也解决了我们实际开发中一些潜在风险;
    • 从性能的角度来说,有问题,如果对象过于庞大,这种拷贝的方式会带来性能问题以及内存浪费;
  • 有人会说,开发中不都是这样做的吗?
    • 从来如此,便是对的吗
  • 为了解决上面的问题,出现了Immutable对象的概念:
    • Immutable对象的特点是只要修改了对象,就会返回一个 新的对象,旧的对象不会发生改变;
  • 但是这样的方式就不会浪费内存了吗?
    • 为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构);
  • 当然,我们一听到持久化第一反应应该是数据被保存到本地或 者数据库,但是这里并不是这个含义:
    • 用一种数据结构来保存数据;
    • 当数据被修改时,会返回一个对象,但是新的对象会尽可 能的利用之前的数据结构而不会对内存造成浪费;
  • 如何做到这一点呢?结构共享。

会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构) ,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:

结构共享

实例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>
    <script>
      const im = Immutable;

      // Map的使用
      const info = {
        name: "kobe",
        age: 30,
        friend: {
          name: "james",
          age: 25,
        },
      };

      const infoIM = im.Map(info);

      const obj = infoIM;
      // 修改后返回一个新对象,新对象的值改变,原对象的值不便
      const infoIM2 = infoIM.set("name", "why");
      console.log(obj.get("name")); //kobe
      console.log(infoIM.get("name")); //kobe
      console.log(infoIM2.get("name")); //why

      // List的使用;
      const names = ["abc", "cba", "nba"];
      // const arr = names;
      // names[0] = "why";
      // console.log(arr); //也会发生改变

      const namesIM = im.List(names);
      const arrIM = namesIM.set(0, "why");
      console.log(namesIM.get(0));
      console.log(arrIM.get(0));

      //注意:Map只能进行浅层转换,将最外层对象转换成Immutable.js,而fromJS能进行深层转换,无论嵌套几层
      const zgc = {
        name: "kobe",
        age: 30,
        friend: {
          name: "james",
          age: 25,
        },
      };

      const zgcIM1 = im.Map(zgc);
      const zgcIM2 = im.fromJS(zgc);
      console.log(zgcIM1.get("friend"));
      console.log(zgcIM2.get("friend"));
    </script>
  </body>
</html>

1641885876(1).png

在项目中使用ImmutableJS

yarn add immutable

在每个模块的reducer.js中引入immutable

//ImmutableJS方法
import * as actionTypes from "./constants";
import { Map } from "immutable";

const defaultState = Map({
  topBanners: [],
});
function reducer(state = defaultState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_TOP_BANNERS:
      return state.set("topBanners", action.topBanners);
    default:
      return state;
  }
}
export default reducer;

// 普通方法
// import * as actionTypes from "./constants";

// const defaultState = {
//   topBanners: [],
// };
// function reducer(state = defaultState, action) {
//   switch (action.type) {
//     case actionTypes.CHANGE_TOP_BANNERS:
//       return { ...state, topBanners: action.topBanners };
//     default:
//       return state;
//   }
// }
// export default reducer;

yarn add redux-immutable

在store/reducer.js中

// import { combineReducers } from "redux";
//reducer联合方法combineReducers改为从redux-immutable中导入
import { combineReducers } from "redux-immutable";

import { reducer as recommendReducer } from "../pages/Discover/c-pages/Recommend/store";
//合并
const reducer = combineReducers({
  recommend: recommendReducer,
});

export default reducer;

读取

 // 从redux的store对象中提取数据(state)。
  const { topBanners } = useSelector(
    (state) => ({
      // topBanners: state.recommend.topBanners,//普通读取数据方法
      
      // topBanners: state.get("recommend").get("topBanners"), //ImmutableJS读取数据方法
      topBanners: state.getIn(["recommend", "topBanners"]), //简写
    }),
    shallowEqual
  );

10. Discover-Recommend组件(发现音乐--推荐模块)

1.构建文件结构及子目录

  • Discover组件选项卡数据放在公共组件common文件夹的local-data.js文件下,直接导入遍历构建文件结构
  • 新建style.js,然后开始编写css
  • 配置子路由映射表,并通过props引入
  • 在Discover文件夹下创建新目录,放入其子组件

1641797940(1).png

2. 完成轮播图组件-TopBanner

  • 在Discover/c-pages/Recommend(推荐)文件夹下创建新目录,放入其子组件
  • 采用antd库中的Carousel完成轮播图,并重写了其圆点样式

3. ThemeHeaderRec头部模块的封装

1641959652(1).png

  • 使用时只需要导入模块并且传入title与keywords就行,用props接收
  • 另外设置了类型校验与默认参数,防止keywords为undefined时报错

4. HotRecommend热门推荐模块完成

1641980250(1).png

  • 在components/SongsCover封装组件,且使用了在utils/Rec-format.js中封装了两个方法 1641980089(1).png
  • redux获取和读取数据,并遍历SongsCover组件

5. NewAlbum新碟上架模块完成

1642054060(1).png

  • 在components/AlbumCover封装组件,且使用了在utils/Rec-format.js中封装的方法
  • redux获取和读取数据,并遍历AlbumCover组件

1642054092(1).png

6. RecommendRanking排行模块完成

  • 在components/TopRanking封装组件,且使用了在utils/Rec-format.js中封装的方法

1642062271(1).png

  • redux获取和读取数据,并使用TopRanking组件三次 1642062239(1).png
  • 整个背景只是一张大背景图

7. 本页面其余模块完成

1642074133(1).png

  • 用户登录模块只完成了UI
  • 入驻歌手模块是从后端请求的数据
  • 热门主播模块是写死在common/local-data.js中的数据
  • 入驻歌手模块与热门主播模块用了同一个头部封装--components/ThemeHeaderSmall

11. 云音乐播放工具栏的实现

image.png

  • 在pages下面新建Player/AppPlayerBar文件夹
  • 在pages下面新建Player/store文件夹
  • 其中滑动条是antd组件库的Slider组件,其原本样式被精灵图覆盖
  • 播放音乐用的<audio/>标签
  • 播放和暂停根据状态动态改变

12. 歌曲展示页面

  • 在pages/Player/index.jsx中搭建展示页面

1642388973(1).png

  • 接收search参数,得到当前歌曲id

1642388863(1).png

  yarn add url-parse
  import { qs } from "url-parse"; //要引入
  const { search } = props.location;
  const { id } = qs.parse(search);

13. 播放歌曲的逻辑

1642390874(1).png

  • 添加歌曲到播放列表:

    • 当用户点击了某个歌曲, 得到当前歌曲详情(ids)
    • 如果该歌曲已存在于播放列表,获取该歌曲在播放列表的索引,修改当前播放歌曲的索引,修改当前播放的歌曲,然后音乐播放器会播放这一首歌曲。
    • 如果当前歌曲不存在,请求该歌曲数据,将歌曲添加到播放列表的最后,获取到这个最后的index,修改当前播放歌曲的索引 ,修改当前播放的歌曲,然后音乐播放器会播放这一首歌曲。
  • 记录当前的播放顺序:

 sequence: 0, // 0 循环 1 随机 2 单曲
 //点击按钮切换播放顺序
 const changeSequence = () => {
    let Sequence = sequence + 1;
    if (Sequence > 2) {
      Sequence = 0;
    }
    dispatch(changeSequenceAction(Sequence));
  };
  • 两种方式实现歌曲播放顺序切换(项目中用第二种):
    • 单曲循环
      1. 再建立一个播放列表,取播放歌曲的时候从这里取,此播放列表的数据只取原列表当前循环的这一首
      2. 直接当前歌曲的索引不变
    • 随机播放
      1. 再建立一个播放列表,取播放歌曲的时候从这里取 ,此播放列表的数据在从原列表复制时打乱顺序
      2. 用随机数
    • 顺序播放
      1. 再建立一个播放列表,取播放歌曲的时候从这里取 ,此播放列表的数据就是原列表的数据
      2. 直接当前歌曲的索引+1
  • 歌曲播放的切换逻辑

1642477993(1).png

// 点击左右按钮,切换(<- ->)播放歌曲
export const changeCurrentIndexAndSongAction = (tag) => {
  return (dispatch, getState) => {
    const playList = getState().getIn(["player", "playList"]);
    const sequence = getState().getIn(["player", "sequence"]);
    let currentSongIndex = getState().getIn(["player", "currentSongIndex"]);
    // 得到新索引
    switch (sequence) {
      case 1: //随机播放
        // 随机播放时,得到一个随机索引值
        let randomIndex = getRandomNumber(playList.length);
        // 如果得到的索引值等于当前索引,重新得到一个新的索引
        while (randomIndex === currentSongIndex) {
          randomIndex = getRandomNumber(playList.length);
        }
        currentSongIndex = randomIndex;
        break;
      default:
        //其他情况,即单曲或者循环播放时(这两种情况点击左右切换按钮都会切换歌曲)
        currentSongIndex = currentSongIndex + tag;
        if (currentSongIndex >= playList.length) {
          currentSongIndex = 0;
        }
        if (currentSongIndex < 0) {
          currentSongIndex = playList.length - 1;
        }
    }
    const currentSong = playList[currentSongIndex];
    dispatch(changeCurrentSongAction(currentSong));
    dispatch(changeCurrentSongIndexAction(currentSongIndex));
  };
};

  • 歌曲对应歌词的请求与解析
    • 从后端获取歌词
    • 解析歌词函数 [00:31.160]如果场景里出现一架钢琴 ==>{time: 16280, content: 'あんなに愛した君がいない'}
const parseExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;

export function parseLyric(lyricString) {
  const lineStrings = lyricString.split("\n");

  const lyrics = [];
  for (let line of lineStrings) {
    if (line) {
      const result = parseExp.exec(line);
      // console.log(result);
      // 如果这一次没有匹配到,则跳过这一次进行下一次匹配
      if (!result) continue;
      const time1 = result[1] * 60 * 1000;
      const time2 = result[2] * 1000;
      const time3 = result[3].length === 3 ? result[3] * 1 : result[3] * 10;
      const time = time1 + time2 + time3;
      // replace方法:用后面的值取代前面的值,trim:去掉空格
      const content = line.replace(parseExp, "").trim();
      const lineObj = { time: time, content: content };
      lyrics.push(lineObj);
    }
  }
  return lyrics;
}
  • 根据当前时间找到并展示歌词
 // 获取当前播放的歌词
    let i = 0;
    // 遍历,如果当前播放的时间小于当前项歌词的时间,break跳出循环,
    // 当前记录的i减去1即为当前播放的歌词
    for (i; i < lyricList.length; i++) {
      let lyricItem = lyricList[i];
      if (currentTime < lyricItem.time) {
        break;
      }
    }
    if (currentLyricIndex !== i - 1) {
      dispatch(changeCurrentLyricIndexAction(i - 1));
      const content = lyricList[i - 1]?.content;
      message.open({
        key: "lyric",
        content: content,
        duration: 0,
        className: "lyric-class",
      });
    }

14. 路由懒加载

//正常
// import Discover from "@/pages/Discover";

// import Recommend from "@/pages/Discover/c-pages/Recommend";
// import Ranking from "@/pages/Discover/c-pages/Ranking";
// import Songs from "@/pages/Discover/c-pages/Songs";
// import Djradio from "@/pages/Discover/c-pages/Djradio";
// import Artist from "@/pages/Discover/c-pages/Artist";
// import Album from "@/pages/Discover/c-pages/Album";

// import Friends from "@/pages/Friends";
// import Mine from "@/pages/Mine";
// import NotFound from "@/components/NotFound";
// import Song from "@/pages/Player";

//懒加载
const Discover = React.lazy(() => import("@/pages/Discover"));
const Recommend = React.lazy((_) =>
  import("@/pages/Discover/c-pages/Recommend")
);
const Ranking = React.lazy((_) => import("@/pages/Discover/c-pages/Ranking"));
const Songs = React.lazy((_) => import("@/pages/Discover/c-pages/Songs"));
const Djradio = React.lazy((_) => import("@/pages/Discover/c-pages/Djradio"));
const Artist = React.lazy((_) => import("@/pages/Discover/c-pages/Artist"));
const Album = React.lazy((_) => import("@/pages/Discover/c-pages/Album"));

const Friends = React.lazy((_) => import("@/pages/Friends"));
const Mine = React.lazy((_) => import("@/pages/Mine"));
const NotFound = React.lazy((_) => import("@/components/NotFound"));
const Song = React.lazy((_) => import("@/pages/Player"));

同时在App组件中用Suspense包裹路由组件,fallback中是一个组件,在加载时调用

import React, { memo, Suspense } from "react";

 <Suspense fallback={<div>...loading</div>}>
          {renderRoutes(routes)}
 </Suspense>

14. 弹窗歌词轮播页面

在Player/AppPlayPanel中完成该模块

1642561639(1).png 注:该模块只完成了歌词轮播与歌曲展示,其余项(删除,收藏等你可以自行完成)

yarn add classnames

  • PlayHeader 头部
  • PlayList 播放列表
  • LyricPanel 歌词轮播 用ref得到右边的歌词滚动模块 调用该方法实现歌词滚动
// 实现歌词的滚动
export function scrollTo(element, to, duration) {
  if (duration <= 0) return;
  var difference = to - element.scrollTop;
  var perTick = (difference / duration) * 10;
  setTimeout(function () {
    element.scrollTop = element.scrollTop + perTick;
    if (element.scrollTop === to) return;
    scrollTo(element, to, duration - 10);
  }, 10);
}

element为ref得到的元素,to为当前歌词的总高度(每一句歌词heght为32px),设置duration=300,如果duration>0,当前元素的滚动高度