zoukankan      html  css  js  c++  java
  • 【音乐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重新计算
      }

    注:项目来自慕课网

  • 相关阅读:
    第十九节,使用RNN实现一个退位减法器
    深度学习系列经典博客收藏
    第十八节,TensorFlow中使用批量归一化(BN)
    第十七节,深度学习模型的训练技巧-优化卷积核,多通道卷积
    第十六节,使用函数封装库tf.contrib.layers
    第十五节,利用反卷积技术复原卷积网络各层图像
    第十四节,TensorFlow中的反卷积,反池化操作以及gradients的使用
    第十三节,使用带有全局平均池化层的CNN对CIFAR10数据集分类
    第十二节,TensorFlow读取数据的几种方法以及队列的使用
    在hadoop集群添加了slave节点的方法
  • 原文地址:https://www.cnblogs.com/ljq66/p/10167963.html
Copyright © 2011-2022 走看看