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


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

 

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

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

1.1、參數設置
loop:是否循環播放
autoPlay:是否自動播放
interval:自動播放的間隔時間
2.1、實現
(1).需要通過獲取slider的寬度來設置每一個輪播圖和輪播圖的包裹層的寬度
(2).初始化better-scroll實例
若設置 loop為true 會自動 clone 兩個輪播插在前后位置,如果輪播循環播放,是前后各加一個輪播圖保證無縫切換,所以需要再加兩個寬度

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

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

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

(6)在組件銷毀之前 beforeDestroy 銷毀定時器

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的寬度。

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;

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 ,這個圓完全顯示)

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

四、api拿后端數據
組件中的數據全都是拿了qq音樂網頁版的數據,拿數據的方式有兩種,一種可以直接通過jsonp跨域來獲取的,另一種接口通過referer偽造請求,
1.jsonp方式
在common中,封裝一個公用jsonp方法

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就會可能被查到獲取封禁,還有一種方式就是參數驗簽,也就是請求人家的數據必須帶一個簽名參數,然后這個簽名參數是很難拿到的這個正確的簽名,從而達到保護數據的目的;

五、components業務邏輯代碼
1、推薦頁面
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、歌手頁面
2.1、數據重構
歌手頁面的結構是 熱門、 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.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.3、滑動主列表控制錨點
實現效果:滑動主列表,側邊 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)
}
},

3、通過實時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)`
}

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

3.1.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.2、利用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.3、music-list
實現效果:歌手詳情頁的歌單主要實現了歌單向上滾動時,這個歌單也跟着滾動上去,且背景圖逐漸變得灰暗,歌單向下滾動時,背景圖逐漸清晰,放大,
實現思維:主要是監聽歌單的滾動事件,拿到各個變化的點
1、通過實時獲取的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)
}

4、播放器 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()
},

5、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')
}
}
}

六、編譯打包上線
將項目通過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


免責聲明!

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



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