本篇开始作 「网易云音乐PC」项目,建议最好有如下基础
react、redux、redux-thunk、react-router
,上一章只是对项目进行初步介绍认识,本章节会带你完成:网易云的基本骨架结构并完成使用redux-immutable
重构redux
css<details>
<summary>本章节完成结果以下</summary>html<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6680e5c03844424827672caa46dabba~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:80%;" /></details>前端
为了更便捷的开发项目,推荐安装如下vscode
插件react
ESLint
: 代码风格检查工具,帮助咱们规范代码书写vscode-styled-components
: 在编写styled-components
中语法高亮显示和样式组件的ES7 React/Redux/GraphQL/React-Native snippets
: 代码片断chrome
插件webpack
redux
数据json
数据进行美化create-react-app
脚手架初始化项目结构: create-react-app music163_xxx
│─src ├─assets 存放公共资源css和图片 ├─css 全局css ├─img ├─common 公共的一些常量 ├─components 公共组件 ├─pages 路由映射组件 ├─router 前端路由配置 ├─service 网络配置和请求 └─store 全局的store配置 └─utils 工具函数 └─hooks 自定义hook
项目样式重置选择:ios
normalize.css
+ custom.css
(也就是自定义的css
)安装normalize.css
: yarn add normalize.css
git
css
文件引入: src->assets->css-> normalize.css
↓首先下载项目资源(都是项目使用到的一些背景图和精灵图)github
github
文件慢,参考个人这篇文章加速🚀加载文件下面的全局CSS
是用于页面初始化,若是你的css
掌握的不错,那么建议直接拷贝😏web
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;; }
第一步:安装craco:chrome
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') } } }
使用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/>
styled-components
库yarn add styled-components
Flex
实现功能:点击头部列表项,添加背景实现高亮和下面的小三角 实现思路:(利用`NavLink`组件被点击有`active`的`className`单独给class设置样式便可) 1.NavLink点击活跃后实现上面的效果 2.给NavLink设置自定义className,在对应的css文件实现效果
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/>
<br/>
API接口文档(可选1): 本地安装部署
discover
页面// src/router/router.js -> 对根路径进行重定向到: /discover 👇 const routes = [ // `/`根路径重定向到: /discover路径 --->{ path: '/', exact: true, render: () => <Redirect to="/discover" /> },<---- { path: '/discover', component: JMDiscover } // ... ]
<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 }, ]
discover
页面下渲染嵌套子路由// src->pages->discover->index.js export default memo(function JMDiscover(props) { const { route } = props return ( <div> ... {renderRoutes(route.routes)} </div> ) })
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>
如今让咱们开始请求轮播图数据:
<br/>
安装:
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>
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
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
// 在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> ) })
建立combinReducers
reducer
合并建立store
redux-devtools
配置react-redux
redux
轮播图数据API接口:
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)
---> import { shallowEqual, useDispatch, useSelector } from 'react-redux' <--- const { topBanners } = useSelector(state => ({ topBanners: state.recommend.topBanners, ---> }), shallowEqual) <---
<br/>
Immutable
可让redux
中的维护的state
不在是浅层拷贝再赋值, 而是使用Immutable
数据结构保存数据state
不会修改原有数据结构, 而是返回修改后新的数据结构, 能够利用以前的数据结构而不会形成内存的浪费immutableJS
安装: yarn add immutable
<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') <--- }))
为何不对项目根目录的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
方式
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']) <-- }))
antd
走马灯组件↓使用useRef
获取跑马灯组件暴露的切换轮播图的方法:prev() next()
url
添加了其余的参数咱们在网易云官网发现其实背景图只是添加了:
query string
参数( ?imageView&blur=40x20 )
?imageView&blur=40x20
style.js
中获取URL显示便可 (能够先给banner传递一个固定的高斯模糊背景图)实现思路:
beforeChange
, 切换面板前的回调并使用use Callback
将事件函数包裹
state
发生了改变, 该事件函数没有变化, 却被从新定义了的问题(简单总结)currentIndex
用于记录, 幻灯片切换的索引在事件函数参数有from to
to
参数是Carousel走马灯传递函数的参数)to
变量做为下一个切换的索引cureent index
下标来取出: redux
中保存的top Banners
数组对应的背景图片const bgImage = topBanners[currentIndex] && (topBanners[currentIndex].imageUrl + "?imageView&blur=40x20")