vue實現音樂播放器實戰筆記

原文連接:https://blog.csdn.net/Forever201295/article/details/80266600css

 

1、項目說明
該播放器的是基於學習vue的實戰練習,不用於其餘途徑。應用中的所有數據來自於 QQ音樂 移動端(https://m.y.qq.com/),利用 jsonp 以及 axios 代理後端請求抓取。html

2、目錄結構
目錄/文件         說明
api                    與後臺數據交互文件
base                 一些與業務邏輯無關的基礎組件,例如輪播圖:slider組件
common           存放圖片,字體,樣式,以及js插件等公共資源
components     業務邏輯代碼
router                項目路由
store                 vuex狀態管理配置
3、base組件
一、輪播圖slider
引入組件 better-scroll前端

1.一、參數設置
loop:是否循環播放
autoPlay:是否自動播放
interval:自動播放的間隔時間
2.一、實現
(1).須要經過獲取slider的寬度來設置每個輪播圖和輪播圖的包裹層的寬度
(2).初始化better-scroll實例
若設置 loop爲true 會自動 clone 兩個輪播插在先後位置,若是輪播循環播放,是先後各加一個輪播圖保證無縫切換,因此須要再加兩個寬度vue

if (this.loop) {
width += 2 * sliderWidth
}

(3). 給slider綁定’scrollEnd‘事件,來獲取當前滾動值currentPageIndex
(4).dots小圓點的active狀態。經過currentPageIndex === index 來判斷
(5).爲了保證改變窗口大小依然正常輪播,監聽窗口 resize 事件,經過better-scroll提供的refresh()從新渲染輪播圖node

window.addEventListener('resize', () => {
if (!this.slider) {
return
}
this._setSliderWidth(true)
this.slider.refresh()
})
}

(6)在組件銷燬以前 beforeDestroy 銷燬定時器webpack

2 播放進度條組件
2.1 全屏下 條狀滾動條progeress-bar
參數設置
percent:顯示當前播放進度
實現
a. 拖拽按鈕時候:監聽touchstart,touchmove,touchend事件
touchstart: 獲取第一次點擊的橫座標clinetX:startX,整個progress的clientWidth:left。
touchmove:獲取移動後的橫座標,計算對應的delta,此時進度條的位置 = clinetWidth+delta
touchend:派發出percent。從而改變progress的width
progressTouchStart(e) {
this.touch.startX = e.touches[0].clientX
this.touch.left = this.$refs.progress.clientWidth
},
progressTouchMove(e) {
let delta = e.touches[0].clientX - this.touch.startX
let offsetWidth = this.touch.left + delta
this._offset(offsetWidth)
},
progressTouchEnd() {
this._triggerPercent()
},

b. 點擊時候:也是經過點擊的位置計算出progress的寬度。ios

progressClick(e) {
const rect = this.$refs.progressBar.getBoundingClientRect()
const offsetWidth = e.pageX - rect.left
this._offset(offsetWidth)
this._triggerPercent()
},

注:getBoundingClientRect用於得到頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置。
getBoundingClientRect是DOM元素到瀏覽器可視範圍的距離(不包含文檔卷起的部分),
該函數返回一個Object對象,該對象有6個屬性:top,lef,right,bottom,width,height;css3

2.1 mini 圓形滾動條progeress-circle
參數
radius:設置圓形的直徑
percent:當前進度
實現
圓形採用svg,中有兩個圓,一個是背景圓形,另外一個爲已播放的圓形進度,圓形進度主要用了stroke-dasharray(描邊距離) 和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-background" 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>

viewBox = 0 0 100 100 100 是相對於svg裏面的設置
傳入radius爲32: 100 =》直徑 32
r = 50 :50 =》半徑 16
stroke-dasharray : 描邊距離 (這裏對應爲 math.PI * 100)
stroke-dashoffset: 描邊偏移距離 (設置爲 314,則偏移了314,這個圓就沒有了。設置爲0 ,這個圓徹底顯示)git

computed: {
dashOffset() {
return (1 - this.percent) * this.dashArray //只需根據percent來stroke-dashoffset便可顯示進度
}
}

4、api拿後端數據
組件中的數據全都是拿了qq音樂網頁版的數據,拿數據的方式有兩種,一種能夠直接經過jsonp跨域來獲取的,另外一種接口經過referer僞造請求,
1.jsonp方式
在common中,封裝一個公用jsonp方法es6

import originJsonp from 'jsonp'

export default function jsonp(url, data, option) {
url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)

return new Promise((resolve, reject) => {
originJsonp(url, option, (err, data) => {
if (!err) {
resolve(data)
} else {
reject(err)
}
})
})
}

2.僞造請求
一些接口在後臺簡單設置一下 Referer, Host,能夠限制前臺直接經過瀏覽器抓到你的接口,可是這種方式防不了後端代理的方式,前端 XHR 會有跨域限制,後端發送 http 請求則沒有限制,所以能夠僞造請求
vue提供的axios能夠在瀏覽器端發送 XMLHttpRequest 請求,在服務器端發送 http 請求獲取;
在webpack.dev.config中配置以下

var express = require('express')
var axios = require('axios')
var app = express()
var apiRoutes = express.Router()
before(apiRoutes){
apiRoutes.get('/api/getDiscList',(req,res)=>{
const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg';
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query //這是請求的query
}).then((response) => {
//response是url地址返回的,數據在data裏。
res.json(response.data)
}).catch((e) => {
console.log(e);
})
});
app.use('/api', apiRoutes);
},
}

定義一個路由,拿到一個 /api/getDiscList 接口,經過 axios 僞造 headers,發送給QQ音樂服務器一個 http 請求,獲得服務端正確的響應,經過 res.json(response.data) 返回到瀏覽器端;
注意:此時的這個接口返回的格式已是json,應該設置format:json
那麼問題來了,大公司怎麼防止被惡意代理呢?當你的訪問量大的時候,出口ip就會可能被查到獲取封禁,還有一種方式就是參數驗籤,也就是請求人家的數據必須帶一個簽名參數,而後這個簽名參數是很難拿到的這個正確的簽名,從而達到保護數據的目的;

5、components業務邏輯代碼
一、推薦頁面
Scroll 初始化但卻沒有滾動,是由於初始化時機不對,必須保證數據到來,DOM 成功渲染以後 再去進行初始化
可使用父組件 給 Scrol組件傳 :data 數據,Scroll 組件本身 watch 這個 data,有變化就馬上 refesh 滾動
對應圖片能夠經過監聽onload事件,來進行滾動刷新

<img @load="loadImage" class="needsclick" :src="item.picUrl">
loadImage() {
if (!this.checkloaded) {
this.checkloaded = true
this.$refs.scroll.refresh()
}
}

新版本 BScroll 已經本身實現檢測 DOM 變化,自動刷新,大部分場景下無需傳 data 了

二、歌手頁面
2.一、數據重構
歌手頁面的結構是 熱門、 A-Z 的順序排列,咱們這裏只抓取100條數據,觀察其數據是亂序的,但咱們能夠利用數據的 Findex 進行數據的重構
1.首先能夠定義一個 map 結構

let map = {
hot: {
title: HOT_NAME,
item: []
}
}

接着遍歷獲得的數據,將前10條添加到熱門 hot 裏
而後查看每條的 Findex ,若是 map[Findex] 沒有,建立 map[Findex] push 進新條目,若是 map[Findex] 有,則向其 push 進新條目

list.forEach((item, index) => {
if (index < HOT_SINGER_LEN) {
map.hot.item.push(new SingerFormat({
id: item.Fsinger_mid,
name: item.Fsinger_name,
}))
}
const key = item.Findex
if (!map[key]) {
map[key] = {
title: key,
items: []
}
}
map[key].items.push(new SingerFormat({
id: item.Fsinger_mid,
name: item.Fsinger_name
}))
})

這樣就獲得了一個 符合咱們基本預期的 map 結構,可是由於 map 是一個對象,數據是亂序的,Chrome 控制檯在展現的時候會對 key 作排序,但實際上咱們代碼並無作。
因此還要將其進行排序,這裏會用到 數組的 sort 方法,因此咱們要先把 map對象 轉爲 數組

let hot = []
let ret = []
let un = []
for (let key in map) {
let val = map[key]
if (val.title.match(/[a-zA-z]/)) {
ret.push(val)
} else if (val.title === HOT_NAME) {
hot.push(val)
} else {
un.push(val)
}
}
ret.sort((a, b) => {
return a.title.charCodeAt(0) - b.title.charCodeAt(0)
})
return hot.concat(ret, un)

這樣就拿到一個相似[hot,a,b,c…….]這樣符合需求的按規律排的數組。

2.二、錨點操做控制主區塊列表
實現效果:點擊或滑動 shortcut 不一樣的錨點 ,自動滾動至相應的標題列表
實現思惟:得到每一次操做shortcut上對應的index,想辦法經過index來設置左邊區塊的滾動值
1.如何得到index值
a.點擊時候:循環的時候將給dom綁上data-index屬性,寫上當前index。點擊時候經過DOM操做獲取(e.target對象的getAttribute)
b.滑動時候:第一次點擊觸碰 shortcut ,記錄觸碰位置的 index,y座標值,在touchmove事件中拿到第二次觸碰shortcut的y座標值y2,將兩次觸碰的位置的差值處理成索引上的 delta 差值,從而能夠拿到第二次觸碰的index值。
2.怎麼經過index設置滾動值
利用BScroll的scrollToElement能夠設置content滾動到某個DOM位置

scrollToElement() {
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
}

經過index值能夠知道當前content應該滾動到第幾個標題列表裏面

_scrollTo(index) {
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) //listgroup表明左邊標題區塊
}

2.三、滑動主列表控制錨點
實現效果:滑動主列表,側邊 shortcut 自動高亮不一樣錨點
實現思惟:實時監聽主列表的滑動事件,獲得每次滑動的值scrollY,設計一個listHeight數組存放每個主列表到瀏覽器頂端的距離,比對scrollY存放在哪一個listHeight的區間中獲得currentIndex數值,這個currentIndex就對應着shortcut需高亮的錨點
1.scrollY的獲取
a.獲取滾動值:添加參數「listenScroll」來設置是否實時監聽content的滾動事件,並向父組件派發事件scroll傳出當前的滾動值:pos.y

if (this.listenScroll) {
let me = this
this.scroll.on('scroll', (pos) => { // 實時監測滾動事件,派發事件:Y軸距離
me.$emit('scroll', pos)
})
}

b.父組件監聽到滾動派發的事件,並將值存入scrollY

@scroll="scroll" //template調用組件時綁定
scroll(pos) {
this.scrollY = pos.y // 實時獲取 BScroll 滾動的 Y軸距離
}

2.listHeight的設計實現

_calculateHeight(){
const lists = this.$refs.listGroup //listGroup爲主列表區塊
let height = 0;
this.listHeight.push(height)
for(let i=0; i<lists.length; i++){
height += lists[i].clientHeight
this.listHeight.push(height)
}
},

三、經過實時watch scrollY的值,比對listHeight,拿到當前content滾動值落在哪一個區間,也就拿到了currentIndex

scrollY(newY){ //獲取的瀏覽器滾動的值均爲負數
const listHeight = this.listHeight
if(newY > 0){ //當滾動到屏幕頂部時
newY = 0
return
}
for (let i = 0; i < listHeight.length - 1; i++){
let hei_1 = listHeight[i]
let hei_2 = listHeight[i+1]
if(-newY >= hei_1 && -newY < hei_2){
this.currentIndex = i; //currentIndex值是定義錨點高亮的值
this.diff = hei_2 + newY
return
}
}
},

ps:vue用法小記:watch 的 scrollY(newY){}
1.當咱們在 Vue 裏修改了在 data 裏定義的變量,就會觸發這個變量的 setter,通過vue的封裝處理,會觸發 watch 的回調函數,也就是 scrollY(newY) {} 這裏的函數會執行,同時,newY 就是咱們修改後的值。
2.scrollY 是定義在 data 裏的,列表滾動的時候,scroll 事件的回調函數裏有修改 this.scrollY,因此能 watch 到它的變化。

2.4 滾動固定標題
實現效果:主列表頂端固定一個標題,顯示當前滾動的列表標題,標題改變時有transform上移效果
實現思惟:固定標題fixedTitle拿到當前滾動數組中的title數據便可,在主列表的滾動事件中。設計一個diff值:存入每一個區塊的高度上限(也就是底部)減去 Y軸偏移的值,實時監聽diff,當diff小於title塊的高度時候,開始上移效果
1.fixedTitle獲取

fixedTitle() { //computed中設置
if (this.scrollY > 0) {
return ''
}
return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ''
}

2.diff設置和監聽

this.diff = hei_2 + newY //hei_2爲即將滾動到下一個listGroup的高度
diff(newVal) {
let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0 ; //TITLE_HEIGHT 爲title塊的高度:30
if (this.fixedTop === fixedTop) { //設定this.fixedTop的值存入fixedTop值。若滾動過程當中fixedTop沒有發生變化就不進行transform設置。減小DOM操做,
return
}
this.fixedTop = fixedTop
this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)`
}

三、歌手詳情頁
歌手詳情頁是在歌手頁singer跳轉至二級路由頁 singer-detail
index.js 路由裏配置

{
path: '/singer',
component: Singer,
children: [
{
path: ':id', // 表示 id 爲變量
component: SingerDetail
}
]
}

在singer頁面裏面跳轉路由設定

selectSinger(singer){
this.$router.push({
path: `/singer/${singer.id}`
})
}

3.一、vuex
因爲歌手詳情頁這個組件在app中有屢次調用,這裏設計到‘多個組件共享狀態’的問題。故採用vuex狀態管理,本項目簡要介紹以下,具體移步 vuex:
一般的流程爲:
- 定義 state,考慮項目須要的原始數據(最好爲底層數據)
- getters,就是對原始數據的一層映射,能夠只爲底層數據作一個訪問代理,也能夠根據底層數據映射爲新的計算數據(至關於 vuex 的計算屬性)
- 修改數據:mutations,定義如何修改數據的邏輯(本質是函數),在定義 mutations 以前 要先定義 mutation-types
actions.js 一般是兩種操做
- 異步操做
- 是對mutation的封裝,好比一個動做須要觸發多個mutation的時候,就能夠把多個mutation封裝到一個action中,達到調用一個action去修改多個mutation的目的。

3.1.一、歌手詳情頁關於vuex的設置
a.state.js:建立singer對象

const state = {
singer: {},
}
export default state

b.getter.js:對singer對象設置映射

export const singer = state => state.singer

c.mutation-types.js:設置singer能夠修改的type值

export const SET_SINGER = 'SET_SINGER'

D.mutation.js:寫入修改singer的函數

import * as types from './mutation-types'

const mutations = {
[types.SET_SINGER](state, singer){
state.singer = singer
}
}

export default mutations

e:index.js:實現vuex配置

import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'
import createLogger from 'vuex/dist/logger'
Vue.use(Vuex)
//非生產模式下開啓debug
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
actions,
getters,
state,
mutations,
strict: debug,
plugins: debug ? [createLogger()] : []
})

3.二、利用vuex進行數據傳遞
實現效果:在singer頁面點擊進入singer-detai頁面。傳入歌手信息。顯示歌手詳情
實現思惟:在singer組件跳轉路由的時候,將當前點擊的歌手信息寫入singer的state狀態中

首先 listview.vue 檢測點擊事件,將具體點擊的歌手派發出去,以供父組件 singer 監聽
selectItem(item) {
this.$emit('select', item) //item即爲歌手數據
},

父組件監聽事件執行 selectSinger(singer)
selectSinger(singer) {
this.$router.push({
path: `/singer/${singer.id}`
})
this.setSinger(singer)
},

...mapMutations({ /語法糖,'...'將多個對象注入當前對象
setSinger: 'SET_SINGER' // 將 this.setSinger() 映射爲 this.$store.commit('SET_SINGER')
})

mapMutation爲vuex提供的語法糖,獲取全部的mutation
3. singer-detail 取 vuex 中存好的數據

computed: {
...mapGetters([ // 將 this.singer映射爲 this.$store.getter.singer
'singer'
])
}

mapGetters爲vuex提供的語法糖,獲取全部的getters

3.三、music-list
實現效果:歌手詳情頁的歌單主要實現了歌單向上滾動時,這個歌單也跟着滾動上去,且背景圖逐漸變得灰暗,歌單向下滾動時,背景圖逐漸清晰,放大,
實現思惟:主要是監聽歌單的滾動事件,拿到各個變化的點
一、經過實時獲取的scrollY與背景圖片的比值。從而獲得圖片放大的比例,以及圖片模糊的opacity值

scrollY(newY){
let translateY = Math.max(this.minTranslateY, newY) //設置minTranslateY值,用來限制歌單隻能滾動到離頂部一段距離 this.minTranslateY = -this.imgHeight + RESERVE_HEIGHT(40)
let zIndex = 0 //滾動過程當中須要有層級的切換
let scale = 1
let blur = 1
const percent = Math.abs(newY / this.imgHeight)
if (newY > 0) {
scale = 1 + percent
zIndex = 10
} else {
blur = Math.max(0.2, 1-percent)
}
this.$refs.bgLayer.style['transform'] = `translate3d(0, ${translateY}px,0)`
this.$refs.bgImage.style['opacity'] = `${blur}`
if(newY < translateY){
this.$refs.bgImage.style.paddingTop = 0
this.$refs.bgImage.style.height = `${RESERVE_HEIGHT}px`
zIndex = 10
} else {
this.$refs.bgImage.style.paddingTop = '70%'
this.$refs.bgImage.style.height = 0
}
this.$refs.bgImage.style['transform'] = `scale(${scale})`
this.$refs.bgImage.style.zIndex = zIndex
}

此操做中涉及到css的transform的設置。考慮到瀏覽器對其兼容性不一,封裝一個prefixStyle自動加上瀏覽器對應的前綴

let elementStyle = document.createElement('div').style

let vendor = (() => {
let transformNames = {
webkit: 'webkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
ms: 'msTransform',
standard: 'transform'
}

for (let key in transformNames) {
if (elementStyle[transformNames[key]] !== undefined) return key
}
return false
})()

export function prefixStyle(style) {
if (vendor === false) return false
if (vendor === 'standard') return style
return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}

四、播放器 player組件
播放器是本次實戰的難點及重點,把播放器組件放在 App.vue 下,由於它是一個跟任何路由都不相關的東西。在任何路由下,它均可以去播放。切換路由並不會影響播放器的播放。

4.1 vuex設計
因爲點擊 詳情頁,以及搜索等 均可以進行播放歌曲,且播放組件在哪個路由下都存在,故對其相關狀態進行vuex管理

playing: false, //當前是否正在播放
fullScreen: false, //全屏屬性
playlist: [], //爲實現下一首,上一首功能,保存當前播放列表
sequenceList: [], //當前按正常順序的列表,列表還有一種隨機列表
mode: playMode.sequence, //播放模式。其三种放到配置文件config中
currentIndex: -1 //當前歌曲的index

4.2 展開收起動畫
全屏和底部之間的切換加上一些動畫切換效果,引入插件‘create-keyframe-animation’
實現效果:該動畫是點到點之間位移且慢慢放大的縮放效果
實現思惟:獲取兩個點的位置,利用css3的translate3d屬性進行位移和scale的設置,結合transition提供動畫的四個鉤子函數(@enter,@after-enter,@leave,@after-leave)以及插件create-keyframe-animation來作緩動的動畫效果
1. 獲取mini-player對應位置(x,y)以及scale

_getPosAndScale() {
const targetWidth = 40
const paddingLeft = 40
const paddingBottom = 30
const paddingTop = 80
const width = window.innerWidth * 0.8
const scale = targetWidth / width
const x = -(window.innerWidth / 2 - paddingLeft)
const y = window.innerHeight - paddingTop - width / 2 - paddingBottom
return {
x,y,scale
}
},

4.2 調用插件作動畫
import animations from 'create-keyframe-animation' //引入

const {x, y, scale} = this._getPosAndScale() //在method裏的enter()鉤子中調用
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)

4.3 切換播放模式
實現思惟:播放模式三種:sequence(順序播放)、loop(循環播放)、random(隨機播放),默認是sequence,主要難點在與random播放,須要打亂播放列表。
打亂洗牌算法:遍歷數組,且每次在0-數組長度 內獲取一個隨機數。將隨機數的值與遍歷值互換,

function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}

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
}

當打亂了播放數組後。爲了要保持當前播放的歌曲不變。那麼currentIndex也要相應改變

_resetCurrentIndex(list) { //這裏傳入打亂的list
let index = list.findIndex((item) => {
return item.id == this.currentSong.id
})
this.setCurrentIndex(index)
},

4.4 歌詞
獲取歌詞
獲取歌詞的接口也須要繞過refer,用axios服務器去拿qq的服務器,其設置接口以下:
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query
}).then((response) => { //ps:這裏返回的response依然爲jsonp格式,故須要對數據處理成json數據
let ret = response.data
if (typeof ret === 'string') {
const reg = /^\w+\(({.+})\)$/
const matches = ret.match(reg)
if (matches) {
ret = JSON.parse(matches[1])
}
}
res.json(ret)
})

獲取到的歌詞是base64格式,引入js-base64庫對其進行解碼
2. 歌詞滾動
當前歌曲的歌詞高亮是利用 js-lyric 會派發的 handle 事件

this.currentLyric = new Lyric(lyric, this.handleLyric)

js-lyric 會在每次改變當前歌詞時觸發這個函數,函數t提供的參數爲:當前歌詞的 lineNum 和 txt
爲了當前高亮歌詞保持最中間 是利用了 BScroll 滾動至高亮的歌詞

handleLyric({lineNum, txt}) {
this.currentLineNum = lineNum
if(lineNum > 5){
this.$refs.lyricList.scrollToElement(this.$refs.lyricLine[lineNum - 5],0,1000) //lyricLine表明每一句歌詞 ,lyricList 爲包裹歌詞的content
}else{
this.$refs.lyricList.scrollTo(0, 0, 1000)
}
},

4.5 運用mixins
因爲底部多是會有播放min-player,致使頁面的滾動區域少了60px,這是一個公共的問題。在其餘不少頁面都會出現。故選擇使用mixins處理機制
1.實現思路: handlePlaylist方法在調用頁面處理程序實現滾動區域的bottom值,設置content的bottom。而後進行刷新scroll。在mixin文件中也設置一個handlePlaylist方法。當調用mixin的頁面沒有handlePlaylist方法時候。就會這行mixin裏面得handlePlaylist進行報錯。

mounted() {
this.handlePlaylist(this.playlist)
},
activated() {
this.handlePlaylist(this.playlist)
},
watch: {
playlist(newVal) {
this.handlePlaylist(newVal)
}
},
methods: {
handlePlaylist() {
throw new Error('component must implement handlePlaylist method')
}
}

調用mixins頁面的handlePlaylist方法

handlePlaylist(playList) {
const bottom = playList.length > 0 ? '60px' : '0'
this.$refs.singer.style.bottom = bottom
this.$refs.list.refresh()
},

五、search頁面
5.1 搜索結果上拉加載
上拉加載數據須要根據接口的參數設置來一頁頁拿數據,該接口的參數設計以下

w: query //搜索內容
p: page //拿搜索的第幾頁數據了
perpage:perpage //每一頁返回的數據條數
catZhida:zhida ? 1 : 0 //是否進行搜索歌手

實現關鍵代碼
1. 拓展scroll組件,加上pullup屬性來監聽是否滑到了底部,滑到了底部就派發scrollToEnd事件

this.scroll.on('scrollEnd', () => {
if(this.scroll.y <= (this.scroll.maxScrollY + 50)){ //增長50的buffer
this.$emit('scrollToEnd')
}
})

監聽scrollToEnd事件,執行加載更多searchMore事件,並在每次調用search接口的時候經過傳回來的數據判斷是否還有下一頁數據
//調用seacrh接口
search(query) {
search(query, this.page, this.showSinger, perpage).then((res) => {
if(res.code === ERR_OK){
this.result = this.result.concat(this._getResult(res.data))
this._checkMore(res.data)
}
})
},
//並判斷是否有下一頁數據
_checkMore(data) {
const song = data.song
if (!song.list.length || (song.curnum + song.curpage * perpage) >= song.totalnum) {
this.hasMore = false
}
},
//每次搜索時候。初次調用search
searchFirst(){
this.page = 1
this.result = []
this.hasMore = true
this.search(this.query)
},
//下拉加載更多
searchMore(){
if(!this.hasMore) {
return
}
this.page++
this.search(this.query)
},

5.2 搜索歷史
將每次搜索的關鍵詞存入搜索歷史,且頁面刷新了,搜索歷史還在,搜索歷史列表數量固定在15個之內,且排在第一個的必須是最新搜索的關鍵詞,還有對搜索歷史的單個刪除和批量刪除做用。
實現關鍵代碼
1. 引入good-storage庫,該庫對localstorage進行簡單的封裝。能夠直接存入數組。提供了set,get方法
2. 存入關鍵詞,因爲要屢次調用這個方法,故封裝一個insertArray方法

function insertArray(arr, val, compare, maxLen) {
const index = arr.findIndex(compare)
if(index === 0){ //傳入搜索詞在第一個位置。arr不作任何變化
return
}
if(index > 0){ //傳入搜索詞存在且不是第一個位置,先刪掉舊的位置上的數據,再把搜索詞插入數組頭部
arr.splice(index, 1)
}
arr.unshift(val)
if (maxLen && arr.length > maxLen) { //最大值限定
arr.pop()
}
}
//將插入後的新數組寫入localstorage,並返回新數組供外部調用給vuex狀態管理
export function saveSearch(query){
let searches = storage.get(SEARCH_KEY, [])
insertArray(searches, query, (item) => {
return item === query
},SEARCH_MAX_LEN)
storage.set(SEARCH_KEY, searches)
return searches
}

批量刪除,做爲一個比較嚴重的「大操做」,這裏應須要提示用戶是否確認真的要刪除所有,故加上一個「確認框」。
methods: {
hide() { //往外提供show,hide方法供調用是否顯示該確認框
this.showFlag = false
},
show() {
this.showFlag = true
},
confirm() { //派發出確認(confirm)以及取消(cancel)事件
this.hide()
this.$emit('confirm')
},
cancel() {
this.hide()
this.$emit('cancel')
}
}
}

6、編譯打包上線
將項目經過npm run build 編譯成一個dist文件目錄,能夠搭一個簡單的node服務器跑起來。能夠採用express框架結合axios。
配置prod.server.js

對項目的優化:對組件進行異步加載處理

const UserCenter = (resolve) => {
import('components/user-center/user-center').then((module) => {
resolve(module)
})
}

移動端調試利器:vconsole
項目github:https://github.com/caoyanyuan/vue-player

七 、疑難總結 & 小技巧
6.1.關於 Vue
v-html能夠轉義字符。處理某些帶有html的數據
watch 對象能夠獲得某個屬性每次變化的新值
created()裏面定義的數據只是初始化。而不會像data()那樣給數據添加getter()和setter()方法從而監測數據變化。
mounted 是先觸發子組件的 mounted,再會觸發父組件的 mounted,可是對於 created 鉤子,又會先觸發父組件,再觸發子組件。
若是組件有計數器,在組件銷燬時期要記得清理,
對於 Vue 組件,this.refs.xxx拿到的是Vue實例,因此須要再經過refs.xxx拿到的是Vue實例,因此須要再經過el 拿到真實的 dom

6.2關於 JS
setTimeout(fn, 20)
通常來講 JS 線程執行完畢後一個 Tick 的時間約17ms內 DOM 就能夠渲染完畢因此課程中 setTimeout(fn, 20) 是很是穩妥的寫法
audio 提供的API
<audio ref="audio" src=「」 @play="ready" @error="error"@timeupdate="updateTime" @ended="end"></audio>

@play :當src資源拿到了以後執行的事件
使用場景:songReady做爲一首歌能夠播放的標誌位,能夠解決」若是未拿到歌曲資源,就進行播放形成的DOM報錯「

@play=‘ready’
ready() {
this.songReady = true
}

@timeupdate:拿到當前歌曲的播放時間
使用場景:獲取當前時間來作對應的滾動條進度顯示

@timeupdate="updateTime"
updateTime(e) {
this.currentTime = e.target.currentTime
},

currentTime:屬性,拿到或者設置當前播放到的時間點
使用場景:currentTime爲進度條拖拽到一個點,對應這個點的時間刻。將currentTime設置給audio,便可把歌曲對應播放到這個時間刻

this.$refs.audio.currentTime = currentTime //設置

@ended:歌曲播放結束後發生的事件
使用場景:播放完了、進行播放到下一首,或是若是在循環單曲模式下,繼續循環這一首

end() {
if (this.mode === playMode.loop) {
this.loop()
} else {
this.next()
}
},

6.3 關於 webpack
」 ~ 」 使 SCSS 可使用 webpack 的相對路徑
@import 「~common/scss/mixin」;
@import 「~common/scss/variable」;
babel-runtime 會在編譯階段把 es6 語法編譯的代碼打包到業務代碼中,因此要放在dependencies裏。
Fast Click 是一個簡單、易用的庫,專爲消除移動端瀏覽器從物理觸摸到觸發點擊事件之間的300ms延時
爲何會存在延遲呢?
從觸摸按鈕到觸發點擊事件,移動端瀏覽器會等待接近300ms,緣由是瀏覽器會等待以肯定你是否執行雙擊事件
什麼時候不須要使用FastClick
FastClick 不會伴隨監放任何桌面瀏覽器
Android 系統中,在頭部 meta 中設置 width=device-width 的Chrome32+ 瀏覽器不存在300ms 延時,因此,也不須要
<meta name="viewport" content="width=device-width, initial-scale=1">

一樣的狀況也適用於 Android設備(任何版本),在viewport 中設置 user-scalable=no,但這樣就禁止縮放網頁了
IE11+ 瀏覽器中,你可使用 touch-action: manipulation; 禁止經過雙擊來放大一些元素(好比:連接和按鈕)。IE10可使用 -ms-touch-action: manipulation

————————————————版權聲明:本文爲CSDN博主「cyyeel」的原創文章。原文連接:https://blog.csdn.net/Forever201295/article/details/80266600

相關文章
相關標籤/搜索