前言
项目初始化
前言-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.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
组件
- 前提: 在
- 安装
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引入icons
3.使用antd组件: Input组件
4.修改placehold文本样式
- 注意:右侧的搜索的
键盘图标
我是后来加的,可以先暂时跳过,有兴趣的朋友可以做一下
<br/>
Footer底部组件
1.底部区域布局
2.实现效果
<br/>
路由优化和API说明
1.项目接口文档
API接口文档(可选1): 本地安装部署
- 项目使用到的接口(可选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>
)
})
小结
创建
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'
// 轮播图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管理数据
- 使用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->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']) <--
}))
推荐页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