前言

本篇开始做 「网易云音乐PC」我的项目,倡议最好有以下根底react、redux、redux-thunk、react-router,上一章只是对我的项目进行初步介绍意识,本章节会带你实现:网易云的根本骨架构造并实现应用redux-immutable重构redux

<details>
<summary>本章节实现后果如下</summary>

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6680e5c03844424827672caa46dabba~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:80%;" />

</details>

我的项目初始化

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

  • 如果曾经装置过了能够抉择跳过,以下都是可选的,当然不装置也没问题
  • 为了更便捷的开发我的项目,举荐装置以下vscode插件

    • ESLint: 代码格调查看工具,帮忙咱们标准代码书写
    • vscode-styled-components: 在编写styled-components中语法高亮显示和款式组件的
    • path-alias: 别名门路有对应的智能提醒
    • ES7 React/Redux/GraphQL/React-Native snippets: 代码片段
  • chrome插件

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

1.我的项目目录划分

  • 应用create-react-app脚手架初始化我的项目构造: create-react-app music163_xxx
  • 目录构造也能够依照本人习惯的构造来划分
│─src  ├─assets 寄存公共资源css和图片    ├─css  全局css    ├─img    ├─common  公共的一些常量  ├─components 公共组件  ├─pages   路由映射组件  ├─router  前端路由配置  ├─service 网络配置和申请  └─store   全局的store配置  └─utils   工具函数  └─hooks   自定义hook

2.我的项目款式抉择

  • 我的项目款式重置抉择:

    • [ ] reset.css
    • [x] normalize.css + custom.css(也就是自定义的css)
  • 装置normalize.css: yarn add normalize.css

    • 在全局css文件引入: src->assets->css-> normalize.css
    • 首先下载我的项目资源(都是我的项目应用到的一些背景图和精灵图)

      • 如果下载github文件慢,参考我的这篇文章减速????加载文件
    • 上面的全局CSS是用于页面初始化,如果你的css把握的不错,那么倡议间接拷贝????

      • 将上面????css拷贝到全局自定义的css文件当中(src -> assets -> css -> reset.css)
      • 定义的挺多的都是一些精灵图背景
      • 精灵图的类名都是对应的图片文件名
/* reset.css (自定义的css) */@import '~normalize.css';/* 后续有阐明,先跳过即可(装置完antd再导入的) *//* @import '~antd/dist/antd.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;}/* 全局款式 */body, textarea, select, input, button {  font-size: 12px;  color: #333;  font-family: Arial, Helvetica, sans-serif;  background-color: #f5f5f5;}.text-nowrap {  white-space: nowrap;  text-overflow: ellipsis;  overflow: hidden;}.w1100 {  width: 1100px;  margin: 0 auto;}.w980 {  width: 980px;  margin: 0 auto;}.text-indent {  text-indent: -9999px;}.inline-block {  display: inline-block;}.sprite_01 {  background: url(../img/sprite_01.png) no-repeat 0 9999px;}.sprite_02 {  background: url(../img/sprite_02.png) no-repeat 0 9999px;}.sprite_cover {  background: url(../img/sprite_cover.png) no-repeat 0 9999px;}.sprite_icon {  background: url(../img/sprite_icon.png) no-repeat 0 9999px;}.sprite_icon2 {  background: url(../img/sprite_icon2.png) no-repeat 0 9999px;}.sprite_button {  background: url(../img/sprite_button.png) no-repeat 0 9999px;}.sprite_button2 {  background: url(../img/sprite_button2.png) no-repeat 0 9999px;}.sprite_table {  background: url(../img/sprite_table.png) no-repeat 0 9999px;}.my_music {  background: url(../img/mymusic.png) no-repeat 0 9999px;}.not-login {  background: url(../img/notlogin.jpg) no-repeat 0 9999px;}.image_cover {  position: absolute;  left: 0;  right: 0;  top: 0;  bottom: 0;  text-indent: -9999px;  background: url(../img/sprite_cover.png) no-repeat -145px -57px;}.sprite_player {  background: url(../img/playbar_sprite.png) no-repeat 0 9999px;}.lyric-css .ant-message-notice-content {  position: fixed;  left: 50%;  bottom: 50px;  transform: translateX(-50%);  background-color: rgba(0,0,0,.5);  color: #f5f5f5;}.wrap-bg2 {  background: url(../img/wrap3.png) repeat-y center 0;;}

3.配置门路别名

  • 第一步:装置craco:

    • yarn add @craco/craco
  • 第二步:批改package.json文件

    • 本来启动时,咱们是通过react-scripts来治理的;
    • 当初启动时,咱们通过craco来治理;
    "scripts": {-"start": "react-scripts start",-"build": "react-scripts build",-"test": "react-scripts test",\+ "start": "craco start",\+ "build": "craco build",\+ "test": "craco test",}
  • 第三步:在根目录下创立 craco.config.js 文件用于批改默认配置↓

    • module.exports = { // 配置文件 }
// 根门路 -> craco.config.jsconst path = require('path')const resolve = dir => path.resolve(__dirname, dir)module.exports = {  webpack: {    alias: {       // @映射src门路      '@': resolve('src'),      'components': resolve('src/components')    }  }}

我的项目构造划分

header组件

  • 状态:固定,不会随着URL发生变化
  • 组件寄存:src/"components/app-header"文件夹中
  • 先点击查看要实现成果

footer组件

  • 状态:固定,不会随着URL发生变化
  • 组件寄存:src/"components/app-footer"文件夹中
  • 先点击查看要实现成果

main主体内容

  • 主体内容会是随着门路变动动静的产生扭转的
  • 应用router动静渲染path对应的组件,具体配置如下↓

    • 前提: 在src/pages文件夹有创立discover和mine和friend组件
  1. 装置routeryarn add react-router-dom
  2. 集中式配置路由映射:yarn add react-router-config

    // src/router->index.js  (配置路由映射)import { Redirect } from "react-router-dom";import Discover from "@/pages/discover";import Mine from "@/pages/mine";import Friend from "@/pages/friend";const routes = [  {    path: "/discover",    component: Discover      },  {    path: "/mine",    component: Mine  },  {    path: "/friend",    component: Friend  },];export default routes;
  3. App.js应用HashRouter组件包裹应用router-config配置的路由映射(使路由映射表的配置失效):

    • <details>
      <summary>点击查看 App.js 中的配置</summary>

        <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ddc98a03ea644a8ca1c06c3f3e2db678~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:80%;" />

      </details>

    • 验证路由是否配置胜利:在header组件中,应用NavLink测试门路切换,渲染对应组件
    • 点击查看实现成果

实现成果如下????

  • 主体内容追随URL发生变化,留神门路的变动和组件的切换

<br/>
<br/>

Header头部组件

1.头部组件款式编写

  • 为了避免多个组件中款式抵触, 组件内应用款式styled-components
  • 装置应用: yarn add styled-components
  • 布局应用: Flex

2.头部区域划分

3.头部区域实现及思路(左)

实现性能:点击头部列表项,增加背景实现高亮和上面的小三角实现思路:(利用`NavLink`组件被点击有`active`的`className`独自给class设置款式即可)   1.NavLink点击沉闷后实现下面的成果   2.给NavLink设置自定义className,在对应的css文件实现成果

4.头部区域实现及思路(右)

  • 右侧能够应用Antd组件也能够自行编写
  • 装置Ant design: yarn add antd
  • 装置Ant design icons: yarn add @ant-design/icons
1.在reset.css文件引入: antd款式 ↓    @import '~antd/dist/antd.css';2.在Header.js引入icons3.应用antd组件: Input组件4.批改placehold文本款式
  • 留神:右侧的搜寻的键盘图标我是起初加的,能够先临时跳过,有趣味的敌人能够做一下

<br/>

Footer底部组件

1.底部区域布局

2.实现成果

<br/>

路由优化和API阐明

1.我的项目接口文档

  • API接口文档(可选1): 本地装置部署

    • API接口文档装置
    • 应用: 本地Node服务器运行
    • API服务器运行
  • 我的项目应用到的接口(可选2): http://123.57.176.198:3000

2.路由优化_重定向

  • 对'根路由'进行重定向到: discover页面
//  src/router/router.js -> 对根门路进行重定向到: /discover   ????const routes = [    //   `/`根门路重定向到: /discover门路--->{ path: '/', exact: true, render: () => <Redirect to="/discover" /> },<----    { path: '/discover', component: JMDiscover }  //  ...]

3.嵌套路由

布局划分

创立对应的子组件

<details>
<summary>创立Discover文件夹下的对应的子组件</summary>

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0374a1352cba41ceb4ea090be4310160~tplv-k3u1fbpfcp-zoom-1.image"  />

</details>

配置"嵌套路由映射表"

const routes = [  { path: '/', exact: true, render: () => <Redirect to="/discover" /> },  {    path: '/discover',    component: JMDiscover,--->routes: [      { path: '/discover', render: () => <Redirect to="/discover" /> },      { path: '/discover/recommend', component: JMRecommend },      { path: '/discover/ranking', component: JMRanking },      { path: '/discover/album', component: JMAlbum },      { path: '/discover/djradio', component: JMDjradio },      { path: '/discover/artist', component: JMArtist },      { path: '/discover/songs', component: JMSongs }    ],<----  },  { path: '/mine', component: JMMine },  { path: '/friend', component: JMFriend },]

渲染嵌套子路由config

  • discover页面下渲染嵌套子路由
// src->pages->discover->index.jsexport default memo(function JMDiscover(props) {  const { route } = props    return (    <div>       ...      {renderRoutes(route.routes)}    </div>  )})

实现成果↓

4.轮播图API

  • 开始发送网络申请应用axios
  • 装置axios: yarn add axios

    • src文件夹下新建service文件夹????用于网络申请
    • 前提: 将axios封装好的文件, 拷贝到该文件夹下
    • 如果以前没有封装过, 能够参考下我的axios繁难封装如下????
    • <details>
      <summary>axios繁难封装,点击查看</summary>

      <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4a971fd109094419ae0aa82b74b43573~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:80%;" />

      </details>

  • 当初让咱们开始申请轮播图数据:

    • 轮播图API: /banner
    • 演示: http://123.57.176.198:3000/banner

<br/>

redux保留服务器返回的数据

1.装置redux

  • 装置:

    • yarn add redux
    • yarn add react-redux
    • yarn add redux-thunk
  • 合并装置: yarn add redux react-redux redux-thunk

<details>
<summary>组织目录织构造</summary>
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb44faf4501f4218b644920af81e347f~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:80%;" />
</details>

2.配置redux

我的项目根目录src下的store → reducer.js

import { combineReducers } from "redux";// 引入recommend页面的store(上面能够临时不写,跳到下第3小结)import { reducer as recommendReducer } from '../pages/discover/child-pages/recommend/store'// 将多个reducer合并const cRducer = combineReducers({   // 上面能够临时不写(上面能够临时不写,跳到下第3小结)  recommend: recommendReducer})export default cRducer

我的项目根src下store → index.js

import { createStore, applyMiddleware, compose } from "redux";// 引入thunk中间件(能够让派发的action能够是一个函数)import thunk from 'redux-thunk'// 引入合并后的reducerimport cRducer from "./reducer";// redux-devtools -> 浏览器插件const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;// 创立store并传递: 1.reducer(纯函数) 2.StoreEnhancerconst store = createStore(cRducer, composeEnhancers(  applyMiddleware(thunk)))export default store

我的项目根src目录下app.js文件中 → 配置react-redux

// 在App.js组件中应用react-reduximport { Provider } from 'react-redux'import store from './store'export default memo(function App() {  return (    <Provider store={store}>       // ...          {renderRoutes(routes)}    </Provider>  )})

小结

  1. 创立combinReducers

    • 用于将多个reducer合并
  2. 创立store

    • 配置中间件和redux-devtools
  3. 配置react-redux

    • 帮忙咱们实现连贯redux

3.轮播图数据通过redux-thunk来申请

  • 轮播图数据API接口:

    • /banner
    • 演示????: http://123.57.176.198:3000/banner
  • 将申请下来的的轮播图数据,放到 redux 当中
  • 留神代码正文中的文件门路
// src->page->dicover->child-pages->recommend->store->actionCreator.js (派发action用的)import * as actionTypes from './actionTypes'import { getTopBanners } from '@/service/recommend.js'// 轮播图Actionexport const changeTopBannerAction = res => ({  type: actionTypes.CHANGE_TOP_BANNER,  topBanners: res,})// 轮播图网络申请export const getTopBannersAction = () => {  return dispatch => {    // 发送网络申请    getTopBanners().then(res => {      dispatch(changeTopBannerAction(res))    })  }}// service->recommend.js ------举荐页的轮播图API接口-----------import request from './request'export function getTopBanners() {  return request({    url: "/banner"  })}// page->dicover->child-pages->recommend.js import React, { memo, useEffect } from 'react'import { useDispatch, useSelector } from 'react-redux'import { getTopBannersAction } from './store/actionCreator'function JMRecommend(props) {  // redux Hook 组件和redux关联: 获取数据和进行操作  const { topBanners } = useSelector(state => ({    topBanners: state.recommend.topBanners,  }))  const dispatch = useDispatch()    useEffect(() => {    dispatch(getTopBannersAction())  }, [dispatch])  return (    <div>      <h2>JMRecommend</h2>      <h3>{topBanners.length}</h3>    </div>  )}export default memo(JMRecommend)

4.useSelector性能优化

  • 具体细节参考: useSelector性能优化
---> import { shallowEqual, useDispatch, useSelector } from 'react-redux'  <---const { topBanners } = useSelector(state => ({  topBanners: state.recommend.topBanners,---> }), shallowEqual)  <---

<br/>

联合ImmutableJS

immutableJS介绍及装置

  • immutableJS介绍: 应用Immutable能够让redux中的保护的state不在是浅层拷贝再赋值, 而是应用Immutable数据结构保留数据
  • 应用immutableJS益处: 批改的state不会批改原有数据结构, 而是返回批改后新的数据结构, 能够利用之前的数据结构而不会造成内存的节约
  • immutableJS装置: yarn add immutable

联合Redux治理数据

  1. 应用redux-immutable中的combineReducers;
  2. 所有的reducer中的数据都转换成Immutable类型的数据

immutableJS融入我的项目

<details>
<summary>对我的项目当前目录reducer应用ImmutableJS</summary>
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5aa0b42d08684a60bd905ee468283cb2~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20200927215952708" />
</details>

// 1.在reducer.js文件应用Immutable设置: discover->child-cpn->recommend->store->reducer.js/* --> */ import { Map } from "immutable";  //<---import * as actionTypes from './actionTypes'// 应用Immutable治理redux中的state (批改的`state`不会批改原有数据结构, 而是返回批改后新的数据结构)const defaultState = Map({  topBanners: [],})export default function reducer(state = defaultState, action) {  switch (action.type) {    case actionTypes.CHANGE_TOP_BANNER:/* ---> */   return state.set('topBanners', action.topBanners)  //<---    default:      return state  }}// 2.在recommend的index.js文件获取的是Immutable对象, 须要进行设置const { topBanners } = useSelector(state => ({--->   topBanners: state.recommend.get('topBanners')  <---  }))

redux-Immutable

  • 为什么不对我的项目根目录的reducer应用immutableJS?

    • 因为根目录的reducer是将多个reducer进行合并的
    • 会十分频繁的操作reducer, 而且应用对combineRducer返回的对象应用immutable进行治理是不能合并的
  • 应用redux-immutable:

    • 装置: yarn add redux-immutable
// 根目录下src->store->reducerimport { combineReducers } from 'redux-immutable'import { reducer as recommendReducer } from '../pages/discover/child-pages/recommend/store'// 多个reducer合并const cRducer = combineReducers({  recommend: recommendReducer})export default cRducer
  • recommend.js文件中批改获取state形式

    • 因为原有stateimmutable对象, 所以须要批改原有获取形式
// 在recommend????c-cpns????top-banners????index.js文件 (获取的是Immutable对象, 须要进行设置)const { topBanners } = useSelector(state => ({    // 上面两行获取state形式相等    // topBanners: state.get('recommend').get('topBanners')--> topBanners: state.getIn(['recommend', 'topBanners']) <--  }))

举荐页Banner

1.轮播图区域布局


  • 轮播图组件布局: TopBanner

  • 轮播图采纳antd走马灯组件↓

2.应用antd走马灯组件

  • 应用走马灯组件
  • 应用useRef获取跑马灯组件裸露的切换轮播图的办法:prev() next()

    • 应用两个按钮管制切换下一张图片 https://ant-design.gitee.io/c...

3.背景高斯含糊实现

  • 在咱们在网易云音乐官网,切换轮播图的时候,会发现轮播图在切换的时候会有突变成果
  • 如何实现突变成果:其实就是一张背景图片, 只不是在申请背景图url增加了其余的参数

增加高斯含糊背景

  • 咱们在网易云官网发现其实背景图只是增加了:

    • 查问字符串也就是query string参数( ?imageView&blur=40x20 )

?imageView&blur=40x20

    • 只须要给Banner元素传递背景图片URL,通过属性穿透在style.js中获取URL显示即可 (能够先给banner传递一个固定的高斯含糊背景图)
    • 实现思路:

      1. 监听轮播图的切换 走马灯组件有对应的APIbeforeChange, 切换面板前的回调
      2. 并应用use Callback将事件函数包裹

        • <details>
          <summary>如果不理解该Hook点我</summary>
          解决了: 当父组件的其余state产生了扭转, 该事件函数没有变动, 却被从新定义了的问题(简略总结)
          </details>
        • 定义组件的currentIndex用于记录, 幻灯片切换的索引
      3. 在事件函数参数有from to

        • (to参数是Carousel走马灯传递函数的参数)
        • 咱们这里应用to变量作为下一个切换的索引
      4. 依据cureent index下标来取出: redux中保留的top Banners数组对应的背景图片
      const bgImage = topBanners[currentIndex] && (topBanners[currentIndex].imageUrl + "?imageView&blur=40x20")

    实现成果