【音樂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解碼
  1. 安裝js-base64依賴:
    npm install js-base64 --save
  2. common->js->song.js中:
    import {Base64} from 'js-base64'
    this.lyric = Base64.decode(res.lyric)//解碼 得到字符串

  • 解析字符串
  1. 安裝 第三方庫 lyric-parser
    npm install lyric-parser --save 
  2. 優化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()中添加判斷
  1. 當歌詞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)//滾動到頂部
     } }
  2. 此時,如果手動將歌詞滾動到其它位置,歌曲播放的當前歌詞還是會滾動到中間
五、播放器歌詞左右滑動

       需求:兩個點按鈕對應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空對象
  1. created()中創建touch空對象:因為touch只存取數據,不需要添加gettter和setter監聽
    created(){ this.touch = {} }
  2. <div class="middle">綁定touch事件:一定記得阻止瀏覽器默認事件
    <div class="middle" @touchstart.prevent="middleTouchStart" @touchmove.prevent="middleTouchMove" @touchend="middleTouchEnd">
  3. 實現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` }
  • 坑:
  1. 使用 <scroll class="middle-r" ref="lyricList">的引用改變其style是:this.$refs.lyricList.$el.style
  2. 使用 <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頁展示當前播放的歌詞
  1. 添加DOM結構:
    <div class="playing-lyric-wrapper">
        <div class="playing-lyric">{{playingLyric}}</div>
    </div>
  2. data中維護數據

    playingLyric: ''

     

  3. 在回調函數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詳解

  • vue中提供了一種混合機制--mixins,用來更高效的實現組件內容的復用
  • 組件在引用之后相當於在父組件內開辟了一塊單獨的空間,來根據父組件props過來的值進行相應的操作,但本質上兩者還是涇渭分明,相對獨立。
  • 而mixins則是在引入組件之后,則是將組件內部的內容如data等方法、method等屬性與父組件相應內容進行合並。相當於在引入后,父組件的各種屬性方法都被擴充了。
  1. 單純組件引用:父組件 + 子組件 >>> 父組件 + 子組件
  2. mixins:父組件 + 子組件 >>> new父組件
  • 值得注意的是,在使用mixins時,父組件和子組件同時擁有着子組件內的各種屬性方法,但這並不意味着他們同時共享、同時處理這些變量,兩者之間除了合並,是不會進行任何通信的
  • 具體使用以及內容合並策略請參照官方API及其他技術貼等
  1. https://cn.vuejs.org/v2/guide/mixins.html
  2. http://www.deboy.cn/Vue-mixins-advance-tips.html

——轉載自【木子墨博客】   

  • 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重新計算
    }

注:項目來自慕課網


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM