zoukankan      html  css  js  c++  java
  • 【音乐App】—— Vue-music 项目学习笔记:播放器内置组件开发(一)

    前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。

    项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。


    播放暂停 前进后退
    一、播放器Vuex数据设计
    • 需求: 播放器可以通过歌手详情列表、歌单详情列表、排行榜、搜索结果多种组件打开,因此播放器数据一定是全局的
    • state.js目录下:定义数据
      import {playMode} from '@/common/js/config'
      
      const state = {
              singer: {},
              playing: false, //播放状态
              fullScreen: false, //播放器展开方式:全屏或收起
              playlist: [], //播放列表(随机模式下与顺序列表不同)
              sequenceList: [], //顺序播放列表
              mode: playMode.sequence, //播放模式: 顺序、循环、随机
              currentIndex: -1 //当前播放歌曲的index(当前播放歌曲为playlist[index])
      }
    • common->js目录下:创建config.js配置项目相关
      //播放器播放模式: 顺序、循环、随机
      export const playMode = {
             sequence: 0, 
             loop: 1,
             random: 2
      }
    • getter.js目录下:数据映射(类似于计算属性)
      export const playing = state => state.playing
      export const fullScreen = state => state.fullScreen
      export const playlist = state => state.playlist
      export const sequenceList = state => state.sequenceList
      export const mode = state => state.mode
      export const currentIndex = state => state.currentIndex
      
      export const currentSong = (state) => {
                return state.playlist[state.currentIndex] || {}
      }

      组件中可以通过mapgetters拿到这些数据

    • mutaion-type.js目录下:定义事件类型字符串常量
      export const SET_PLAYING_STATE = 'SET_PLAYING_STATE'
      export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'
      export const SET_PLAYLIST = 'SET_PLAYLIST'
      export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST'
      export const SET_PLAY_MODE = 'SET_PLAY_MODE'
      export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'

      mutation中都是动作,前缀加SET、UPDATE等

    • mutaion.js目录下:操作state
      const mutations = {
             [types.SET_SINGER](state, singer){
                   state.singer = singer
             },
             [types.SET_PLAYING_STATE](state, flag){
                   state.playing = flag
             },
             [types.SET_FULL_SCREEN](state, flag){
                   state.fullScreen = flag
             },
             [types.SET_PLAYLIST](state, list){
                   state.playlist = list
             },
             [types.SET_SEQUENCE_LIST](state, list){
                   state.sequenceList = list
             },
             [types.SET_PLAY_MODE](state, mode){
                   state.mode = mode
             },
             [types.SET_CURRENT_INDEX](state, index){
                   state.currentIndex = index
             }
      } 
    二、播放器Vuex的相关应用
    • components->player目录下:创建player.vue
    1. 基础DOM:
      <div class="normal-player">
              播放器
      </div>
      <div class="mini-player"></div>
    •  App.vue中应用player组件:因为它不是任何一个路由相关组件,而是应用相关播放器,切换路由不会影响播放器的播放
      <player></player>
    • player.vue中获取数据:控制播放器的显示隐藏
      import {mapGetters} from 'vuex'
      
      computed: {
            ...mapGetters([
                 'fullScreen',
                 'playlist'
            ])
      }

      通过v-show判断播放列表有内容时,显示播放器,依据fullScreen控制显示不同的播放器

      <div class="player" v-show="playlist.length">
              <div class="normal-player" v-show="fullScreen">
                    播放器
              </div>
              <div class="mini-player" v-show="!fullScreen"></div>
      </div>
    • song-list.vue中添加点击播放事件:基础组件不写业务逻辑,只派发事件并传递相关数据
      @click="selectItem(song, index)
      selectItem(item, index){
            this.$emit('select', item, index)
      }

      子组件行为,只依赖本身相关,不依赖外部调用组件的需求,传出的数据可以不都使用

    • music-list.vue中监听select事件
      <song-list :songs="songs" @select="selectItem"></song-list>
    1. 设置数据,提交mutations:需要在一个动作中多次修改mutations,在actions.js中封装
      import * as types from './mutation-types'
      
      export const selectPlay = function ({commit, state}, {list, index}) {
               //commit方法提交mutation
               commit(types.SET_SEQUENCE_LIST, list)
               commit(types.SET_PLAYLIST, list)
               commit(types.SET_CURRENT_INDEX, index)
               commit(types.SET_FULL_SCREEN, true)
               commit(types.SET_PLAYING_STATE, true)
      }
    2. music-list.vue中代理actions,并在methods中调用:
      import {mapActions} from 'vuex' 
      
      selectItem(item, index){
              this.selectPlay({
                   list: this.songs,
                   index
              })
      }
      ...mapActions([
              'selectPlay'
      ])
    三、播放器基础样式及歌曲数据的应用
    •  通过mapGetter获取到currentSong数据填入到DOM中:点击切换播放器展开收起,需要修改fullScreen
      import {mapGetters, mapMutations} from 'vuex'
      
      methods: {
           back() {
                //错误做法: this.fullScreen = false
                //正确做法: 通过mapMutations写入 
                this.setFullScreen(false)
           },
           open() {
                this.setFullScreen(true)
           },
           ...mapMutations({
                setFullScreen: 'SET_FULL_SCREEN'
           })
      }
    四、播放器展开收起动画
    • 需求:normal-player背景图片渐隐渐现,展开时头部标题从顶部下落,底部按钮从底部回弹,收起时相反
    • 实现:动画使用<transition>,回弹效果使用贝塞尔曲线
    1. normal-player设置动画<transition name="normal">
      &.normal-enter-active, &.normal-leave-active
               transition: all 0.4s
               .top, .bottom
                    transition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32)
      &.normal-enter, &.normal-leave-to
               opacity: 0
               .top
                    transform: translate3d(0, -100px, 0)
               .bottom
                    transform: translate3d(0, 100px, 0)
    2. mini-player设置动画<transition name="mini">
      &.mini-enter-active, &.mini-leave-active
            transition: all 0.4s
      &.mini-enter, &.mini-leave-to
            opacity: 0
    • 需求:展开时,mini-player的专辑图片从原始位置飞入CD图片位置,同时有一个放大缩小效果, 对应顶部和底部的回弹;收起时,normal-player的CD图片从原始位置直接落入mini-player的专辑图片位置
    • 实现:Vue提供了javascript事件钩子,在相关的钩子中定义CSS3动画即可
    1. 利用第三方库:create-keyframe-animation 使用js编写CSS3动画
    2. github地址:https://github.com/HenrikJoreteg/create-keyframe-animation
    3. 安装: 
      npm install create-keyframe-animation --save  
    4. 引入:
      import animations from 'create-keyframe-animation'
      <transition name="normal" @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave">
    5. methods中封装函数_getPosAndScale获取初始位置及缩放尺寸: (计算以中心点为准
      _getPosAndScale(){ 
             const targetWidth = 40 //mini-player icon宽度
             const width = window.innerWidth * 0.8 //cd-wrapper宽度
             const paddingLeft = 40 
             const paddingTop = 80
             const paddingBottom = 30 //mini-player icon中心距底部位置
             const scale = targetWidth / width
             const x = -(window.innerWidth / 2 - paddingLeft) //X轴方向移动的距离
             const y = window.innerHeight - paddingTop - width / 2 - paddingBottom
             return {
                   x,
                   y, 
                   scale
             }
      }
    6. 给cd-wrapper添加引用:
      <div class="cd-wrapper" ref="cdWrapper">
    7. 定义事件钩子方法:
      //事件钩子:创建CSS3动画
      enter(el, done){
              const {x, y, scale} = this._getPosAndScale()
      
              let animation = {
                     0: {
                        transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`
                     },
                     60: {
                        transform: `translate3d(0, 0, 0) scale(1.1)`
                     }, 
                     100: {
                        transform: `translate3d(0, 0, 0) scale(1)`
                     }
              }
      
              animations.registerAnimation({
                     name: 'move',
                     animation,
                     presets: {
                        duration: 400,
                        easing: 'linear'
                     }
              })
      
             animations.runAnimation(this.$refs.cdWrapper, 'move', done)
      },
      afterEnter() {
             animations.unregisterAnimation('move')
             this.$refs.cdWrapper.style.animation = ''
      },
      leave(el, done){
             this.$refs.cdWrapper.style.transition = 'all 0.4s'
             const {x, y, scale} = this._getPosAndScale()
             this.$refs.cdWrapper.style[transform] = `translate3d(${x}px, ${y}px, 0) scale(${scale})`
             this.$refs.cdWrapper.addEventListener('transitionend', done)
      },
      afterLeave(){
             this.$refs.cdWrapper.style.transition = ''
             this.$refs.cdWrapper.style[transform] = ''
      }
    8. transform属性使用prefix自动添加前缀:
      import {prefixStyle} from '@/common/js/dom'
      const transform = prefixStyle('transform')
    五、播放器歌曲播放功能实现--H5 audio
    • 添加H5 <audio>实现歌曲的播放
      <audio :src="currentSong.url" ref="audio"></audio>
    • 在watch中监听currentSong的变化,播放歌曲
      watch: {
          currentSong() {
                this.$nextTick(() => { //确保DOM已存在
                      this.$refs.audio.play()
                })
          }
      }
    • 给按钮添加点击事件,控制播放暂停
      <i class="icon-play" @click="togglePlaying"></i>
    1. 通过mapGetters获得playing播放状态
    2. 通过mapMutations定义setPlayingState方法修改mutation:
      setPlayingState: 'SET_PLAYING_STATE'
    3. 定义togglePlaying()修改mutation:传递!playing为payload参数
      togglePlaying(){
           this.setPlayingState(!this.playing)
      }
    4. 在watch中监听playing的变化,执行播放器的播放或暂停:
      playing(newPlaying){
              const audio = this.$refs.audio
              this.$nextTick(() => { //确保DOM已存在
                      newPlaying ? audio.play() : audio.pause()
              })
      }
    5. 坑:调用audio标签的play()或pause(),都必须是在DOM audio已经存在的情况下,否则就会报错
    6. 解决: 在this.$nextTick(() => { })中调用
    • 图标样式随播放暂停改变:动态绑定class属性playIcon,替换掉原原来的icon-play
      <i :class="playIcon" @click="togglePlaying"></i>
      playIcon() {
            return this.playing ? 'icon-pause' : 'icon-play'
      }
    • CD 旋转动画效果
    1. 动态绑定class属性cdCls:
      <div class="cd" :class="cdCls">
      cdCls() {
           return this.playing ? 'play' : 'pause'
      }
    2. CSS样式:
      &.play
           animation: rotate 20s linear infinite
      &.pause
           animation-play-state: paused
      
      @keyframes rotate
          0%
               transform: rotate(0)
          100%
               transform:  rotate(360deg)
    六、播放器歌曲前进后退功能实现
    • 给按钮添加点击事件
      <i class="icon-prev" @click="prev"></i>
      <i class="icon-next" @click="next"></i>
    • 通过mapGetters获得currentIndex当前歌曲index

    • 通过mapMutations定义setCurrentIndex方法修改mutation
      setCurrentIndex: 'SET_CURRENT_INDEX'
    • 定义prev()和next()修改mutation: 限制index边界
      next() {
         let index = this.currentIndex + 1
         if(index === this.playlist.length){
             index = 0
         }
         this.setCurrentIndex(index)
      },
      prev() {
         let index = this.currentIndex - 1
         if(index === -1){
             index = this.playlist.length - 1
         }
         this.setCurrentIndex(index)
      }
    • 坑:前进或后退后会自动开始播放,但播放按钮的样式没有改变
    • 解决:添加判断,如果当前是暂停状态, 切换为播放
      if(!this.playing){
          this.togglePlaying()
      }
    • 坑:切换太快会出现报错:Uncaught (in promise) DOMException: The play() request was interrupted by a new load request
    • 原因:切换太快audio 数据还没有加载好
    • 解决:audio W3C文档中记录,audio有两个事件:
    1. 当歌曲地址请求到时,会派发canplay事件;
    2. 当没有请求到或请求错误时,会派发error事件
      <audio :src="currentSong.url" ref="audio" @canplay="ready" @error="error"></audio>

      在data中维护一个标志位数据songReady,通过ready方法控制只有歌曲数据请求好后,才可以播放

      data() {
         return {
              songReady: false
         }
      }
      ready() {
          this.songReady = true
      }

      在prev()、next()和togglePlaying中添加判断,当歌曲数据还没有请求好的时候,不播放

      if(!this.songReady){
         return
      }

      其中prev()和next()中歌曲发生改变了之后,重置songReady为false,便于下一次ready()

      this.songReady = false
    • 坑:当没有网络,或切换歌曲的url有问题时,songReady就一直为false,所有播放的逻辑就执行不了了
    • 解决: error()中也使songReady为true,这样既可以保证播放功能的正常使用,也可以保证快速点击时不报错
    • 优化: 给按钮添加disable的样式
      <div class="icon i-left" :class="disableCls">
      <div class="icon i-center" :class="disableCls">
      <div class="icon i-right" :class="disableCls">
      disableCls() {
          return this.songReady ? '' : 'disable'
      }
      &.disable
          color: $color-theme-d
    七、播放器时间获取
    • data中维护currentTime当前播放时间:currentTime: 0 (audio的可读写属性
    • audio中监听时间更新事件:
      @timeupdate="updateTime"
    • methods中定义updateTime()获取当前时间的时间戳,并封装format函数格式化:
      //获取播放时间
      updateTime(e) {
          this.currentTime = e.target.currentTime //时间戳
      },
      format(interval){
          interval = interval | 0 //向下取整
          const minute = interval / 60 | 0
          const second = this._pad(interval % 60)
          return `${minute}:${second}`
      }
    • 坑:秒一开始显示个位只有一位数字,体验不好
    • 解决:定义_pad()用0补位
      _pad(num, n = 2){ //用0补位,补2位字符串长度
          let len = num.toString().length
          while(len < n){
               num = '0' + num
               len++
          }
          return num
      }
    • 格式化后的数据填入DOM,显示当前播放时间和总时间:
      <span class="time time-l">{{format(currentTime)}}</span>
      <span class="time time-r">{{format(currentSong.duration)}}</span>
    八、播放器progress-bar进度条组件实现
    • base->progress-bar目录下:创建progress-bar.vue

           需求:进度条和小球随着播放时间的变化而变化

    • 实现:
    1. 从父组件接收props参数:进度比percentplayer.vue中通过计算属性得到)
    2. watch中监听percent,通过计算进度条总长度和偏移量,动态设置进度条的width和小球的transform
      const progressBtnWidth = 16 //通过样式设置得到
      
      props: {
          percent: {
             type: Number,
             default: 0
          }
      },
      watch: {
          percent(newPercent) {
            if(newPercent >= 0){
               const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
               const offsetWidth = newPercent * barWidth
               this.$refs.progress.style.width = `${offsetWidth}px` //进度条偏移
               this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` //小球偏移
           }
         }
      }

           需求:拖动进度条控制歌曲播放进度

    • 实现:
    1. 监听touchstart、touchmove、touchend事件,阻止浏览器默认行为;
      <div class="progress-btn-wrapper" ref="progressBtn"
           @touchstart.prevent="progressTouchStart"
           @touchmove.prevent="progressTouchMove"
           @touchend="progressTouchEnd">
    2. created()中创建touch空对象,用于挂载共享数据;
      created(){
         this.touch = {}
      }
    3. methods中定义3个方法,通过计算拖动偏移量得到进度条总偏移量,并派发事件给父组件:
      progressTouchStart(e) {
            this.touch.initiated = true //标志位 表示初始化
            this.touch.startX = e.touches[0].pageX //当前拖动点X轴位置
            this.touch.left = this.$refs.progress.clientWidth //当前进度条位置
      },
      progressTouchMove(e) {
            if(!this.touch.initiated){
                return
            }
            const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
            const deltaX = e.touches[0].pageX - this.touch.startX //拖动偏移量
            const offsetWidth = Math.min(barWidth, Math.max(0, this.touch.left + deltaX))
            this._offset(offsetWidth)
      },
      progressTouchEnd() {
            this.touch.initiated = false
            this._triggerPercent()
      },
      _triggerPercent(){
            const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
            const percent = this.$refs.progress.clientWidth / barWidth
            this.$emit('percentChange', percent)
      },
      _offset(offsetWidth){
            this.$refs.progress.style.width = `${offsetWidth}px` //进度条偏移
            this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` //小球偏移
      }
    4. watch中添加条件设置拖动时,进度条不随歌曲当前进度而变化:
      watch: {
          percent(newPercent) {
               if(newPercent >= 0 && !this.touch.initiated){
                  const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
                  const offsetWidth = newPercent * barWidth
                  this._offset(offsetWidth)
               }
          }
      }
    5. player.vue组件中监听percentChange事件,将改变后的播放时间写入currentTime,并设置改变后自动播放:
      @percentChange="onProgressBarChange"
      onProgressBarChange(percent) {
           this.$refs.audio.currentTime = this.currentSong.duration * percent
           if(!this.playing){
               this.togglePlaying()
           }
      }

           需求:点击进度条任意位置,改变歌曲播放进度

    • 实现:添加点击事件,通过react.left计算得到偏移量,设置进度条偏移,并派发事件改变歌曲播放时间
      <div class="progress-bar" ref="progressBar" @click="progressClick">
      progressClick(e) {
           const rect = this.$refs.progressBar.getBoundingClientRect()
           const offsetWidth = e.pageX - rect.left
           this._offset(offsetWidth)
           this._triggerPercent()
      }
    九、播放器progress-circle圆形进度条实现 -- SVG
    • base->progress-circle目录下:创建progress-circle.vue
    1. 使用SVG实现圆:
      <div class="progress-circle">
              <!-- viewBox 视口位置 与半径、宽高相关 stroke-dasharray 描边虚线 周长2πr stroke-dashoffset 描边偏移 未描边部分-->
              <svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1"
                    xmlns="http://www.w3.org/2000/svg">
                   <circle class="progress-backgroud" r="50" cx="50" cy="50" fill="transparent"/>
                   <circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" 
                           :stroke-dasharray="dashArray" :stroke-dashoffset="dashOffset"/>
              </svg>
              <slot></slot>
      </div>  
    2. 需要从父组件接收props参数:视口半径、当前歌曲进度百分比
      props: {
          radius: {
             type: Number,
             default: 100
          },
          percent: {
             type: Number,
             default: 0
         }
      }
    • player.vue中使圆形进度条包裹mini-player的播放按钮,并传入半径和百分比:
      <progress-circle :radius="radius" :percent="percent"><!-- radius: 32 -->
           <i :class="miniIcon" @click.stop.prevent="togglePlaying" class="icon-mini"></i>
      </progress-circle>
    • progress-circle.vue中维护数据dashArray,并使用computed计算出当前进度对应的偏移量:
      data() {
          return {
             dashArray: Math.PI * 100 //圆周长 描边总长
          }
      },
      computed: {
          dashOffset() { 
             return (1 - this.percent) * this.dashArray //描边偏移量
         }
      }

     注:项目来自慕课网

  • 相关阅读:
    【HDU】2461 Rectangles
    【POJ】2409 Let it Bead
    【HDU】3208 Integer’s Power
    【HDU】1812 Count the Tetris
    【POJ】2888 Magic Bracelet
    【HDU】2865 Birthday Toy
    黑白色的华为(6) 不是银弹的银弹
    黑白色的华为(10) 不是答案的答案
    ubuntu上升级cmake到3.16版本
    黑白色的华为(8) 从加法到减法
  • 原文地址:https://www.cnblogs.com/ljq66/p/10167951.html
Copyright © 2011-2022 走看看