【音乐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