乐趣区

关于react.js:基于React全家桶开发网易云音乐PC项目实战二

前言

本篇开始做「网易云音乐 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.js
const 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 引入 icons
3. 应用 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.js
export 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'
// 引入合并后的 reducer
import cRducer from "./reducer";
// redux-devtools -> 浏览器插件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 创立 store 并传递: 1.reducer(纯函数) 2.StoreEnhancer
const store = createStore(cRducer, composeEnhancers(applyMiddleware(thunk)
))

export default store

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

// 在 App.js 组件中应用 react-redux
import {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'
// 轮播图 Action
export 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->reducer
import {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=40×20)

?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")

    实现成果

    退出移动版