前言:以下內容均為學習慕課網高級實戰課程的實踐爬坑筆記。
項目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重新計算 }
注:項目來自慕課網