【音樂App】—— Vue2.0開發移動端音樂WebApp專案爬坑(三)
前言:在學習《慕課網音樂App》課程的過程中,第一次接觸並實踐了資料的跨域抓取和圍繞音樂播放展開的不同功能,也是這個專案帶給我最大的收穫,前面的實踐記錄分別總結了:推薦頁面開發和歌手頁面開發。這一篇主要梳理一下:音樂播放器的開發。專案github地址: ofollow,noindex">https://github.com/66Web/ljq_vue_music ,歡迎Star。
![]() |
![]() |
歌曲播放 | 歌詞播放 |
- 需求: 播放器可以通過歌手詳情列表、歌單詳情列表、排行榜、搜尋結果多種元件開啟,因此播放器資料一定是全域性的
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 } }
components->player目錄下:建立 player.vue
- 基礎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>
- 設定資料,提交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) }
- 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>, 回彈效果使用貝塞爾曲線
- 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)
-
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動畫即可:
- 利用第三方庫: create-keyframe-animation 使用js編寫CSS3動畫
-
安裝:
npm install create-keyframe-animation --save
引入:
import animations from 'create-keyframe-animation'
-
使用JavaScript/">JavaScript事件鉤子:
<transition name="normal" @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave">
-
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 } }
- 給cd-wrapper新增引用:
<div class="cd-wrapper" ref="cdWrapper">
- 定義事件鉤子方法:
//事件鉤子:建立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] = '' }
- transform屬性使用prefix自動新增字首:
import {prefixStyle} from '@/common/js/dom' const transform = prefixStyle('transform')
歌曲的播放
- 新增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>
- 通過mapGetters獲得playing播放狀態
- 通過mapMutations定義setPlayingState方法修改mutation:
setPlayingState: 'SET_PLAYING_STATE'
- 定義togglePlaying()修改mutation: 傳遞!playing為payload引數
togglePlaying(){ this.setPlayingState(!this.playing) }
- 在watch中監聽playing的變化,執行播放器的播放或暫停:
playing(newPlaying){ const audio = this.$refs.audio this.$nextTick(() => { //確保DOM已存在 newPlaying ? audio.play() : audio.pause() }) }
- 坑: 呼叫audio標籤的play()或pause(),都必須是在DOM audio已經存在的情況下 ,否則就會報錯
- 解決: 在this.$nextTick(() => { })中呼叫
圖示樣式隨播放暫停改變
- 動態繫結class屬性 playIcon,替換掉原原來的icon-play:
<i :class="playIcon" @click="togglePlaying"></i>
playIcon() { return this.playing ? 'icon-pause' : 'icon-play' }
CD 旋轉動畫效果
- 動態繫結class屬性 cdCls:
<div class="cd" :class="cdCls">
cdCls() { return this.playing ? 'play' : 'pause' }
- CSS樣式:
&.play animation: rotate 20s linear infinite &.pause animation-play-state: paused @keyframes rotate 0% transform: rotate(0) 100% ransform: rotate(360deg) View Code
- 給按鈕新增點選事件
<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有兩個事件:
- 當歌曲地址請求到時,會派發canplay事件 ;
- 當沒有請求到或請求錯誤時,會派發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>
- base->progress-bar目錄下:建立 progress-bar.vue
需求:進度條和小球隨著播放時間的變化而變化
- 實現:從父元件接收props引數:進度比 percent (player.vue中通過計算屬性得到)
- 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)` //小球偏移 } } }
需求:拖動進度條控制歌曲播放進度
- 實現: 監聽touchstart、touchmove、touchend事件,阻止瀏覽器預設行為 ;
<div class="progress-btn-wrapper" ref="progressBtn" @touchstart.prevent="progressTouchStart" @touchmove.prevent="progressTouchMove" @touchend="progressTouchEnd">
- created()中建立touch空物件,用於掛載共享資料 ;(在created中定義是因為touch不需要進行監聽)
created(){ this.touch = {} }
- 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)` //小球偏移 }
- watch中新增條件設定拖動時,進度條不隨歌曲當前進度而變化:
watch: { percent(newPercent) { if(newPercent >= 0 && !this.touch.initiated){ const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth const offsetWidth = newPercent * barWidth this._offset(offsetWidth) } } }
- 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() }
- base->progress-circle目錄下:建立progress-circle.vue
- progress-circle.vue 中使用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>
- progress-circle.vue需要從父元件接收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 //描邊偏移量 } }
按鈕樣式隨模式改變而改變
- 動態繫結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解碼
- 安裝 js-base64 依賴:
npm install js-base64 --save
- common->js-> song.js 中:
import {Base64} from 'js-base64' this.lyric = Base64.decode(res.lyric) //解碼 得到字串
解析字串
-
安裝 第三方庫 lyric-parser :用來例項化歌詞物件
npm install lyric-parser --save
- 優化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() —— lyric-parser的API
//獲取解析後的歌詞 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()中新增判斷: 當歌詞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)//滾動到頂部 } }
- 此時,如果手動將歌詞滾動到其它位置,歌曲播放的當前歌詞還是會滾動到中間
需求:兩個點按鈕對應CD頁面和歌詞頁面,可切換
- 實現:data中維護資料currentShow,動態繫結active class :
currentShow: 'cd' //預設顯示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空物件
- created()中建立touch空物件:因為touch只存取資料,不需要新增gettter和setter監聽
created(){ this.touch = {} }
- <div class="middle">繫結touch事件: 一定記得阻止瀏覽器預設事件
<div class="middle" @touchstart.prevent="middleTouchStart" @touchmove.prevent="middleTouchMove" @touchend="middleTouchEnd">
- 實現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` }
- 注意:
- 使用 < scroll class="middle-r" ref="lyricList">的引用改變其style是:this.$refs.lyricList. $el .style
- 使用 <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() //歌詞切換播放暫停 -- lyric-parser的API }
坑:單曲迴圈播放模式下,歌曲播放完畢後,歌詞並沒有返回到一開始
- 解決:在loop()中判斷如果存在currentLyric,就呼叫currentLyric的seek()將歌詞偏移到最開始
if(this.currentLyric){ this.currentLyric.seek(0)//歌詞偏移到一開始 -- lyric-parser的API }
坑:拖動進度條改變歌曲播放進度後,歌詞沒有隨之改變到對應位置
- 解決:在onProgressBarChange()中判斷如果存在currentLyric,就 呼叫seek()將歌詞偏移到currentTime*1000位置處
const currentTime = this.currentSong.duration * percent if(this.currentLyric){ this.currentLyric.seek(currentTime * 1000) //偏移歌詞到拖動時間的對應位置 }
需求:CD頁展示當前播放的歌詞
- 新增DOM結構:
<div class="playing-lyric-wrapper"> <div class="playing-lyric">{{playingLyric}}</div> </div>
- data中維護資料 playingLyric : ' '
- 在回撥函式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)
- 問題:播放器收縮為mini-player之後,播放器佔據列表後的一定空間,導致BScroll計算的高度不對,滾動區域受到影響
- mixin的適用情況:當多種元件都需要一種相同的邏輯時,引用mixin處可以將其中的程式碼新增到元件中
【vue中提供了一種混合機制--mixins,用來更高效的實現元件內容的複用】
- 元件在引用之後相當於在父元件內開闢了一塊單獨的空間,來根據父元件props過來的值進行相應的操作, 但本質上兩者還是涇渭分明,相對獨立。
- 而mixins則是在引入元件之後,則是將元件內部的內容如data等方法、method等屬性與父元件相應內容進行合併。 相當於在引入後,父元件的各種屬性方法都被擴充了。
- 單純元件引用: 父元件 + 子元件 >>> 父元件 + 子元件
- mixins: 父元件 + 子元件 >>> new父元件
- 值得注意的是,在使用mixins時,父元件和子元件同時擁有著子元件內的各種屬性方法, 但這並不意味著他們同時共享、同時處理這些變數,兩者之間除了合併,是不會進行任何通訊的
- 具體使用以及內容合併策略請參照官方API及其他技術貼等
——轉載自【木子墨部落格】
- 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重新計算 }
注:專案來自慕課網,本文整理只作學習