前言
本篇开始做 「网易云音乐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
数据进行丑化
- Redux DevTools: 不便调试
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
组件
- 前提: 在
- 装置
router
:yarn add react-router-dom
集中式配置路由映射:
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;
在
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> )})
小结
创立
combinReducers
- 用于将多个
reducer
合并
- 用于将多个
创立
store
- 配置中间件和
redux-devtools
- 配置中间件和
配置
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治理数据
- 应用redux-immutable中的combineReducers;
- 所有的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
形式- 因为原有
state
是immutable
对象, 所以须要批改原有获取形式
- 因为原有
// 在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传递一个固定的高斯含糊背景图)
实现思路:
- 监听轮播图的切换 走马灯组件有对应的API
beforeChange
, 切换面板前的回调 并应用
use Callback
将事件函数包裹- <details>
<summary>如果不理解该Hook点我</summary>
解决了: 当父组件的其余state
产生了扭转, 该事件函数没有变动, 却被从新定义了的问题(简略总结)
</details> - 定义组件的
currentIndex
用于记录, 幻灯片切换的索引
- <details>
在事件函数参数有
from to
- (
to
参数是Carousel走马灯传递函数的参数) - 咱们这里应用
to
变量作为下一个切换的索引
- (
- 依据
cureent index
下标来取出:redux
中保留的top Banners
数组对应的背景图片
const bgImage = topBanners[currentIndex] && (topBanners[currentIndex].imageUrl + "?imageView&blur=40x20")
- 监听轮播图的切换 走马灯组件有对应的API