前言
首页主体内容
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.热门推荐模块->发送网络请求
- 这个时候我们就可以来发送网络请求,将请求下来的数据保存在
redux的store
中 - 热门推荐API接口↓:
- /personalized?limit=8
- 示例:http://123.57.176.198:3000/personalized?limit=8
一个组件需要发送网络的基本步骤
// 1.在网络请求对应文件中封装对应的函数
// 2.修改"当前"组件目录下store中的reducer (前提在actionTypes定义常量名)
// 3.在actionCreator文件中添加action发送网络请求
// 4.在组件中使用dispatch action 测试网络请求数据
// 5(可选).定义常量 constans 用于控制limit的数量(方便后期维护修改)
// 6.在组件中使用useSelector展示数据
详细步骤如下?
-
网络请求接口的封装: src/service/recommend.js
export function getHotRecommends(limit) { return request({ url: "/personalized", params: { limit } })
-
修改redux
-
添加actionCreator
-
定义常量用于控制
limit
的数量, 好处是如果有一天想修改数量直接在常量文件中修改即可 -
在组件中使用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.新碟上架
网络请求
-
新碟上架接口:
/top/album?limit=10已废除- /album/newest
- 示例:http://123.57.176.198:3000/album/newest
-
将请求的数据放到
reudx
里面- 封装网络接口请求
- 修改
redux
,添加state
,添加case
语句 - 添加
actionCreator
:getNewAlbumsAction
- 用于发送网络请求
- 组件派发该
action
测试 - 添加
actionCreator
:changeNewAlbumAction
- 用于在网络请求中派发该
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>
- 待完善效果?
AlbumCover组件封装
// AlbumCover组件要求(数据是不固定的): 宽高和bgp(背景图片横纵坐标)由调用者传递
// 因为在其他的页面中使用该组件的尺寸和bgp是不同的
<Carousel dots={false} ref={albumRef}>
...
<AlbumCover
key={cItem.id}
info={cItem}
size={100}
bgp="-570px"
>
{cItem.name}
</AlbumCover>
...
</Carousel>
- 完成效果?
6.榜单
榜单说明
请求榜单数据
-
榜单数据API: /top/list?idx=0已废弃,项目接口又重新改了一遍0: 云音乐飙升榜2: 云音乐新歌榜3: 云原创歌曲榜
-
榜单数据API:
-
下拉查看: 首页三个榜单的id
-
云音乐飙升榜API: http://localhost:3000/playlist/detail?id=19723756
-
云音乐新歌榜API: http://localhost:3000/playlist/detail?id=3779629
-
网易原创欧曲榜API: http://localhost:3000/playlist/detail?id=2884035
-
在请求下来的数据,对其中的
tracks
字段进行数据截取,只保留前10条数据
-
-
注意事项↓
发送网络请求将请求的数据放到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
文件夹?中
布局参考
PlayBar组件布局采用固定定位:
PlayerWrapper↓
内容(content)分了三个部分:↓
Control(左侧)
三个按钮.添加背景图,外层采用flex布局
PlayIInfo(中间)
两个部分(上、下),下面滑动条采用andt组件库Slider组件,找到类名覆盖样式即可
Opertaor(右侧)
两部分,外层采用flex布局
Slider组件(进度条)样式覆盖
Slider组件样式更改(覆盖)
1.外层包裹背景图和宽高样式..
2.mairign边距为0
3.设置背景颜色为透明
4.设置鼠标滑动时的背景图
5.设置圆点的样式覆盖
完成效果
- 图片和一些动态获取的数据暂时先写死
歌曲播放数据请求
-
先固定播放一首歌曲
-
歌曲API接口:/song/detail?ids=167876
- 示例:http://123.57.176.198:3000/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组件提供了2个api:
(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 }
-
完成效果
-
下拉查看
-
单曲循环或顺序播放或列表循环
- 当前歌曲播放完毕后,决定下一首是顺序播放还是单曲循环等等
第一种思路: 创建一个播放列表数组,决定下一首播放什么音乐
如果是顺序播放,直接把源数组拷贝,如果是随机播放,将顺序打乱
第二种思路: 决定下一首播放什么音乐,让当前currentSongIndex + 1,
设计顺序的数据结构(sequence)
0 顺序播放
1 随机播放
2 单曲循环
背景图切换
歌曲列表显示个数
点击按钮播放上一首或下一首
- 点击按钮: 播放上一首或下一首音乐
- 两个按钮监听点击事件: 都使用同一个函数,传递不同的tab(标记),处理不同的逻辑
- 因为需要派发action,所以放到actionCreator里编写
- 单曲循环也是切换到下一首的, 所以它们的逻辑一样
- 切换歌曲的实现思路(参考):
- 根据
playSequence
决定是顺序播放还是随机播放 - 根据播放顺序选择下一首音乐
- 随机播放 ...
- 顺序播放 ...
- 获取需要播放的音乐
- 更改当前播放的索引
- 更改当前播放的音乐
- 根据
决定下一首音乐播放的顺序
- 给音频元素监听:
onEnded
事件(歌曲播放完后触发) - 当前歌曲播放完后只有两种情况:
- 第一种情况: 单曲循环
- 设置当前播放时间为0:
audioRef.current.currentTime = 0
- 设置当前播放时间为0:
- 第二种情况: 切换下一首音乐(根据
playSequence
决定是随机播放还是顺序播放)
- 第一种情况: 单曲循环
其他细节补充
- 点击切换歌曲顺序图标按钮后: 切换对应图标
0顺序播放
1随机播放
2单曲循环
setIsPlaying
修改状态时为什么要添加随机值?- 如果当前是播放状态: 添加下一首音乐时, 还是播放状态, 设置的值还是
true
- 如果这一次的值和上一次的值时相同的, 就不会执行依赖于
isPlaying
的useEffect
回调 - 所以每次更新
isPlaying
时, 需要显示的更新isPlaying
- 如果当前是播放状态: 添加下一首音乐时, 还是播放状态, 设置的值还是
- 点击切换顺序按钮后, 悬浮当前播放的顺序文本提示, 单曲循环还是随机播放等等
- Tooltip文字提示组件, 鼠标经过显示气泡, 内容是单曲循环还是随机播放等等
歌词显示
对请求下来的歌词分析
歌词数据API
接口
- http://123.57.176.198:3000/lyric?id=167876
[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
}
完成效果如下图
拿到当前播放的歌词
在歌曲播放的时候会有一个 currentTime 变量,
拿到这个变量和当前播放的歌词中的 time 进行比对,
小于歌词中time,之后获取索引值-1(要展示的歌词是前一句)
在timeUpdate事件中: 获取当前播放的歌词
1.获取歌词的索引
2.遍历歌词数组.Length
3.判断当前播放时间小于歌词播放时间,获取当前循环的索引
注意: 注意时间问题,转换为毫秒(current)进行对比判断
4.从歌词数组取出索引拿到当前播放的歌词
优化: 对for循环进行优化
5.对歌词进行管理: 由于歌词在多处使用,使用redux进行管理
优化: dispatch action 过于频繁.
解决: index如果没有变不需要dispatch(index和currentLyricIndex对比)
实现效果
展示歌词
-
使用
Antd Message
组件 -
修改内置样式
-
实现效果
最后
- 非常感谢王红元老师的
React核心技术实战
让我学习到很多React
的知识。 - 非常感谢后台提供者
Binaryify
,接口很稳定,文档很完善
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!