【音樂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 //描邊偏移量
     } }

 注:項目來自慕課網


免責聲明!

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



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