今天就写一下歌曲播放这个功能,进度条拉伸以及歌曲时间的变化,当我们改变一个状态的时候,其他几个都相应改变,这个功能还是有一点复杂的…


就是这个,当我们可以播放,暂停,当我们播放的时候,进度条改变以及歌曲时间改变,当我们拉动进度条的时候,歌曲时间暂停,当我们松开的时候,直接跳到这个地方歌词的位置…

话不多说了,开整

一.在我们pages文件夹下,建立一个player文件夹,里面创建一个app-player-bar和一个store文件夹


二.那肯定是封装我们的网络请求了

在services文件夹下,创建一个player.js

import request from './request';
export function getSongDetail(ids) {return request({url: "/song/detail",params: {ids}})
}

三.我们需要在我们创建的player文件夹下的store里面去,把数据存到我们的redux里面,这里说一下,我是把所有网络请求的数据都放在我们的redux里面进行管理,而且每一个页面我都分别创建一个子redux,最后再把它们合并到主redux里面,通过redux-thunk

来到我们的store

首先是constants.js,定义常量啊

export const CHANGE_CURRENT_SONG = "player/CHANGE_CURRENT_SONG";

再是actionCreators.js,写方法

import { getSongDetail } from '@/services/player';
//引入我们player中封装的网络请求
import * as actionTypes from './constants';const changeCurrentSongAction = (currentSong) => ({type: actionTypes.CHANGE_CURRENT_SONG,currentSong
})export const getSongDetailAction = (ids) => {return dispatch => {getSongDetail(ids).then(res => {dispatch(changeCurrentSongAction(res.songs[0]));})}
}

再来到我们的reducer.js

import { Map } from 'immutable';
//提升性能,深拷贝,不清楚的可以百度一下
import * as actionTypes from './constants';const defaultState = Map({currentSong: {}
});function reducer(state = defaultState, action) {switch(action.type) {case actionTypes.CHANGE_CURRENT_SONG:return state.set("currentSong", action.currentSong);default:return state;}
}export default reducer;

之后就是index.js了,导出reducer

import reducer from './reducer';export {reducer
}

四.数据存到redux之后,我们需要到我们的主redux文件里面进行合并

reducer.js

// combineReducers:合成reducer,因为可能有多个reducer// 类似:immutable,深拷贝
import { combineReducers } from 'redux-immutable';//怕命名冲突,as是重命名
import { reducer as recommendReducer } from '../pages/discover/c-pages/recommend/store';
import { reducer as playerReducer } from '../pages/player/store';const cReducer = combineReducers({recommend: recommendReducer,   //这是我们之前demo中定义的,之前的博客player: playerReducer   //这是我们播放的子redux
});export default cReducer;

五.OK啦,数据全部弄好了,接下来可以去组件里面了

来到我们app-player-bar文件夹下,创建一个index.js,一个style.js

index.js

老规矩,还是分三步走

第一步,导入我们的配置

import React, { memo, useState, useEffect, useRef, useCallback } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';import { getSizeImage, formatDate, getPlaySong } from '@/utils/format-utils';import { Slider } from 'antd';import { getSongDetailAction } from '../store/actionCreators';
import {PlaybarWrapper,Control,PlayInfo,Operator
} from './style';

getSizeImage是我们改变图片大小的,formatDate是格式化时间戳的,getPlaySong是我们要获取音乐的mp3,网易云搞的坏事…Slider就是我们那个进度条,在antdui库里面获取的

format-utils.js

export function getSizeImage(imgUrl, size) {return `${imgUrl}?param=${size}x${size}`;
}export function formatDate(time, fmt) {let date = new Date(time);if (/(y+)/.test(fmt)) {fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));}let o = {'M+': date.getMonth() + 1,'d+': date.getDate(),'h+': date.getHours(),'m+': date.getMinutes(),'s+': date.getSeconds()};for (let k in o) {if (new RegExp(`(${k})`).test(fmt)) {let str = o[k] + '';fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));}}return fmt;
};function padLeftZero(str) {return ('00' + str).substr(str.length);
};export function formatMonthDay(time) {return formatDate(time, "MM月dd日");
}export function formatMinuteSecond(time) {return formatDate(time, "mm:ss");
}export function getPlaySong(id) {return `https://music.163.com/song/media/outer/url?id=${id}.mp3`;
}

第二步,我们的逻辑代码,这个有点复杂,我们也分几步好吧

useState

   const [currentTime, setCurrentTime] = useState(0);const [progress, setProgress] = useState(0);const [isChanging, setIsChanging] = useState(false);const [isPlaying, setIsPlaying] = useState(false);

解析

currentTime:我们歌曲播放的时间
progress:进度条的位置
isChanging: 因为我们滑动进度条的时候和我们歌曲播放的过程中,都会改变我们进度条的位置,这样它们两个就冲突了,啥意思呢,就是当我们把进度条滑到50,但是歌曲刚刚播放,这个时候进度条它就会马上回到我们播放的位置,两者冲突,所以必须有个判断
isPlaying:就是我们播放暂停,来回切换

useDispatch, useSelector, shallowEqual

const { currentSong } = useSelector(state => ({currentSong: state.getIn(["player", "currentSong"])}), shallowEqual);const dispatch = useDispatch();

解析

useSelector:获取我们redux中的state
shallowEqual:优化性能,有一个浅层比较
useDispatch:获取我们的dispatch

useRef,audioRef

 const audioRef = useRef();useEffect(() => {dispatch(getSongDetailAction(167876));}, [dispatch]);useEffect(() =>  {audioRef.current.src = getPlaySong(currentSong.id);}, [currentSong]);

解析

useRef:这是给我们audio标签绑定的一个ref<audio ref={audioRef}/>
useEffect:发送网络请求,获取我们redux里面的数据,通过dispatch,下面那个获取歌曲的src地址

处理一些我们的数据,比如转换时间戳啊,地址等等

  const picUrl = (currentSong.al && currentSong.al.picUrl) || "";const singerName = (currentSong.ar && currentSong.ar[0].name) || "未知歌手";const duration = currentSong.dt || 0;const showDuration = formatDate(duration, "mm:ss");const showCurrentTime = formatDate(currentTime, "mm:ss");

再来看下面的逻辑

1.歌曲的暂停与播放

   const playMusic = () => {isPlaying ? audioRef.current.pause(): audioRef.current.play();setIsPlaying(!isPlaying);}

2.当我们歌曲播放的时候,进度条和我们歌曲时间的改变

 const timeUpdate = (e) => {if (!isChanging) {setCurrentTime(e.target.currentTime * 1000);setProgress(currentTime / duration * 100);}}

3.当我们拉动进度条的时候,当 Slider 的值发生改变时,把改变后的值作为参数传入

 const sliderChange = useCallback((value) => {setIsChanging(true);const currentTime = value / 100 * duration;setCurrentTime(currentTime);setProgress(value);}, [duration]);

至于为什么用useCallback,useCallback主要用在,当把一个回调函数传到一个自定义组件内部时候用

4.把当前值作为参数传入,这个地方要注意我上面说的那种情况,播放时候和我们拉动进度条的时候冲突了,进度条的位置,不过我已经解决了在这

 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]);

好了,逻辑代码完毕

第三步,接下来是页面布局

return (<PlaybarWrapper className="sprite_player"><div className="content wrap-v2"><Control isPlaying={isPlaying}><button className="sprite_player prev"></button><button className="sprite_player play" onClick={e => playMusic()}></button><button className="sprite_player next"></button></Control><PlayInfo><div className="image"><a href="/#"><img src={getSizeImage(picUrl, 35)} alt="" /></a></div><div className="info"><div className="song"><span className="song-name">{currentSong.name}</span><a href="#/" className="singer-name">{singerName}</a></div><div className="progress"><Slider defaultValue={30} value={progress}onChange={sliderChange}onAfterChange={sliderAfterChange}/><div className="time"><span className="now-time">{showCurrentTime}</span><span className="divider">/</span><span className="duration">{showDuration}</span></div></div></div></PlayInfo><Operator><div className="left"><button className="sprite_player btn favor"></button><button className="sprite_player btn share"></button></div><div className="right sprite_player"><button className="sprite_player btn volume"></button><button className="sprite_player btn loop"></button><button className="sprite_player btn playlist"></button></div></Operator></div><audio ref={audioRef} onTimeUpdate={timeUpdate}/></PlaybarWrapper>)

这是这个页面所有代码

import React, { memo, useState, useEffect, useRef, useCallback } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';import { getSizeImage, formatDate, getPlaySong } from '@/utils/format-utils';import { Slider } from 'antd';import { getSongDetailAction } from '../store/actionCreators';
import {PlaybarWrapper,Control,PlayInfo,Operator
} from './style';export default memo(function HYAppPlayerBar() {// props and stateconst [currentTime, setCurrentTime] = useState(0);const [progress, setProgress] = useState(0);const [isChanging, setIsChanging] = useState(false);const [isPlaying, setIsPlaying] = useState(false);// redux hookconst { currentSong } = useSelector(state => ({currentSong: state.getIn(["player", "currentSong"])}), shallowEqual);const dispatch = useDispatch();// other hooksconst audioRef = useRef();useEffect(() => {dispatch(getSongDetailAction(167876));}, [dispatch]);useEffect(() =>  {audioRef.current.src = getPlaySong(currentSong.id);}, [currentSong]);// other handleconst picUrl = (currentSong.al && currentSong.al.picUrl) || "";const singerName = (currentSong.ar && currentSong.ar[0].name) || "未知歌手";const duration = currentSong.dt || 0;const showDuration = formatDate(duration, "mm:ss");const showCurrentTime = formatDate(currentTime, "mm:ss");// handle functionconst playMusic = () => {isPlaying ? audioRef.current.pause(): audioRef.current.play();setIsPlaying(!isPlaying);}const timeUpdate = (e) => {if (!isChanging) {setCurrentTime(e.target.currentTime * 1000);setProgress(currentTime / duration * 100);}}//useCallback:当把一个回调函数传到一个自定义组件内部时候用const sliderChange = useCallback((value) => {setIsChanging(true);const currentTime = value / 100 * duration;setCurrentTime(currentTime);setProgress(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]);return (<PlaybarWrapper className="sprite_player"><div className="content wrap-v2"><Control isPlaying={isPlaying}><button className="sprite_player prev"></button><button className="sprite_player play" onClick={e => playMusic()}></button><button className="sprite_player next"></button></Control><PlayInfo><div className="image"><a href="/#"><img src={getSizeImage(picUrl, 35)} alt="" /></a></div><div className="info"><div className="song"><span className="song-name">{currentSong.name}</span><a href="#/" className="singer-name">{singerName}</a></div><div className="progress"><Slider defaultValue={30} value={progress}onChange={sliderChange}onAfterChange={sliderAfterChange}/><div className="time"><span className="now-time">{showCurrentTime}</span><span className="divider">/</span><span className="duration">{showDuration}</span></div></div></div></PlayInfo><Operator><div className="left"><button className="sprite_player btn favor"></button><button className="sprite_player btn share"></button></div><div className="right sprite_player"><button className="sprite_player btn volume"></button><button className="sprite_player btn loop"></button><button className="sprite_player btn playlist"></button></div></Operator></div><audio ref={audioRef} onTimeUpdate={timeUpdate}/></PlaybarWrapper>)
})

style.js

import styled from 'styled-components';export const PlaybarWrapper = styled.div`position: fixed;left: 0;right: 0;bottom: 0;height: 52px;background-position: 0 0;background-repeat: repeat;.content {display: flex;align-items: center;justify-content: space-between;position: absolute;left: 50%;transform: translateX(-50%);bottom: 0;height: 47px;}
`export const Control = styled.div`display: flex;align-items: center;.prev, .next {width: 28px;height: 28px;}.prev {background-position: 0 -130px;}.play {width: 36px;height: 36px;margin: 0 8px;background-position: 0 ${props => props.isPlaying ? "-165px": "-204px"};}.next {background-position: -80px -130px;}
`export const PlayInfo = styled.div`display: flex;width: 642px;align-items: center;.image {width: 34px;height: 34px;border-radius: 5px;}.info {flex: 1;color: #a1a1a1;margin-left: 10px;.song {color: #e1e1e1;position: relative;top: 8px;left: 8px;.singer-name {color: #a1a1a1;margin-left: 10px;}}.progress {display: flex;align-items: center;.ant-slider {width: 493px;margin-right: 10px;.ant-slider-rail {height: 9px;background: url(${require("@/assets/img/progress_bar.png")}) right 0;}.ant-slider-track {height: 9px;background: url(${require("@/assets/img/progress_bar.png")}) left -66px;}.ant-slider-handle {width: 22px;height: 24px;border: none;margin-top: -7px;background: url(${require("@/assets/img/sprite_icon.png")}) 0 -250px;}}.time {.now-time {color: #e1e1e1;}.divider {margin: 0 3px;}}}}
`export const Operator = styled.div`display: flex;position: relative;top: 5px;.btn {width: 25px;height: 25px;cursor: pointer;}.favor {background-position: -88px -163px;}.share {background-position: -114px -163px;}.right {width: 126px;padding-left: 13px;background-position: -147px -248px;.volume {background-position: -2px -248px;}.loop {background-position: -3px -344px;}.playlist {width: 59px;background-position: -42px -68px;}}
`

好啦,最后一步,到我们的app.js中,将其引入

import React, { memo } from 'react'
//共享redux
import { Provider } from 'react-redux';// 可以将路由划分到一个文件里面
import {renderRoutes} from 'react-router-config'
import { HashRouter } from 'react-router-dom';
import routes from './router'
import store from './store';import LSHAppHeader from '@/components/app-header'
import LSHAppFooter from '@/components/app-footer'import HYAppPlayerBar from './pages/player/app-player-bar';export default memo(function App() {return (<Provider store={store}><HashRouter><LSHAppHeader/>{renderRoutes(routes)}<LSHAppFooter/><HYAppPlayerBar/></HashRouter></Provider>)
})

注意:有些是之前的demo

好啦,完成哈哈

如果有不清楚的话,可以去我的github,有源码,下一篇博客,就更新我们歌词显示这部分内容了,歌词滚动哦,激动吧

github项目地址:https://github.com/lsh555/WYY-Music

React实现(Web端)网易云音乐项目(四),错过了真的可惜呀相关推荐

  1. React实现(Web端)网易云音乐项目(一),错过了真的可惜呀

    首先肯定是搭建项目的结构了,通过脚手架安装这部分我就不说了 首先看项目的目录结构 assets:放我们的静态资源,图片,字体和公共初始样式等 common:放我们公共的JS文件 components: ...

  2. React实现(Web端)网易云音乐项目(三),错过了真的可惜呀

    接着前面的继续写了,这篇博客就写这两个页面,下一篇就主要讲歌曲播放功能,进度条拉伸以及歌曲时间的变化了 先完成新碟上架Demo 一.第一步不用说肯定是先获取我们这个数据对吧 去到我们services文 ...

  3. React实现(Web端)网易云音乐项目(六),错过了真的可惜呀

    今天实现歌曲播放时,歌词随着滚动的效果 网易云原本的歌词是这样的 [00:00.000] 作曲 : 许嵩 [00:01.000] 作词 : 许嵩 [00:22.240]天空好想下雨 [00:24.38 ...

  4. React实现(Web端)网易云音乐项目(二),错过了真的可惜呀

    接着上一篇来继续写了,这篇博客主要完成下面这部分 一.首先先完成这个轮播图了,那肯定需要请求数据了,所以我们先把网络请求部分先写好 在React里面我们也是通过axios来发送网络请求的,先安装 ya ...

  5. React实现(Web端)网易云音乐项目(五),错过了真的可惜呀

    今天我们做歌曲的单曲循环,按序播放,随机播放以及通过手动点击上一首,下一首这些功能哈,下一篇博客就写我们歌词滚动功能 由于我每篇都和前面是联系在一起的,如果想获取整个项目,可以去我的github下载源 ...

  6. PC端网易云音乐播放云盘音乐时显示加载失败,自动调转下一首的解决方法

    PC端网易云音乐播放云盘音乐时显示加载失败,自动调转下一首解决方法 注意: 一定要看看是不是和你的情况一样,不一样不要用这种方法!!! 具体情况: 我们经常会下载歌曲存到电脑文件夹里,然后通过网易云音 ...

  7. angular8 | 网易云音乐项目实战(一)

    angular8 网易云音乐项目实战(一) 视频教程原地址:https://www.bilibili.com/video/av70355308 本文为学习笔记 从github上clone相关源码 cs ...

  8. 微信小程序之网易云音乐(四)- 排行、歌手及歌手详情页模块开发

    微信小程序之网易云音乐(四)- 排行.歌手及歌手详情页模块开发 一. 排行模块开发 二. 歌手模块开发 三. 歌手详情页开发 微信小程序之网易云音乐导航 一. 排行模块开发 rank.vue文件: & ...

  9. Vue2 - 网易云音乐项目笔记(基于Vant UI组件库)

    目录 一.项目技术 二.准备工作 1.初始化Vue项目 2.配置Vant UI组件库 3.下载并使用vue-router库 4.接口API 5.postcss插件 三.分析页面实现功能 1.路由页面准 ...

最新文章

  1. ipython --pandas
  2. CF650C Table Compression
  3. 手把手教你安装Navicat——靠谱的Navicat安装教程
  4. 当前操作系统缺少黑体等字体_操作系统开发之——中断
  5. STM32F013 十元板
  6. vue 属性 watch
  7. K8S_Google工作笔记0004---平台规划和部署方式介绍_搭建k8s集群准备
  8. [leetcode]328. Odd Even Linked List
  9. (一)关于NLP的概念和处理过程
  10. 如果计算机黑屏请分析原因是什么原因,电脑黑屏是什么原因 电脑黑屏原因分析【解决方法】...
  11. matlab方差 anov,方差分析
  12. GAMIT处理GLONASS数据
  13. OpenJDK构建工具IcedTea 1.7发布
  14. Bazinga(HDU5510+KMP)
  15. 宁波银行成长的AB面:增长与合规不可兼得?管理漏洞不容忽视
  16. 解决问题CondaVerificationError: The package for olefile located at...
  17. J2EE平台WEB组件开发中如何使用定制标签
  18. 安卓隐藏摄像_一款可以隐藏录像的app
  19. 【老生谈算法】标准粒子群算法(PSO)及其Matlab程序和常见改进算法——粒子群算法
  20. TagSupport

热门文章

  1. c语言中:=和==的区别是什么?
  2. %d, %ld, %lld 区别
  3. 行为识别框架Slowfast解读
  4. python对数据集处理,绘制世界地图
  5. 01-计算机网络术语中英文对照
  6. 【沃顿商学院学习笔记】商业分析——Customer Analytics:05 营销中的新兴数据集:营销科学的未来 EMERGING DATA SETS IN MARKETING
  7. 20均线和60均线的关系和看点
  8. Lyndon的量化修炼之路——均线差与MACD联动策略(一)
  9. #pragma omp parallel for
  10. 5G套餐到底该不该换?看完你就明白啦!