项目功能说明
- 可顺序、单曲循环、随机播放音乐
- 播放上一曲、下一曲音乐
- 歌词的实时展示
- 歌曲滚动条实时
- 点击歌曲添加到播放列表
项目技术
- react
- antd组件库
- 用react-routerconfig进行路由管理
- Hooks的useState进行状态管理;
- 后端使用网易云API接口获取数据
- axios进行交互。
文件目录结构说明
- assets: 存放项目资源,包括css,img,font等
- common:公共的数据、常量等
- components:多个页面间共享的组件
- pages:划分各个页面
- router:路由配置
- services:网络请求
- store:redux合并
- utils:js的工具
导入文件的规范
- 导入第三方模块(最上)
- 功能性东西(网络请求,actionCreators、utils)
- 导入组件
本项目使用React.memo()来帮助我们控制何时重新渲染组件。组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是通过 PureComponent 和 React.memo(),我们可以仅仅让某些组件进行渲染,从而提升性能。PureComponent 用于类组件,React.memo()用于函数组件
css重置
安装normalize.css yarn add normalize.css
,并通过@import "~normalize.css";
模块化导入。
项目配置别名
yarn add @craco/craco
-
在package.json内修改项目启动配置
原package.json
修改后 - 在根目录下创建craco.config.js添加配置
- 重新启动该项目
API接口
网易云音乐接口.png
项目结构划分
添加路由
yarn add react-router-dom
- 使用
yarn add react-router-config
将所有路由配置统一管理 - 使用
renderRoutes
函数完成配置 - 网易云音乐使用HashRouter
AppHeader和AppFooter布局
- 安装
yarn add style-components
,编写css样式 - 导航栏采用flex布局
- 导航栏前三项为路由跳转,后三项为超链接跳转
- 导航栏选项卡提前配置成数组数据,在
local-config.js
文件内导出,使用时只需要遍历数组即可 - 搜索框使用antdesign
yarn add antd
引入yarn add @ant-design/icons
,使用图标库
@import "~ant/dist/antd.css";
在reset.css内引入
discover子路由的搭建和配备
- discover文件夹下创建c-pages文件夹,放入子组件
- 项目中引入axios发送网络请求
yarn add axios
使用Hooks useState和axios获取数据
使用antDesign组件制作轮播图(走马灯)
- 轮播图背景图:高斯模糊图片;是由原图片拼接上
"?imageView&blur=40x20"
后得到。
轮播图下内容制作
- 将每个相似样式的标题组件抽出到公共组件中,
components
文件夹下theme-header-rcm
文件夹中 - 热门推荐中每个歌曲封面相同样式也抽到公共组件中,
components
文件夹下songs-sover
文件夹中 - 对音乐播放量数字处理的函数
getCount()
写在utils
文件夹的format-utils.js
文件内 - 优化页面性能:图片大小优化(对获取到的封面图片大小处理的函数
getSizeImage()
也写在utils
文件夹的format-utils.js
文件内)
新碟上架
- 新碟上架的分页效果采用轮播图方法制作
榜单
- 榜单的自定义组件也在components文件夹内的top-ranking文件内
播放音乐
- 使用h5新增audio标签播放音乐,通过增加点击事件播放音乐;使用antd的滑动输入条组件更改样式模仿
//播放音乐
useEffect(() => {
audioRef.current.src = getPlayUrl(song.id);
}, [currentSong]);
- 使用audio标签中的onTimeUpdate={CurrentTime}属性获得当前音乐播放时间,利用useState()保存音乐进度时间
const [currentTime, setCurrentTime] = useState(0);
const timeUpdate = (e) => {
// console.log(e.target.currentTime);
// e.target.currentTime(获得当前播放音乐的进度时间) *1000 转换为毫秒
setCurrentTime(e.target.currentTime * 1000)
}
- 实时更新进度条:antd的滑动输入条自带value={progress}属性,可以实时更新进度
// 当前进度条进度
const [progress, setProgress] = useState(0);
//写在timeUpdate 回调函数内
setCurrentTime(e.target.currentTime * 1000)
//duration 为当前音乐的总时长(s)
新增
const [isChanging, setIsChanging] = useState(false);
来判断当前进度条是否正在改变,以便于当用户正在播放音乐并滑动滚动条时,滚动条可以滑动-
利用antd的滑动输入条自带onChange和onAfterChange的API可以得到进度条滑动的位置和滑动后结束的位置
antd滑动输入条API useCallback()使用:当把一个回调函数传入到自定义组件内部的时候用其做一个包裹;目的是不希望子组件进行多次渲染;是为了进行性能的优化
// 滑动中的位置
const sliderChange = useCallback((value) => {
setIsChanging(true);
// 滑动滚动条时,实时更新时间的改变
const currentTime = value /100 * duration ;
setCurrentTime(currentTime)
setProgress(value) ;
// console.log("change:",value);
},[duration]);
//滑动结束后的位置
const sliderAfterChange = useCallback( (value) => {
//互动进度条后的进度条时间
const currentTime = value / 100 * duration /1000;
audioRef.current.currentTime = currentTime;
// 重新更新进度条时间
setCurrentTime(currentTime * 1000);
setIsChanging(false);
//如果没有播放音乐,当滑动滚动条后开始播放音乐
if(!isPlaying){
playMusic();
}
}, [duration, isPlaying,playMusic] )
- 播放和暂停播放
// 点击事件
const playMusic = () => {
if(!isPlaying){ // 播放音乐
audioRef.current.play();
// console.log("播放");
}
else{ //暂停音乐
audioRef.current.pause();
// console.log("暂停");
}
setIsPlaying(!isPlaying);
}
音乐播放工具栏的实现
- 逻辑为:当点击推荐音乐的榜单组件内每个歌曲的播放图标时,判断该歌曲是否在播放列表内,如果不在则添加该歌曲到播放列表内并播放该歌曲;如果在则获取该歌曲在列表中的位置,将其位置放在最顶端,并播放该歌曲。
- 跨组件间的事件传递使用到了事件总线,安装events
yarn add events
events常用的API:- 创建EventEmitter对象:eventBus对象;
- 发出事件:eventBus.emit("事件名称", 参数列表
- 监听事件:eventBus.addListener("事件名称", 监听函数);
- 移除事件:eventBus.removeListener("事件名称", 监听函数);
歌词
- 展示歌词部分也使用的是antd的message组件
- 通过api接口获取到歌词并解析展示
项目中出现的问题
- 背景图片引入出现问题
本来打算使用字符串拼接背景图片地址,使用require()
导入本地图片
background-image: url(${require("@/assets/img/banner_sprite.png")});
但是这样无法获取到本地图片地址,我猜测是项目配置别名@craco
使得无法使用@获取到图片地址。
我的解决办法:
import url2 from '@/assets/img/banner_sprite.png'
background-image: url(${url2});
- 加载图片
使用useEffect()获取信息,使用useState保存信息状态,并赋予他们初始状态为[](空数组)。因为获取到的信息仍然为一个数组,若要取得属性,则要通过数组下标来获取,但是打印空数组[0]为undefined,此时在获取他的属性就会报错。
我的解决办法:
const Song = currentSong[0] || "";
const picUrl = (Song.al && Song.al.picUrl) || "";
若获取到的值为undefine,则赋予“”。
- 播放音乐滑动进度条后,进度条不会立即到滑动的位置,而是会先回弹一下。
原因:在获取当前音乐播放时间时,利setCurrentTime(e.target.currentTime * 1000)
,但是e.target.currentTime
无法更加实时的获得当前滑动的数据,所以或出现上述bug。
我的解决办法:在滑动结束后的回调函数冲重新更新进度条时间
//滑动结束后的位置
const sliderAfterChange = useCallback( (value) => {
//互动进度条后的进度条时间
const currentTime = value / 100 * duration /1000;
audioRef.current.currentTime = currentTime;
// 重新更新进度条时间
setCurrentTime(currentTime * 1000);
setIsChanging(false);
}, [duration] )
- 播放音乐滑动进度条后,时间刚开始会随着进度条的滑动而改变,转而瞬间按照正在播放的音乐的时间变化。
原因:当前时间没有放进isChanging中,(啊哈哈哈哈哈笑死了)
解决办法:
// 控制当前进度条是否正在改变
const [isChanging, setIsChanging] = useState(false);
//当前音乐播放时
const timeUpdate = (e) => {
// console.log(e.target.currentTime);
// e.target.currentTime(获得当前播放音乐的进度时间) *1000 转换为毫秒
if(!isChanging){
setProgress(currentTime / duration * 100);
setCurrentTime(e.target.currentTime * 1000);
}
}