Skip to content

仿网易云音乐-react

2022.1.5

  1. 创建项目
  2. 规划目录
jsx
web_music
├─ public
│  └─ index.html
├─ README.md
└─ src
   ├─ App.jsx // 入口组件
   ├─ assets // 辅助文件
   ├─ common // 公共
   ├─ components // 公共组件
   ├─ index.js // 入口js
   ├─ pages // 页面组件
   ├─ redux // 状态管理器
   ├─ router // 路由
   ├─ services // 网络请求
   └─ utils // 工具
  1. css 重置 normalize

  2. 配置别名 @craco/craco 根目录新建 craco.config.js

jsx
const path = require('path')
const resolve = (dir) => path.resolve(__dirname, dir)

module.exports = {
  // 使用 @craco/craco 配置别名
  webpack: {
    alias: {
      '@': resolve('src'),
      components: resolve('src/components')
    }
  }
}
  1. 安装路由并配置路由 react-router-dom@5.3.0 react-router-config 使用 renderRoutes(routes) 进行路由的自动渲染

  2. 封装 axios 请求数据

jsx
//  config.js

// 在这个文件定义axios 的配置,不仅是react可以用,vue或者别的项目也应该这样做
const devBaseUrl = '开发地址'
const proBaseUrl = '生产地址'

export const BASE_URL =
  process.env.NODE_ENV === 'development' ? devBaseUrl : proBaseUrl

export const TIMEOUT = 5000
// ---------------------------------------------------
// request.js

import axios from 'axios'

// 导入axios的配置
import { BASE_URL, TIMEOUT } from './config'

const instance = axios.create({
  baseURL: BASE_URL,
  timeout: TIMEOUT
})

// 还可以手动添加拦截器
// 请求拦截
instance.interceptors.request.use((config) => {
  return config
})
// 响应拦截
instance.interceptors.response.use((res) => {
  return res.data
})

export default instance
  1. 函数式组件中函数的书写顺序
  • 组件中使用的 state
  • 组件关联 redux:获取和操作数据
  • 其他 hooks
  • 其他业务逻辑

2022.01.08

  1. 创建 header 和 footer 组件

  2. 配置路由

jsx
import Discover from '../pages/Discover'
import Mine from '../pages/Mine'
import Friends from '../pages/Friends'
const routes = [
  {
    path: '/',
    exact: true,
    component: Discover
  },
  {
    path: '/mine',
    component: Mine
  },
  {
    path: '/friends',
    component: Friends
  }
]

export default routes

2022.01.09

  1. 使用 styled-components 构建样式 在 styled-components 中使用 background-img 引入的图片,需要按照模块的方式先进行引入,使用${xxx} 获取引入的图片

  2. 调整 App-header 组件的样式

2022.01.12

  1. 使用 antd 构建 App-header 组件右侧的样式 yarn add antd

  2. 构建 App-footer 组件的样式

2022.01.13

  1. react 路由的重定向 在定义路由 js 中,除了使用 component 以外,还可以使用 reder 函数,使其返回一个组件

    jsx
    // 路由的重定向
    import { Redirect } from 'react-router-dom'
    const routes = [
      {
        path: '/',
        exact: true,
        render: () => <Redirect to="/discover" />
      }
    ]
  2. 复制之前封装的 axios js,并测试

  3. 使用 redux 及 react-redux 进行状态管理

    yarn add redux react-redux redux-thunk

    [redux](E:\git\studyProgram\70 react 学习\11_再学 redux\README.md)

2022.01.14

  1. 使用 Immutable.js 操作 redux 中的数据(reducer),优化性能 yarn add immutable 主要是修改 reducer 中的默认数据,后续修改数据时,使用 immutable 提供的 API 即可。 Immutable 能优化性能的原因,最大化复用原本的数据。一定程度上解决 reducer 中默认数据很多的情况,只修改一个地方,就会将所有数据进行重新修改,重新渲染所有使用了数据的页面。
  • 修改设置数据的方法

    jsx
    const defaultState = Map({
      topBanners: []
    })
    • 修改 reducer 中设置数据的方法

      jsx
      return preState.set('topBanners', data)
    • 修改组件中使用了状态时,获取数据的方法

    jsx
    // 获取 store 中存储的数据,
    const { topBanners } = useSelector(
      (state) => ({
        topBanners: state.discover.get('topBanners')
      }),
      shallowEqual
    )
  1. 而合并全部 reducer,则不能直接使用 immutable ,需要使用 redux-immutable ,将原本的 combineReducers 的引入替换,redux-immutable 内部会自动帮助我们进行性能的优化。

    jsx
    - import { combineReducers } from 'redux'
    + import { combineReducers } from 'redux-immutable'
    
    
     // 容器组件
     const { topBanners } = useSelector(
     (state) => ({
    
     - topBanners: state.discover.get('topBanners')
    
     * topBanners: state.get('discover').get('topBanners')
       // 还可以简写,意思是先获取 discover ,再获取 topBanners
       topBanners: state.getIn(['discover', 'topBanners'])
       }),
       shallowEqual
       )

2022-01-15

  1. 制作 recommend 页面中的轮播图 在调用 beforeChange(用于设置轮播图之后的背景图片的切换) 时,直接使用一个函数名就可以

    还需要使用 useCallback 包裹 handleBannerChange 函数,进行性能优化

    • useCallback 的使用时机:将函数传到自定义组件中.
    jsx
    // 还需要使用 useCallback 来进行性能优化
    const handleBannerChange = useCallback((from, to) => {
      setcurrentIndex(to)
    }, [])
  2. 制作 recommend 的中的通用标题组件 要传递数据,所以需要对数据进行校验和设置默认值,可以使用 propTypes

    jsx
    // 对props数据类型进行限制
    CommonListHeader.propTypes = {
      title: PropTypes.string.isRequired,
      keywords: PropTypes.array
    }
    // 对props数据设置默认值
    CommonListHeader.defaultProps = {
      keywords: []
    }

2022-01-16

  1. 制作通用的歌曲封面组件 SongCover

    • 封装格式化播放量和设置请求图片大小的方法
  2. 制作底部播放相关组件 PlayBar

2022-01-17

  1. 修改底部播放相关组件 PlayBar 样式

  2. 修改精灵图的引入,改用重置样式的地方进行引入,避免轮播图切换时,图片闪动

  3. 增加 播放歌曲相关的 redux(action,reducer) 并在 PlayBar 组件中显示请求的对应数据

2022-01-18

  1. 完善拖动 PlayBar 进度条相关操作,修改播放和暂停按钮的切换

  2. 进度条修改时,不用 useState 也是可行的

jsx
const progressPercent = ((currentTime * 1000) / duration) * 100 || 0 // 设置播放进度条位置
// -----------------------------------------

// 后续替换为了,修改 当前播放时间的时候 也同时修改进度条位置
const timeUpdate = (e) => {
  // 判断是否在拖动进度条中,如果拖动中不改变当前播放时间
  if (!isChange) {
    setcurrentTime(e.target.currentTime)
    setprogressPercent(((currentTime * 1000) / duration) * 100)
  }
}

// 并且还需要在拖动进度条的时候触发对应的修改事件
const sliderChange = useCallback(
  (value) => {
    // 改变是否在拖动的状态
    setisChange(true)
    // 修改进度条位置
    setprogressPercent(value)
    // 设置当前进度对应的时间
    setcurrentTime(((value / 100) * duration) / 1000)
  },
  [isChange, duration]
)
  1. 将 PlayBar 组件拆分 在 reducer 中新增了多个与播放相关的初始化数据

  2. 增加音乐列表 musicList 在 reducer 中新增 musicList,currentSongIndex 等默认数据

2022-01-19

  1. 完善新增音乐到列表

  2. 增加播放模式的切换

  3. 下一首歌曲的播放

    1. 单曲循环 只需要将 audio.currentTime 设置为 0,并重新调用一下 audio.play() 即可,但是将歌曲的显示时间和进度条位置都保存在 redux 中,所以还需要使用 dispatch 重置一下 进度条和显示的时间
    2. 列表循环和随机播放 直接使用 下一曲按钮调用的 action 就可以了
    3. 还要考虑当前是否在播放状态中,如果是在播放状态中,继续播放,否则就不做操作
    jsx
    // play.jsx
    useEffect(() => {
      // 设置请求歌曲的数据到 audio 中
      audio.src = getPlaySong(currentSongs.id)
      if (isPlaying) {
        // 在useEffect中判断是否处于播放状态,如果处于播放状态,redux 中 currentSongs 发生了变化,也需要继续播放音乐,用于处理下一曲
        audio.play()
      }
    }, [currentSongs, audio])

2022-01-20

  1. 理解 useEffect 中 ,依赖项会引起的刷新问题
jsx
useEffect(() => {
  // 设置请求歌曲的数据到 audio 中
  audio.src = getPlaySong(currentSongs.id)
  if (isPlaying) {
    // 在useEffect中判断是否处于播放状态,如果处于播放状态,redux 中 currentSongs 发生了变化,也需要继续播放音乐,用于处理下一曲
    audio.play()
  }
}, [currentSongs]) // 这里的依赖项不能增加 isPlaying 不然切换播放暂停会导致重新执行这个函数,使得播放的音乐重新播放
  1. 增加当前歌词的匹配和歌词滚动的效果

2022-01-21 ---> 2022-01-24

  1. 重构播放组件

  2. 重构播放组件逻辑代码,解决其中存在的问题

  3. 增加歌曲列表,增加在歌曲列表中删除歌曲的逻辑

    jsx
    /* 
    判断是否是最后一首歌曲
    - 不是
      id 与 当前 播放的是否相同 只有相同时需要播放新的歌曲
        - 相同 - 判断索引是否是音乐列表中的最后一首
                是   => 索引为新列表最后一位 播放新的索引对应的歌
                不是 => 索引不变 播放新列表中 旧索引的歌曲
        - 不同 - 查找要删除的歌曲索引与当前索引对比 
                小于 =>  当前索引减少一位
                大于 =>  当前索引不变
      更新列表
    */

2022-01-25

  1. 增加播放组件(PlayBar)的显示和隐藏逻辑,使用工具函数,解决 mouseover 和 mouseout 多次进入组件重复触发的问题。 核心是用 event 中的 relatedTarget 进行判断,使用 fromElement 判断进入和离开的的目标是否相同,如果为 null,则说明是离开组件,否则是在组件内部的元素

  2. 解决 点击其他位置,收起音乐列表的功能 一个 DOM 节点, 可以通过其 contains 方法来判断它是否包含一个元素, 也就是判断这个元素是否位于它自己内部, 如果这个元素是它自己, 同样是返回 true

    jsx
    // Play.jsx
    
    document.addEventListener('click', (event) => {
      let ele = playBarRef.current
      let inele = ele.contains(event.target)
      if (!inele) {
        setshowMusicList(false)
        // 还要添加定时器
        if (!alwaysShowPlayBar) {
          time = setTimeout(() => {
            setplayBarStatus(false)
          }, 4000)
        }
      }
    })

2022-01-26

  1. 使用正则匹配 匹配传递的 location 中的参数
jsx
const text = '?k=成都123&type=1'
const reg = new RegExp('(?<=/?k=).*?(?=&)') // 匹配两个字符之间的内容
const result = text.match(reg)[0] // 成都123
  1. 了解 auido.play() 是一个 Promise,可以使用 then 和 catch 。
  • 可以在 catch 中捕获错误,当歌曲不能播放时,使用 antd 的全局提示,然后跳转下一首歌曲

2022-01-27

  1. 学习打包

  2. 路由的懒加载

  • 修改路由配置 lazy 需要引入 const Discover = lazy(()=>{import('../pages/Discover')})
  • 需要为引用了懒加载的组件提供一个 Suspense 进行包裹,避免因为懒加载时,页面没有加载出来提示的信息,直接为 renderRoutes 提供了 Suspense 进行包裹就可以了。
jsx
<Suspense fallback={<div></div>}>{renderRoutes(routes)}</Suspense>
  1. 测试发布部署 若需要访问在根目录下的子目录,只需修改 package.json 文件 添加

    json
    "homepage":".",

    即可

2022-01-27~2022-02-04

  1. 重写进度条组件 主要使用的是监听鼠标按下后的事件,再榜单鼠标移动的事件,最后在鼠标按下结束后取消事件绑定 如下

    jsx
    //滑块添加拖拽事件
    eBarDrag.current.addEventListener('mousedown', function (event) {
      //初始化鼠标开始拖拽的点击位置
      const nInitX = event.clientX
      //初始化滑块位置
      const nInitLeft = this.offsetLeft
      let nX = 0
    
      //页面绑定鼠标移动事件
      document.onmousemove = (event) => {
        //鼠标移动时取消默认行为,避免选中其他元素或文字
        event.preventDefault()
    
        //获取鼠标移动后滑块应该移动到的位置
        nX = event.clientX - nInitX + nInitLeft
        //限制滑块最大移动位置
        if (nX >= nMax) {
          nX = nMax
        }
        //限制滑块最小移动位置
        if (nX <= 0) {
          nX = 0
        }
    
        // 父组件传递的事件
        props.onChange(nX)
      }
    
      //鼠标松开绑定事件,取消页面上所有事件
      document.onmouseup = function (event) {
        document.onmousemove = null
        document.onmouseup = null
        // 父组件传递的事件
        props.onAfterChange(nX)
      }
    })
  2. 优化功能,基本完整播放组件的开发

  3. 使用进度组件的封装方式,手写一个音量控制组件

  4. 增加缓存的使用,刷新后记录播放的歌曲、歌曲列表、歌词列表、音量大小

  5. 重写播放相关的 action,只有在播放,且不是暂停切换播放的时候请求歌曲地址,节省网络请求。

2022-02~05-2022-02-10

  1. 优化播放进度条,修改 useEffect 事件,增加使用完成后,删除对应的事件。
  2. 切换下一曲的时候,不仅修改 redux 中的 currentPlayurl,还将 audio.src 也设置为空,用于解决切换歌曲后,音乐最大长度还是上一曲的,需要将歌曲的 src 设置为空'',才能在播放音乐-暂停后,切换到下一曲,拖动进度条时能正常拖动。
  3. 开发首页榜单相关内容,将操作相关的组件 播放、删除、收藏等按钮,封装成组件。
  4. 增加显示播放列表时,滚动条调整到播放歌曲的位置,并优化对应的逻辑。超过范围才动态改变滚动条的位置。
  5. 修改播放下一曲,播放模式只会是 0 的问题。

2022-02-11-2022-02-21

  1. 搜索页面各个组件 单曲、歌手、专辑、视频、歌词、歌单、声音主播、用户
  2. 搜索建议
  3. 搜索结果的正则匹配,并且将对应的文字改变样式
  4. 优化播放组件逻辑
  5. 搜索建议组件开发,搜索框失去焦点 blur 和 搜索建议框的点击事件冲突问题,搜索建议框不能使用 onclick 执行优先度比 blur 低,所以需要使用 onmouseDown