基于React全家桶开发「网易云音乐PC」项目实战(二)

13,667 阅读11分钟

前言

本篇开始做 「网易云音乐PC」项目,建议最好有以下基础react、redux、redux-thunk、react-router上一章只是对项目进行初步介绍认识,本章节会带你完成:网易云的基本骨架结构并完成使用redux-immutable重构redux

本章节完成结果如下

项目初始化

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

  • 如果已经安装过了可以选择跳过,以下都是可选的,当然不安装也没问题

  • 为了更便捷的开发项目,推荐安装以下vscode插件

    • ESLint: 代码风格检查工具,帮助我们规范代码书写

    • vscode-styled-components: 在js中编写styled-components中语法高亮显示和智能提示

    • path-alias: 别名路径有对应的智能提示

    • ES7 React/Redux/GraphQL/React-Native snippets: 代码片段

  • chrome插件

前言-项目预览和源码

  • 在线预览地址👉www.wanguancs.top

  • 项目Gihub地址👉: Musci 163 如果觉得项目还不错的话 👏,就给个 star ⭐ 鼓励一下吧~

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
    • 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. 安装react-router-config(集中式配置路由映射):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配置的路由映射(使路由映射表的配置生效):

    • 点击查看 App.js 中的配置
    • 验证路由是否配置成功:在header组件中,使用NavLink测试路径切换,渲染对应组件

    • 点击查看完成效果

完成效果如下👇

  • 主体内容跟随URL发生变化,注意路径的变化和组件的切换



Header头部组件

1.头部组件样式编写

  • 为了防止多个组件中样式冲突, 组件内使用样式styled-components
  • 安装使用: yarn add styled-components
  • 布局使用: Flex

2.头部区域划分

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

header-item-active

实现功能:点击头部列表项,添加背景实现高亮和下面的小三角
实现思路:(利用`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文本样式
  • 注意:右侧的搜索的键盘图标我是后来加的,可以先暂时跳过,有兴趣的朋友可以做一下

Footer底部组件

1.底部区域布局

2.实现效果


路由优化和API说明

1.项目接口文档

2.路由优化_重定向

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

3.嵌套路由

布局划分

创建对应的子组件

创建Discover文件夹下的对应的子组件

配置"嵌套路由映射表"

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
import { renderRoutes } from 'react-router-config'
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简易封装如下👇

    • axios简易封装,点击查看
  • 现在让我们开始请求轮播图数据:


redux保存服务器返回的数据

1.安装redux

  • 安装:
    • yarn add redux
    • yarn add react-redux
    • yarn add redux-thunk
  • 合并安装: yarn add redux react-redux redux-thunk
组织目录织结构

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 { renderRoutes } from 'react-router-config'
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接口:
  • 将请求下来的的轮播图数据,放到 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))
    })
  }
}

// ---src/service->recommend.js-----------推荐页的轮播图API接口👇-----------------------
import request from './request'
export function getTopBanners() {
  return request({
    url: "/banner"
  })
}

// ---src/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性能优化

// src/page->dicover->child-pages->recommend.js👇
---> import { shallowEqual, useDispatch, useSelector } from 'react-redux'  <---

// --->: 代表发生了变化的标识 <---
const { topBanners } = useSelector(state => ({
  topBanners: state.recommend.topBanners,
---> }), shallowEqual)  <---

结合ImmutableJS

immutableJS介绍及安装

  • immutableJS介绍: 使用Immutable可以让redux中的维护的state不在是浅层拷贝再赋值, 而是使用Immutable数据结构保存数据
  • 使用immutableJS好处: 修改的state不会修改原有数据结构, 而是返回修改后新的数据结构, 可以利用之前的数据结构而不会造成内存的浪费
  • immutableJS安装: yarn add immutable

结合Redux管理数据

  1. 使用redux-immutable中的combineReducers;

  2. 所有的reducer中的数据都转换成Immutable类型的数据

immutableJS融入项目

对项目当前目录reducer使用ImmutableJS image-20200927215952708
// 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走马灯组件

3.背景高斯模糊实现

  • 在我们在网易云音乐官网,切换轮播图的时候,会发现轮播图在切换的时候会有渐变效果
  • 如何实现渐变效果:其实就是一张背景图片, 只不是在请求背景图url添加了其他的参数

添加高斯模糊背景

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

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

    • 查看背景图(注意红线区域)
    • 网易云音乐 高斯模糊背景图 ?imageView&blur=40x20

    • 只需要给Banner元素传递背景图片URL,通过属性穿透在style.js中获取URL显示即可 (可以先给banner传递一个固定的高斯模糊背景图)

  • 实现思路:

    1. 监听轮播图的切换 走马灯组件有对应的APIbeforeChange, 切换面板前的回调

    2. 并使用use Callback将事件函数包裹

      • 如果不了解该Hook点我 解决了: 当父组件的其他state发生了改变, 该事件函数没有变化, 却被重新定义了的问题(简单总结)
      • 定义组件的currentIndex用于记录, 幻灯片切换的索引

    3. 在事件函数参数有from to

      • (to参数是Carousel走马灯传递函数的参数)
      • 我们这里使用to变量作为下一个切换的索引
    4. 根据cureent index下标来取出: redux中保存的top Banners数组对应的背景图片

    const bgImage = topBanners[currentIndex] && (topBanners[currentIndex].imageUrl + "?imageView&blur=40x20")
    

实现效果

banner-switch

  • 到现在我们已经基本完成了「网易云音乐」基本机构样式和轮播图