【音乐App】—— Vue-music 项目学习笔记:播放器内置组件开发(二)
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。
播放模式切换 | 歌词滚动显示 |
一、播放器模式切换功能实现 |
按钮样式随模式改变而改变
- 动态绑定iconMode图标class:
<i :class="iconMode"></i>
import {playMode} from '@/common/js/config' iconMode(){ return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random' }
- 给按钮添加点击事件,通过mapGetters获取mode,通过mapMutaions修改:
<div class="icon i-left" @click="changeMode">
changeMode(){ const mode = (this.mode + 1) % 3 this.setPlayMode(mode) } setPlayMode: 'SET_PLAY_MODE'
播放列表顺序随模式改变而改变
- common->js目录下:创建util.js,提供工具函数
function getRandomInt(min, max){ return Math.floor(Math.random() * (max - min + 1) + min) } //洗牌: 遍历arr, 从0-i 之间随机取一个数j,使arr[i]与arr[j]互换 export function shuffle(arr){ let _arr = arr.slice() //改变副本,不修改原数组 避免副作用 for(let i = 0; i<_arr.length; i++){ let j = getRandomInt(0, i) let t = _arr[i] _arr[i] = _arr[j] _arr[j] = t } return _arr }
- 通过mapGetters获取sequenceList,在changeMode()中判断mode,通过mapMutations修改playlist
changeMode(){ const mode = (this.mode + 1) % 3 this.setPlayMode(mode) let list = null if(mode === playMode.random){ list = shuffle(this.sequenceList) }else{ list = this.sequenceList } this.resetCurrentIndex(list) this.setPlayList(list) }
播放列表顺序改变后当前播放歌曲状态不变
- findIndex找到当前歌曲id值index,通过mapMutations改变currentIndex,保证当前歌曲的id不变
resetCurrentIndex(list){ let index = list.findIndex((item) => { //es6语法 findIndex return item.id === this.currentSong.id }) this.setCurrentIndex(index) }
- 坑:CurrentSong发生了改变,会触发watch中监听的操作,如果当前播放暂停,改变模式会自动播放
- 解决:添加判断,如果当前歌曲的id不变,认为CurrentSong没变,不执行任何操作
currentSong(newSong, oldSong) { if(newSong.id === oldSong.id) { return } this.$nextTick(() => { //确保DOM已存在 this.$refs.audio.play() }) }
当前歌曲播放完毕时自动切换到下一首或重新播放
- 监听audio派发的ended事件:@ended="end"
end(){ if(this.mode === playMode.loop){ this.loop() }else{ this.next() } }, loop(){ this.$refs.audio.currentTime = 0 this.$refs.audio.play() }
“随机播放全部”按钮功能实现
- music-list.vue中给按钮监听点击事件
@click="random"
- actions.js中添加randomPlay action
import {playMode} from '@/common/js/config' import {shuffle} from '@/common/js/util' export const randomPlay = function ({commit},{list}){ commit(types.SET_PLAY_MODE, playMode.random) commit(types.SET_SEQUENCE_LIST, list) let randomList = shuffle(list) commit(types.SET_PLAYLIST, randomList) commit(types.SET_CURRENT_INDEX, 0) commit(types.SET_FULL_SCREEN, true) commit(types.SET_PLAYING_STATE, true) }
- music-list.vue中定义random方法应用randomPlay
random(){ this.randomPlay({ list: this.songs }) } ...mapActions([ 'selectPlay', 'randomPlay' ])
- 坑:当点击了“随机播放全部”之后,再选择歌曲列表中指定的一首歌,播放的不是所选择的歌曲
- 原因:切换了随机播放之后,当前播放列表的顺序就不是歌曲列表的顺序了,但选择歌曲时传给currentIndex的index还是歌曲列表的index
- 解决:在actions.js中的selectPlay action中添加判断,如果是随机播放模式,将歌曲洗牌后存入播放列表,找到当前选择歌曲在播放列表中的index再传给currentIndex
function findIndex(list, song){ return list.findIndex((item) => { return item.id === song.id }) } export const selectPlay = function ({commit, state}, {list, index}) { //commit方法提交mutation commit(types.SET_SEQUENCE_LIST, list) if(state.mode === playMode.random) { let randomList = shuffle(list) commit(types.SET_PLAYLIST, randomList) index = findIndex(randomList, list[index]) }else{ commit(types.SET_PLAYLIST, list) } commit(types.SET_CURRENT_INDEX, index) commit(types.SET_FULL_SCREEN, true) commit(types.SET_PLAYING_STATE, true) }
二、播放器歌词数据抓取 |
- src->api目录下:创建song.js
import {commonParams} from './config' import axios from 'axios' export function getLyric(mid){ const url = '/api/lyric' const data = Object.assign({}, commonParams, { songmid: mid, pcachetime: +new Date(), platform: 'yqq', hostUin: 0, needNewCode: 0, g_tk: 5381, //会变化,以实时数据为准 format: 'json' //规定为json请求 }) return axios.get(url, { params: data }).then((res) => { return Promise.resolve(res.data) }) }
- webpack.dev.config.js中通过node强制改变请求头
app.get('/api/lyric', function(req, res){ var url="https://szc.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg" axios.get(url, { headers: { //通过node请求QQ接口,发送http请求时,修改referer和host referer: 'https://y.qq.com/', host: 'c.y.qq.com' }, params: req.query //把前端传过来的params,全部给QQ的url }).then((response) => { res.json(response.data) }).catch((e) => { console.log(e) }) })
- common->js->song.js中将获取数据的方法封装到class类
getLyric() { getLyric(this.mid).then((res) => { if(res.retcode === ERR_OK){ this.lyric = res.lyric //console.log(this.lyric) } }) }
- player.vue中调用getLyric()测试
currentSong(newSong, oldSong) { if(newSong.id === oldSong.id) { return } this.$nextTick(() => { //确保DOM已存在 this.$refs.audio.play() this.currentSong.getLyric()//测试 }) }
因为请求后QQ返回的仍然是一个jsonp, 需要在后端中做一点处理
- webpack.dev.config.js中通过正则表达式,将接收到的jsonp文件转换为json格式
var ret = response.data if (typeof ret === 'string') { var reg = /^\w+\(({[^()]+})\)$/ // 以单词a-z,A-Z开头,一个或多个 // \(\)转义括号以()开头结尾 // ()是用来分组 // 【^()】不以左括号/右括号的字符+多个 // {}大括号也要匹配到 var matches = ret.match(reg) if (matches) { ret = JSON.parse(matches[1]) // 对匹配到的分组的内容进行转换 } } res.json(ret)
注意:后端配置后都需要重新启动!!!
三、播放器歌词数据解析 |
- js-base64 code解码
- 安装js-base64依赖:
npm install js-base64 --save
- common->js->song.js中:
import {Base64} from 'js-base64' this.lyric = Base64.decode(res.lyric)//解码 得到字符串
- 解析字符串
- 安装 第三方库 lyric-parser:
npm install lyric-parser --save
- 优化getLyric:如果已经有歌词,不再请求
getLyric() { if(this.lyric){ return Promise.resolve() } return new Promise((resolve, reject) => { getLyric(this.mid).then((res) => { if(res.retcode === ERR_OK){ this.lyric = Base64.decode(res.lyric)//解码 得到字符串 // console.log(this.lyric) resolve(this.lyric) }else{ reject('no lyric') } }) }) }
- player.vue中使用lyric-parser,并在data中维护一个数据currentLyric
import Lyric from 'lyric-parser' //获取解析后的歌词 getLyric() { this.currentSong.getLyric().then((lyric) => { this.currentLyric = new Lyric(lyric)//实例化lyric对象 console.log(this.currentLyric) }) }
在watch的currentSong()中调用:this.getLyric()
四、播放器歌词滚动列表实现 |
显示歌词
- player.vue中添加DOM结构
<div class="middle-r" ref="lyricList"> <div class="lyric-wrapper"> <div v-if="currentLyric"> <p ref="lyricLine" class="text" v-for="(line, index) in currentLyric.lines" :key="index" :class="{'current': currentLineNum === index}"> {{line.txt}} </p> </div> </div> </div>
歌词随歌曲播放高亮显示
- 在data中维护数据
currentLineNum: 0
- 初始化lyric对象时传入handleLyric方法,得到当前currentLingNum值,判断如果歌曲播放,调用Lyric的play()
//获取解析后的歌词 getLyric() { this.currentSong.getLyric().then((lyric) => { //实例化lyric对象 this.currentLyric = new Lyric(lyric, this.handleLyric) // console.log(this.currentLyric) if(this.playing){ this.currentLyric.play() } }) }, handleLyric({lineNum, txt}){ this.currentLineNum = lineNum }
- 动态绑定current样式,高亮显示index为currentLineNum值的歌词
:class="{'current': currentLineNum === index}"
歌词实现滚动,歌曲播放时当前歌词滚动到中间显示
- 引用并注册scroll组件
import Scroll from '@/base/scroll/scroll'
- 使用<scroll>替换<div>,同时传入currentLyric和currentLyric.lines作为data
<scroll class="middle-r" ref="lyricList" :data="currentLyric && currentLyric.lines">
- 在handleLyric()中添加判断
- 当歌词lineNum大于5时,触发滚动,滚动到当前元素往前偏移第5个的位置;否则滚动到顶部
handleLyric({lineNum, txt}){ this.currentLineNum = lineNum if(lineNum > 5){ let lineEl = this.$refs.lyricLine[lineNum - 5] //保证歌词在中间位置滚动 this.$refs.lyricList.scrollToElement(lineEl, 1000) }else{ this.$refs.lyricList.scrollTo(0, 0, 1000)//滚动到顶部 } }
- 此时,如果手动将歌词滚动到其它位置,歌曲播放的当前歌词还是会滚动到中间
五、播放器歌词左右滑动 |
需求:两个点按钮对应CD页面和歌词页面,可切换
- 实现:data中维护数据currentShow,动态绑定active class:
currentShow: 'cd'
<div class="dot-wrapper"> <span class="dot" :class="{'active': currentShow === 'cd'}"></span> <span class="dot" :class="{'active': currentShow === 'lyric'}"></span> </div>
需求:切换歌词页面时,歌词向左滑,CD有一个渐隐效果;反之右滑,CD渐现
- 实现:【移动端滑动套路】—— touchstart、touchmove、touchend事件 touch空对象
- created()中创建touch空对象:因为touch只存取数据,不需要添加gettter和setter监听
created(){ this.touch = {} }
- <div class="middle">绑定touch事件:一定记得阻止浏览器默认事件
<div class="middle" @touchstart.prevent="middleTouchStart" @touchmove.prevent="middleTouchMove" @touchend="middleTouchEnd">
- 实现touch事件的回调函数:touchstart和touchmove的回调函数中要传入event,touchstart中定义初始化标志位initiated
//歌词滑动 middleTouchStart(e){ this.touch.initiated = true //初始化标志位 const touch = e.touches[0] this.touch.startX = touch.pageX this.touch.startY = touch.pageY }, middleTouchMove(e){ if(!this.touch.initiated){ return } const touch = e.touches[0] const deltaX = touch.pageX - this.touch.startX const deltaY = touch.pageY - this.touch.startY //维护deltaY原因:歌词本身Y轴滚动,当|deltaY| > |deltaX|时,不滑动歌词 if(Math.abs(deltaY) > Math.abs(deltaX)){ return } const left = this.currentShow === 'cd' ? 0 : -window.innerWidth const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX)) this.touch.percent = Math.abs(offsetWidth / window.innerWidth) //滑入歌词offsetWidth = 0 + deltaX(负值) 歌词滑出offsetWidth = -innerWidth + delta(正值) this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` this.$refs.lyricList.$el.style[transitionDuration] = 0 this.$refs.middleL.style.opacity = 1 - this.touch.percent //透明度随percent改变 this.$refs.middleL.style[transitionDuration] = 0 }, middleTouchEnd(){ //优化:手动滑入滑出10%时,歌词自动滑过 let offsetWidth let opacity if(this.currentShow === 'cd'){ if(this.touch.percent > 0.1){ offsetWidth = -window.innerWidth opacity = 0 this.currentShow = 'lyric' }else{ offsetWidth = 0 opacity = 1 } }else{ if(this.touch.percent < 0.9){ offsetWidth = 0 opacity = 1 this.currentShow = 'cd' }else{ offsetWidth = -window.innerWidth opacity = 0 } } const time = 300 this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` this.$refs.lyricList.$el.style[transitionDuration] = `${time}ms` this.$refs.middleL.style.opacity = opacity this.$refs.middleL.style[transitionDuration] = `${time}ms` }
- 坑:
- 使用 <scroll class="middle-r" ref="lyricList">的引用改变其style是:this.$refs.lyricList.$el.style
- 使用 <div class="middle-l" ref="middleL">的引用改变其style是:this.$refs.middleL.style
六、播放器歌词剩余功能 |
- 坑:切换歌曲后,歌词会闪动
- 原因:每次都会重新实例化Layric,但前一首的Layric中的定时器还在,造成干扰
- 解决:在Watch的currentSong()中添加判断,切换歌曲后,如果实例化新的Layric之前有currentLyric,清空其中的定时器
if(this.currentLyric){ this.currentLyric.stop() //切换歌曲后,清空前一首歌歌词Layric实例中的定时器 }
- 坑:歌曲暂停播放后,歌词还会继续跳动,并没有被暂停
- 解决:在togglePlaying()中判断如果存在currentLyric,就调用currentLyric的togglePlay()切换歌词的播放暂停
if(this.currentLyric){ this.currentLyric.togglePlay()//歌词切换播放暂停 }
- 坑:单曲循环播放模式下,歌曲播放完毕后,歌词并没有返回到一开始
- 解决:在loop()中判断如果存在currentLyric,就调用currentLyric的seek()将歌词偏移到最开始
if(this.currentLyric){ this.currentLyric.seek(0) //歌词偏移到一开始 }
- 坑:拖动进度条改变歌曲播放进度后,歌词没有随之改变到对应位置
- 解决:在onProgressBarChange()中判断如果存在currentLyric,就调用seek()将歌词偏移到currentTime*1000位置处
const currentTime = this.currentSong.duration * percent if(this.currentLyric){ this.currentLyric.seek(currentTime * 1000)//偏移歌词到拖动时间的对应位置 }
- 需求:CD页展示当前播放的歌词
- 添加DOM结构:
<div class="playing-lyric-wrapper"> <div class="playing-lyric">{{playingLyric}}</div> </div>
-
data中维护数据
playingLyric: ''
- 在回调函数handleLyric()中改变当前歌词:
this.playingLyric = txt
- 考虑异常情况:如果getLyric()请求失败,做一些清理的操作
getLyric() { this.currentSong.getLyric().then((lyric) => { //实例化lyric对象 this.currentLyric = new Lyric(lyric, this.handleLyric) // console.log(this.currentLyric) if(this.playing){ this.currentLyric.play() } }).catch(() => { //请求失败,清理数据 this.currentLyric = null this.playingLyric = '' this.currentLineNum = 0 }) }
- 考虑特殊情况:如果播放列表只有一首歌,next()中添加判断,使歌曲单曲循环播放;prev()同理
next() { if(!this.songReady){ return } if(this.playlist.length === 1){ //只有一首歌,单曲循环 this.loop() }else{ let index = this.currentIndex + 1 if(index === this.playlist.length){ index = 0 } this.setCurrentIndex(index) if(!this.playing){ this.togglePlaying() } this.songReady = false } }
- 优化:因为手机微信运行时从后台切换到前台时不执行js,要保证歌曲重新播放,使用setTimeout替换nextTick
setTimeout(() => { //确保DOM已存在 this.$refs.audio.play() // this.currentSong.getLyric()//测试歌词 this.getLyric() }, 1000)
七、播放器底部播放器适配+mixin的应用 |
- 问题:播放器收缩为mini-player之后,播放器占据列表后的一定空间,导致BScroll计算的高度不对,滚动区域受到影响
- mixin的适用情况:当多种组件都需要一种相同的逻辑时,引用mixin处可以将其中的代码添加到组件中
mixin详解
——转载自【木子墨博客】 |
- common->js目录下:创建mixin.js
import {mapGetters} from 'vuex' export const playlistMixin = { computed:{ ...mapGetters([ 'playlist' ]) }, mounted() { this.handlePlaylist(this.playlist) }, activated() { //<keep-alive>组件切换过来时会触发activated this.handlePlaylist(this.playlist) }, watch:{ playlist(newVal){ this.handlePlaylist(newVal) } }, methods: { //组件中定义handlePlaylist,就会覆盖这个,否则就会抛出异常 handlePlaylist(){ throw new Error('component must implement handlePlaylist method') } } }
- music-list.vue中应用mixin
import {playlistMixin} from '@/common/js/mixin' mixins: [playlistMixin]
定义handlePlaylist方法,判断如果有playlist,改变改变list的bottom并强制scroll重新计算
handlePlaylist(playlist){ const bottom = playlist.length > 0 ? '60px' : '' this.$refs.list.$el.style.bottom = bottom //底部播放器适配 this.$refs.list.refresh() //强制scroll重新计算 }
- singer.vue中同上:需要在listview.vue中暴露一个refresh方法后,再在singer.vue中调用
refresh() { this.$refs.listview.refresh() } handlePlaylist(playlist) { const bottom = playlist.length > 0 ? '60px' : '' this.$refs.singer.style.bottom = bottom //底部播放器适配 this.$refs.list.refresh() //强制scroll重新计算 }
- recommend.vue中同上:
handlePlaylist(playlist){ const bottom = playlist.length > 0 ? '60px' : '' this.$refs.recommend.style.bottom = bottom //底部播放器适配 this.$refs.scroll.refresh() //强制scroll重新计算 }
注:项目来自慕课网
越是迷茫、浮躁的时候,保持冷静和耐心,尤为重要