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

3,113 阅读15分钟

前言

hello大家好,我是"风不识途",很长时间没有更新了~,很多朋友一直在催更新(其实没有),本人最近在实习实在有点忙=.=,有时间的话可以将面试&实习经历总结一下。好了回归正题,在前面我们已经完成了转嵌套路由的点击跳转切换了和轮播图,如果首次阅读本系列请点击,下面我们开始完成首页的主体内容+音乐播放器▶需要完成内容如下↓。

image-20210102105716704

项目预览和源码

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

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

首页主体内容

1.热门推荐头部组件

Content主体布局

要完成的预览图如下↓

header组件封装「前言」

  • 查看需要封装的header组件
  • 为什么封装:由于在当前页面下有多个类似的的头部(header)组件

  • 在当前页面中有的header组件是没有keywords关键字(也就是热门推荐后面的分类):

  • 点击查看多个header组件差异

"头部(header)组件"封装

  • 组件存放路径(参考):将header封装到src->components->ThemeHeader文件夹下

  • 组件需要依赖传递的props:

    • 组件的title(标题),必传;

    • keyword(关键字分类) (可以先写死数据),非必传;

  • 使用propTypes传递默认值

// theme-header-rcm.js
import propTypes from 'prop-types'
// ...
// 指定传递props
ThemeHeaderRmc.propTypes = {
  // title属性必填(左侧标题)
  title: propTypes.string.isRequired,
  // 关键字(非必传,左侧关键字)
  keywords: propTypes.array
}
// 指定默认值
ThemeHeaderRmc.defaultProps  = {
  keywords: []
}

实现效果

RecommendWrapper首页外层结构划分(参考)

2.热门推荐模块->发送网络请求

一个组件需要发送网络的基本步骤

// 1.在网络请求对应文件中封装对应的函数
// 2.修改"当前"组件目录下store中的reducer (前提在actionTypes定义常量名)
// 3.在actionCreator文件中添加action发送网络请求
// 4.在组件中使用dispatch action 测试网络请求数据
// 5(可选).定义常量 constans 用于控制limit的数量(方便后期维护修改)
// 6.在组件中使用useSelector展示数据
详细步骤如下👇
  1. 网络请求接口的封装: src/service/recommend.js

    export function getHotRecommends(limit) {
      return request({
        url: "/personalized",
        params: {
          limit
        }
      })
    
  2. 修改redux
  3. 添加actionCreator
  4. 定义常量用于控制limit的数量, 好处是如果有一天想修改数量直接在常量文件中修改即可

  5. 在组件中使用useSelector展示数据

3.歌曲封面(song cover)组件封装

  • 公共组件位置: src/components/song-cover/index.js
点击查看 歌曲封面(song cover)组件封装:
接口字段: 
    封面图片: picUrl
    播放数量: playCount
    封面名字: name
    封面底部文字: copywriter
song cover组件布局思路👇👇👇

  • 将保存在redux中热门推荐的八条数据,进行遍历(外层包裹div, 并使用flex布局, flex-wrap换行)

4.热门推荐组件完成效果

5.新碟上架

网络请求

  • 新碟上架接口:

  • 将请求的数据放到reudx里面

    1. 封装网络接口请求
    2. 修改redux,添加state,添加case语句
    3. 添加actionCreatorgetNewAlbumsAction
      • 用于发送网络请求
    4. 组件派发该action测试
    5. 添加actionCreatorchangeNewAlbumAction
      • 用于在网络请求中派发该action
    查看保存的state

新碟上架组件布局

  • 轮播图: 使用Carousel(走马灯)控件

数据截取逻辑代码

//1.数据方面:为了保证轮播图的每1个页有5条数据:
//  对数据进行截取: 在遍历第一页中0-5 第二页5-10数据
//  注意:slice(方法不包括截取目标number) 
// 2.布局方面:
//  对class name为npage及进行flex布局
<Carousel dots={false} ref={albumRef}>
  {[0, 1].map(item => {
    return (
      <div key={item} className="page">
        {newAlbums.slice(item * 5, (item + 1) * 5).map(cItem => {
          return (
            <div key={cItem.id} className="c-item">
              {cItem.name}
            </div>
          )
        })}
      </div>
    )
  })}
</Carousel>
  • 待完善效果🤔
album-switch

AlbumCover组件封装

// AlbumCover组件要求(数据是不固定的):  宽高和bgp(背景图片横纵坐标)由调用者传递
// 因为在其他的页面中使用该组件的尺寸和bgp是不同的
<Carousel dots={false} ref={albumRef}>
...
<AlbumCover
  key={cItem.id}
  info={cItem}
  size={100}
  bgp="-570px"
>
  {cItem.name}
</AlbumCover>
...
</Carousel>
  • 完成效果😏

6.榜单

榜单说明

请求榜单数据

发送网络请求将请求的数据放到redux中state中 (详细步骤不在展开,和上面步骤一样)
注意: 根据不同 id 请求不同榜单
在派发action时可以使用switch根据不同的 id 派发不同的action

榜单组件(top-ranking)封装

  • 要完成的组件封装如下👇

  • 要实现效果
    • 刚开始: icons的父元素固定的width为0, hover后给固定的宽度
    • 鼠标划过 这行item 让文字溢出隐藏显示...,并显示icons
    • 鼠标离开显示原本效果,隐藏icons,固定歌曲名字宽度即可

完成效果

7.主体右侧

暂时不做登录具体功能,先只做数据渲染。登录模块布局比较简单就略过了;

入驻歌手(settle-singer)

  • 入驻歌手API:
    • /artist/list?limit=5&cat=5001
    • 示例:http://123.57.176.198:3000/artist/list?limit=5&cat=5001
  • 返回的JSON如下
{
    picUrl(pin):"http://p4.music.126.net/LCWqYYKoCEZKuAC3S3lIeg==/109951165034938865.jpg"
    followed(pin):false
    briefDesc(pin):""
    name(pin):"薛之谦"
    id(pin):5781
    alias(pin):
    musicSize(pin):275
    accountId(pin):97137413
    picId_str(pin):"109951165034938865"
    img1v1Id_str(pin):"109951165034950656"
}

热门主播

  • hot-artist
    • 接口没找到,那就先写死吧,在:src/common/local-data.js 文件已经写好了
    • 返回的JSON如下
{
  picUrl: 'http://p1.music.126.net/H3QxWdf0eUiwmhJvA4vrMQ==/1407374893913311.jpg',
  name: '陈立',
  position: '心理学家、美食家陈立教授',
  url: '/user/home?id=278438485',
},

音乐播放

音乐播放组件(app-play-bar)

播放器组件说明:我们在网易云音乐官网切换页面时,会发现音乐播放一直是固定在下面的,和路由切换没有关系

  • 组件存放位置: 所以我们将app-play-bar组件封装到 src/psges 文件夹📂中

image-20210103144821061

布局参考

PlayBar组件布局采用固定定位: 
 PlayerWrapper↓
 	内容(content)分了三个部分:↓
 		Control(左侧)
 			三个按钮.添加背景图,外层采用flex布局
 		PlayIInfo(中间)
 			两个部分(上、下),下面滑动条采用andt组件库Slider组件,找到类名覆盖样式即可
 		Opertaor(右侧)
 			两部分,外层采用flex布局

Slider组件(进度条)样式覆盖

Slider组件样式更改(覆盖)
  1.外层包裹背景图和宽高样式..
  2.mairign边距为0
  3.设置背景颜色为透明
  4.设置鼠标滑动时的背景图
  5.设置圆点的样式覆盖

完成效果

  • 图片和一些动态获取的数据暂时先写死

歌曲播放数据请求

  • 先固定播放一首歌曲

  • 歌曲API接口:/song/detail?ids=167876

  • 请求下来的数据放在哪呢?

    • 请求下的由于是歌曲信息所以就放在player文件夹下store文件夹📂下的store中进行保存
    • rducer中使用immutable管理state
    • 在项目根目录,导入player文件夹下的reducer,进行combine(合并)
step1
    添加player中reducer默认state的 currentSong: {}
    发送网络请求: player.js
    将数据保存在reducer中

step2
    在plaer组件使用redux中请求来的数据: 
	useSelector
    更改player(播放器)组件的固定数据
        currentSong.al.picUrl   图片
        currentSong.name 		歌曲名字
        currentSong.ar[0].name  作者
        currentSong.dt  (歌曲总时长,格式化)
    导入时间格式化工具(转换时间格式)
        formatDate(duration, "mm:ss")

播放音乐功能

  • 歌曲播放API接口: music.163.com/song/media/…
    • id是动态的:可以从请求下来当前歌曲(currentSong)获取当前歌曲id信息

音乐播放逻辑

下面我们开始做音乐播放功能
  step1 
  	添加 audio 标签
	点击 ▶播放按钮 监听click事件,添加src属性↓
  step2 音乐播放
  	使用useRef,获取audio的dom元素
  	封装: 歌曲播放`API`接口,id作为参数
  	之后点击 播放▶按钮 动态设置scr属性,调用play方法开始播放
  step3 歌曲时间显示
  	创建组件局部状态: currentTime 用于更改当前播放时间
  	audio元素有一个OnTimeUpdate事件,当歌曲事件发生变化就会被回调
  		事件参数: e.currentTime属性(用于获取当前播放时间)
  		对秒数->对时间进行格式化->formatDate(currentTime, 'mm:ss')
  step4 进度条滚动
	控制andt的Slider组件的value值: 当前播放的进度=当前时间/总时长*100

拖动滑块逻辑

下面我们开始做拖动滑块,播放对应的进度歌曲
  需求:
    Slider组件滑动时: 当前时间会发生改变
    Slider组件抬起时: 当前歌曲进度发生改变
  step1
    1.Slider组件提供了2api: 
      (1)onChange: 当滑块被拉动时触发,函数的参数是拖动的value
      (2)onAfterChange: 当滑抬起时触发,函数的参数是抬起时的value
    2.progress状态用于保存当前Slider进度,当歌曲播放触发更改进度
      (1)onChange事件参数的value为当前滑动的进度值: 更改setProgress进度
      (2)当我们播放音乐时,拖动滑块时,会有bug(进度条被拉到前面了)
        (2.1)原因: 这是因为我们在onChange事件中,和timeUpdate事件中都在更改"progress"进度值,在歌曲播放时触发TimeUpdate事件中也更改了"progress"进度
        (2.2)解决: 组件中创建一个用于标识是否正在改变的state,如果不是在change那么就在歌曲播放事件中更改progress进度,最后在抬起事件中再将标识change变量更改为false
  step2 
    1.设置歌曲的src属性,放到uesEffect当中依赖于currentSong
    2.播放暂停功能,背景图切换

FAQ

progress进度: 1-100
currentTime: 要的是毫秒数
audioRef.current.currentTime: 要的是总秒数

歌曲播放具体功能完善

点击页面上的一首歌播放音乐

  • 音乐播放逻辑

  • 1.在reducer中添加需要的字段

    • currentSongIndex 记录当前播放音乐的索引
    • playList 播放列表
  • 2.请求歌曲详情逻辑

    • 下拉查看
  • 3.当一个组件内部的actionCreator被其他组件使用时(参考)

    // 将添加歌曲action导出
    export {
      reducer,
      actionCreator
    }
    
  1. 完成效果

    • 下拉查看 redux-change

单曲循环或顺序播放或列表循环

  • 当前歌曲播放完毕后,决定下一首是顺序播放还是单曲循环等等
第一种思路: 创建一个播放列表数组,决定下一首播放什么音乐
  如果是顺序播放,直接把源数组拷贝,如果是随机播放,将顺序打乱
第二种思路: 决定下一首播放什么音乐,让当前currentSongIndex + 1,
  设计顺序的数据结构(sequence)
    0 顺序播放 
    1 随机播放
    2 单曲循环 
  背景图切换
  歌曲列表显示个数

点击按钮播放上一首或下一首

  • 点击按钮: 播放上一首或下一首音乐
  • 两个按钮监听点击事件: 都使用同一个函数,传递不同的tab(标记),处理不同的逻辑
    • 因为需要派发action,所以放到actionCreator里编写
    • 单曲循环也是切换到下一首的, 所以它们的逻辑一样
  • 切换歌曲的实现思路(参考):
    • 根据playSequence决定是顺序播放还是随机播放
    • 根据播放顺序选择下一首音乐
      • 随机播放 ...
      • 顺序播放 ...
    • 获取需要播放的音乐
    • 更改当前播放的索引
    • 更改当前播放的音乐

next-music

决定下一首音乐播放的顺序

  • 给音频元素监听: onEnded事件(歌曲播放完后触发)
  • 当前歌曲播放完后只有两种情况:
    • 第一种情况: 单曲循环
      • 设置当前播放时间为0: audioRef.current.currentTime = 0
    • 第二种情况: 切换下一首音乐(根据playSequence决定是随机播放还是顺序播放)

其他细节补充

  1. 点击切换歌曲顺序图标按钮后: 切换对应图标 0顺序播放 1随机播放 2单曲循环
  2. setIsPlaying修改状态时为什么要添加随机值?
    • 如果当前是播放状态: 添加下一首音乐时, 还是播放状态, 设置的值还是true
    • 如果这一次的值和上一次的值时相同的, 就不会执行依赖于isPlayinguseEffect回调
    • 所以每次更新isPlaying时, 需要显示的更新isPlaying
  3. 点击切换顺序按钮后, 悬浮当前播放的顺序文本提示, 单曲循环还是随机播放等等
    • Tooltip文字提示组件, 鼠标经过显示气泡, 内容是单曲循环还是随机播放等等

歌词显示

对请求下来的歌词分析

歌词数据API接口

[00:00.000] 作曲 : 许嵩 -> {time: 毫秒, content: "歌词内容"}
[00:01.000] 作词 : 许嵩
[00:22.240]天空好想下雨
[00:24.380]我好想住你隔壁
[00:26.810]傻站在你家楼下
[00:29.500]抬起头数乌云
[00:31.160]如果场景里出现一架钢琴
[00:33.640]我会唱歌给你听\n[00:35.900]哪怕好多盆水往下淋\n[00:41.060]夏天快要过去\n[00:43.340]请你少买冰淇淋\n[00:45.680]天凉就别穿短裙\n[00:47.830]别再那么淘气\n[00:50.060]如果有时不那么开心\n[00:52.470]...
  • 咱们会发现请求下来的歌词是有规律的: \n 为换行

请求歌词数据的时机

  • 什么时候请求歌词数据:
    • 组件被渲染完成切换歌曲时
    • 请求当前播放音乐的歌词或者点击页面上的歌曲

封装歌词解析工具函数(逻辑思路)

1.使用slice切割字符串
2.创建正则解析规则: 将"[00:26.810]傻站在你家楼下..."  解析成->  00:26.810 
3.注意: 最后一行也有\n在遍历时加个判断,如果为不为空执行下面操作
4.获取正则解析的3个时间转换为毫秒, 分钟:秒数:00*10 000就是毫秒(加个判断*1转换为number类型)
5.将获取的3个毫秒数相加: 当前歌曲播放的总时长(毫秒)
6.获取当前播放的歌词: replace方法 (完成效果如下👇)
  [
    0: {totalTime: 0, content: "作曲 : 许嵩"}
    1: {totalTime: 1000, content: "作词 : 许嵩"}
    2: {totalTime: 22240, content: "天空好想下雨"}
    3: {totalTime: 24380, content: "我好想住你隔壁"}
  ]
7.将数据保存到redux当中
8.注意: 在切换歌曲时有可能会报 Cannot read property '1' of null , 这是因为从result读取属性时没找到, 加一个if判断,如果result没有值的话, 执行关键字continue跳转到判断条件重新执行

歌词解析代码

const parseExp = /\[([0-9]{2}):([0-9]{2})\.([0-9]{2,3})\]/
export function parseLyric(lyrics) {
  if(!lyrics) return
  const lineStrings = lyrics.split('\n')
  const lyricList = []
  for (const line of lineStrings) {
    if (line) {
      const result = parseExp.exec(line)
      if(!result) continue
      const time1 = result[1] * 60 * 1000
      const time2 = result[2] * 1000
      const time3 = result[3].length > 2 ? result[3] * 1 : result[3] * 1000
      // 当前歌曲播放的总时长(毫秒)
      const totalTime = time1 + time2 + time3
      const content = line.replace(parseExp, '').trim()
      const lineObj = {totalTime, content};
      lyricList.push(lineObj)
    }
  }
  return lyricList
}

完成效果如下图

lyric-result

拿到当前播放的歌词

在歌曲播放的时候会有一个 currentTime 变量, 
拿到这个变量和当前播放的歌词中的 time 进行比对,
小于歌词中time,之后获取索引值-1(要展示的歌词是前一句)

在timeUpdate事件中: 获取当前播放的歌词
	1.获取歌词的索引
   	2.遍历歌词数组.Length
   	3.判断当前播放时间小于歌词播放时间,获取当前循环的索引
   	注意: 注意时间问题,转换为毫秒(current)进行对比判断
   	4.从歌词数组取出索引拿到当前播放的歌词
   	优化: 对for循环进行优化
   	5.对歌词进行管理: 由于歌词在多处使用,使用redux进行管理
   	  优化: dispatch action 过于频繁. 
   	  解决: index如果没有变不需要dispatch(index和currentLyricIndex对比)

实现效果

lyric-show

展示歌词

  • 使用Antd Message组件

  • 修改内置样式

  • 实现效果

show-lyric

  • 到现在我们已经完成「网易云音乐PC」首页基本功能,相信你对React全家桶已经是比较熟练了,接下来想往哪方面扩展可以自行补充完善功能(不过相信能看到这里的小伙伴估计没几个)😂;
  • 如果文章中有哪部分不明白的或写的不好或是有什么建议欢迎大家提出🤗,希望大家共同进步;

最后

  • 非常感谢王红元老师的React核心技术实战让我学习到很多 React 的知识。
  • 非常感谢后台提供者Binaryify,接口很稳定,文档很完善