Vue 實現的音樂項目 music app 知識點總結分享
其他
此應用的全部數據來自 QQ音樂,利用 axios 結合 node.js 代理後端請求抓取
全局通用的應用級狀態使用 vuex 集中管理
全局引入 fastclick 庫,消除 click 移動瀏覽器 300ms 延遲
頁面是響應式的,適配常見的移動端屏幕,采用 flex 布局
疑難總結 & 小技巧
關於 Vue 知識 & 使用技巧
v-html 可以轉義字符,處理特定接口很有用
watch 對象可以觀測 屬性 的變化
像這種父組件傳達子組件的參數通常都是在data()裏面定義的,為什麽這裏要放到created()定義,兩者有什麽區別呢?
因為這個變量不需要觀測它的變化,因此不用定義在 data 裏,這樣也會對性能有所優化
不明白什麽時候要把變量放在data()裏,什麽時候又不需要放 ?
需要監測這個數據變化的時候,放在 data() 裏,會給數據添加 getter 和 setter
生命周期 鉤子函數
生命周期鉤子函數,比如 mounted 是先觸發子組件的 mounted,再會觸發父組件的 mounted,但是對於 created 鉤子,又會先觸發父組件,再觸發子組件。
銷毀計數器
如果組件有計數器,在組件銷毀時期要記得清理,好習慣
對於 Vue 組件,this.$refs.xxx 拿到的是 Vue 實例,所以需要再通過 $el 拿到真實的 dom
關於 JS 知識 & 技巧
setTimeout(fn, 20)
一般來說 JS 線程執行完畢後一個 Tick 的時間約17ms內 DOM 就可以渲染完畢所以課程中 setTimeout(fn, 20) 是非常穩妥的寫法
關於 webpack 知識 & 技巧
" ~ " 使 SCSS 可以使用 webpack 的相對路徑
@import "~common/scss/mixin";
@import "~common/scss/variable";
babel-runtime 會在編譯階段把 es6 語法編譯的代碼打包到業務代碼中,所以要放在dependencies裏。
Fast Click 是一個簡單、易用的庫,專為消除移動端瀏覽器從物理觸摸到觸發點擊事件之間的300ms延時
為什麽會存在延遲呢?
從觸摸按鈕到觸發點擊事件,移動端瀏覽器會等待接近300ms,原因是瀏覽器會等待以確定你是否執行雙擊事件
何時不需要使用
- 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
請求接口
jsonp:
XHR:
手寫輪播圖
利用 BScroll
BScroll 設置 loop 會自動 clone 兩個輪播插在前後位置
如果輪播循環播放,是前後各加一個輪播圖保證無縫切換,所以需要再加兩個寬度
if (this.loop) { width += 2 * sliderWidth }
初始化 dots 要在 BScroll 克隆插入兩個輪播圖之前
dots active狀態 是通過判斷 currentIndex 與 index 是否相等
currentIndex 更新是通過獲取 scroll 當前 page,BScroll 提供了 api 方便調用
this.currentPageIndex = this.scroll.getCurrentPage().pageX
為了保證改變窗口大小依然正常輪播,監聽窗口 resize 事件,重新渲染輪播圖
window.addEventListener(‘resize‘, () => { if (!this.scroll || !this.scroll.enabled) return clearTimeout(this.resizeTimer) this.resizeTimer = setTimeout(() => { if (this.scroll.isInTransition) { this._onScrollEnd() } else { if (this.autoPlay) { this._play() } } this.refresh() }, 60) })
在切換 tab 相當於 切換了 keep-alive 的組件
輪播會出問題,需要手動幫助執行,利用了 activated , deactivated 鉤子函數
activated() { this.scroll.enable() let pageIndex = this.scroll.getCurrentPage().pageX this.scroll.goToPage(pageIndex, 0, 0) this.currentPageIndex = pageIndex if (this.autoPlay) { this._play() } }, deactivated() { this.scroll.disable() clearTimeout(this.timer) }
實測,首次打開網頁並不會執行 activated,只有在之後切換 tab ,切回來才會執行
在組件銷毀之前 beforeDestroy 銷毀定時器是好習慣,keep-alive 因為是將組件緩存了,所以不會觸發
beforeDestroy() { this.scroll.disable() clearTimeout(this.timer) }
後端接口代理
簡單設置一下 Referer, Host,讓別人直接通過瀏覽器抓到你的接口
但是這種方式防不了後端代理的方式
前端 XHR 會有跨域限制,後端發送 http 請求則沒有限制,因此可以偽造請求
axios 可以在瀏覽器端發送 XMLHttpRequest
請求,在服務器端發送 http
請求
(在項目編寫階段,可以將後端代理請求寫在 webpack 的 dev 文件的 before 函數內)
before(app) { app.get(‘/api/getDiscList‘, function (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 }).then((response) => { res.json(response.data) // axios 返回的數據在 response.data,要把數據透傳到我們自定義的接口裏面 res.json(response.data) }).catch((e) => { console.log(e) }) }); }
定義一個路由,get 到一個 /api/getDiscList
接口,通過 axios 偽造 headers,發送給QQ音樂服務器一個 http 請求,還有 param 參數。
得到服務端正確的響應,通過 res.json(response.data)
返回到瀏覽器端
另外 因為是 http 請求數據,是ajax,所以 format 參數要將原本接口的 jsonp 改為 json
大公司怎麽防止被惡意代理呢?當你的訪問量大的時候,出口ip會被查到獲取封禁,還有一種就是參數驗簽,也就是請求人家的數據必須帶一個簽名參數,然後這個簽名參數是很難拿到的這個正確的簽名,從而達到保護數據的目的
當然,獲取的數據並不能直接拿來用,需要做進一步的規格化,達到我們使用的要求,所以在這方面單獨封裝了一個 class 來處理這方面的數據,具體請看src/common/js/song.js
flex 布局,熱門歌單推薦
左側 icon 固定大小,flex: 0 0 60px
flex 屬性是 flex-grow
, flex-shrink
和 flex-basis
的簡寫,默認值為 0 1 auto。後兩個屬性可選。
flex-grow
屬性定義項目的放大比例,默認為 0,即如果存在剩余空間,也不放大。flex-shrink
屬性定義了項目的縮小比例,默認為 1,即如果空間不足,該項目將縮小。flex-basis
屬性定義了在分配多余空間之前,項目占據的主軸空間(main size)。瀏覽器根據這個屬性,計算主軸是否有多余空間。它的默認值為auto,即項目的本來大小。
右側 text 區塊 自適應占據剩下的空間,並且內部也采用 flex,使用 flex-direction: column; justify-content: center;
來達到縱向居中排列
recommend 頁面 利用 BScroll 滾動
Scroll 初始化但卻沒有滾動,是因為初始化時機不對,必須保證數據到來,DOM 成功渲染之後 再去進行初始化
可以使用父組件 給 Scrol組件傳 :data
數據,Scroll 組件自己 watch
這個 data,有變化就立刻 refesh 滾動
新版本 BScroll 已經自己實現檢測 DOM 變化,自動刷新,大部分場景下無需傳 data 了
所以也就 無需監聽 img 的 onload 事件 然後執行 滾動刷新 了
<img @load="loadImage" class="needsclick" :src="item.picUrl">
loadImage() { if (!this.checkloaded) { this.checkloaded = true this.$refs.scroll.refresh() } }
歌手頁面 數據重構
歌手頁面的結構是 熱門、 A-Z
的順序排列,但抓取的接口數據只是 100條常見的歌手,並且是亂序的,但我們可以利用接口的 Findex 進行數據的重構
首先可以定義一個 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)
根據 title 字母的 Unicode 編碼大小排序的(比如:‘A‘.charCodeAt(0)=65;‘B‘.charCodeAt(0)=66)然後就a,b,c,d...的順序了
歌手頁面
shortcut 定位
因為 shortcut 整體的高度是不確定的,所以采用的是 top:50%
之後,transform: translateY(-50%);
這樣就能動態的根據內容高度而垂直居中
歌手頁面 區塊與錨點 的聯動
點擊或滑動 shortcut 不同的錨點 ,自動滾動至相應的標題列表
利用了 BScroll 的 api ,scrollToElement
- scrollToElement 可以滾動至相應的 index 值的區塊
第一次點擊觸碰 shortcut ,獲取點擊具體錨點的 index 值,記錄觸碰位置的 index ,利用 scrollToElement ,滾動至相應 index 的區塊
而之後,滑動錨點實現滾動是利用 touchmove 事件,將兩次觸碰的的位置計算值變成 delta 差值:變成改變後的錨點區塊 index 值,再將首次觸碰的 index 值 + 改變的 delta 值,再利用 scrollToElement ,滾動至相應的區塊
onShortcutTouchStart(e) { let anchorIndex = getData(e.target, ‘index‘) // 獲取 點擊具體錨點的 index 值 let firstTouch = e.touches[0] // 第一次觸碰的位置 this.touch.y1 = firstTouch.pageY // 保存 第一次觸碰的位置的Y值 this.touch.anchorIndex = anchorIndex // 保存 第一次觸碰時的錨點 index 值 this._scrollTo(anchorIndex) }, onShortcutTouchMove(e) { let firstTouch = e.touches[0] this.touch.y2 = firstTouch.pageY let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0 // 兩次觸碰 Y 軸的偏移錨點值 let anchorIndex = +this.touch.anchorIndex + delta // 獲取 偏移了多少 index 值 ,因為 anchorIndex 是字符串,所以要轉成數字再相加 this._scrollTo(anchorIndex) }, _scrollTo(index) { this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 200) }
<Scroll class="listview" ref="listview"> <!--歌手列表--> <ul> <li v-for="group in data" class="list-group" ref="listGroup"> <h2 class="list-group-title">{{group.title}}</h2> <!--首字母條目--> <ul> <li v-for="item in group.items" class="list-group-item"> <img :src="item.avatar" class="avatar"> <span class="name">{{item.name}}</span> </li> </ul> </li> </ul> <div class="list-shortcut" @touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove"> <ul> <li v-for="(item, index) in shortcutlist" :data-index="index" class="item"> {{item}} </li> </ul> </div> </Scroll>
滑動主列表,側邊 shortcut 自動高亮不同錨點
- 首先 BScroll 組件 監聽滾動事件,並派發事件以供父組件監聽,將 pos 值傳出去
if (this.listenScroll) { let self = this this.scroll.on(‘scroll‘, (pos) => { // 實時監測滾動事件,派發事件:Y軸距離 self.$emit(‘scroll‘, pos) }) }
- 父組件監聽到滾動派發的事件
@scroll="scroll"
將 pos.y 存在 this.scrollY
scroll(pos) { this.scrollY = pos.y // 實時獲取 BScroll 滾動的 Y軸距離 }
- 再用 watch 檢測數據的變化,一旦變化,重新計算每個區塊的高度列表。再判斷當前滾動的 Y軸值 是否落在相應的 group 高度區間,然後更新 currentIndex ,使 shortcut 的錨點高亮
-
watch: { data() { // 延時,確保DOM渲染之後執行,通常是nextTick,這裏用setTimeout是為了兼容更低 setTimeout(() => { this._calculateHeight() }, 20) }, // 這裏的 scrollY 是當前組件上的,和 BScroll 的並不是一個 scrollY(newY) { const listHeight = this.listHeight // 1. 當滾動至頂部以上 if (newY > 0) { this.currentIndex = 0 return } // 2. 當在中間部分滾動,length之所以 -1 是因為 當初高度列表定義必須多一個 for (let i = 0; i < listHeight.length - 1; i++) { let height1 = listHeight[i] let height2 = listHeight[i + 1] if (-newY >= height1 && -newY < height2) { this.currentIndex = i this.diff = height2 + newY // height 上限 - newY 的值 return } } // 3. 當滾動至底部,且 newY 大於最後一個元素的上限 this.currentIndex = listHeight.length - 2 } }
每個區塊的高度列表是 通過
_calculateHeight
函數實現-
_calculateHeight() { this.listHeight = [] const list = this.$refs.listGroup let height = 0 this.listHeight.push(height) for (let i = 0; i < list.length; i++) { let item = list[i] height += item.clientHeight this.listHeight.push(height) } }
- 最後只要在 li 上綁定class就可以實現不同位置的錨點高亮了
-
:class="{‘current‘: currentIndex === index}"
這裏的 Vue 用法提示: watch 的 scrollY(newY){} 當我們在 Vue 裏修改了在 data 裏定義的變量,就會出發這個變量的 setter,經過一系列的處理,會觸發 watch 的回調函數,也就是 scrollY(newY) {} 這裏的函數會執行,同時,newY 就是我們修改後的值。 scrollY 是定義在 data 裏的,列表滾動的時候,scroll 事件的回調函數裏有修改 this.scrollY,所以能 watch 到它的變化。 watch 的回調函數的第一個參數表示變化的新值 滾動固定標題 效果實現 在中間部分滾動時,會不斷設置 diff 值,每個區塊的高度上限(也就是底部)減去 Y軸偏移的值
this.diff = height2 + newY // 就是 height 上限 - newY 的值
watch 檢測 diff 變化,判斷如果 diff>0 且 小於 title 塊的高度,設為差值,否則為0
再將 fixed 的 title 塊 translate 偏移 -
diff(newVal) { let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0 if (this.fixedTop === fixedTop) return // 判斷如果兩個title區塊沒有碰到,是不會觸發 DOM 操作的 this.fixedTop = fixedTop this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)` }
歌手詳情頁
singer page 頁面 引入 singer-detail 二級路由
index.js 路由裏配置
-
{ path: ‘/singer‘, component: Singer, children: [ { path: ‘:id‘, // 表示 id 為變量 component: SingerDetail } ] }
singer.vue 裏設定跳轉路由
this.$router.push({})
html: -
<router-view></router-view>
js:
-
selectSinger(singer){ this.$router.push({ path: `/singer/${singer.id}` }) }
Vuex
Vuex 教程見:Vuex
通常的流程為:
- 定義 state,考慮項目需要的原始數據(最好為底層數據)
- getters,就是對原始數據的一層映射,可以只為底層數據做一個訪問代理,也可以根據底層數據映射為新的計算數據(相當於 vuex 的計算屬性)
- 修改數據:mutations,定義如何修改數據的邏輯(本質是函數)。
在定義 mutations 之前 要先定義 mutation-types (通常為動詞+名詞)
actions.js
通常是兩種操作- 異步操作
- 是對mutation的封裝,比如一個動作需要觸發多個mutation的時候,就可以把多個mutation封裝到一個action中,達到調用一個action去修改多個mutation的目的。
歌手頁面,數據利用 vuex 傳遞
1. 首先 listview.vue 檢測點擊事件,將具體點擊的歌手派發出去,以供父組件 singer 監聽
-
selectItem(item) { this.$emit(‘select‘, item) },
2. 父組件監聽事件執行 selectSinger(singer)
- 指向子路由,向地址欄加上
singer.id
- 向 mutation 提
SET_SINGER
的 commit -
selectSinger(singer) { this.$router.push({ path: `/singer/${singer.id}` }) this.setSinger(singer) }, ...mapMutations({ // 語法糖,‘...‘將多個對象註入當前對象 setSinger: ‘SET_SINGER‘ // 將 this.setSinger() 映射為 this.$store.commit(‘SET_SINGER‘) })
mapMutations (語法糖) 映射 mutations ,
this.setSinger(singer)
相當於執行this.$store.commit(‘SET_SINGER‘)
(singer 為 mutation 的第二個參數)
而 mutations 內SET_SINGER
的邏輯為 -
[types.SET_SINGER](state, singer) { state.singer = singer }
3. singer-detail 取 vuex 中存好的數據
-
computed: { ...mapGetters([ ‘singer‘ ]) }
getters 內
singer
的邏輯為singer = state => state.singer
musiclist 與 songlist
-
滑動 songlist 與背景圖的聯動
主要是 監聽滾動距離,根據不同的距離條件發生不同的效果
-
mounted() { this.imageHeight = this.$refs.bgImage.clientHeight this.$refs.list.$el.style.top = `${this.imageHeight}px` // 對於 Vue 組件,this.$refs.xxx 拿到的是 Vue 實例,所以需要再通過 $el 拿到真實的 dom this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT }, watch: { scrollY(newY) { let translateY = Math.max(this.minTransalteY, newY) // 最遠滾動改變的距離就是 minTransalteY let zIndex = 0 let scale = 1 const percent = Math.abs(newY / this.imageHeight) this.$refs.layer.style.transform = `translate3d(0,${translateY}px,0)` this.$refs.layer.style.webkitTransform = `translate3d(0,${translateY}px,0)` if (newY < this.minTransalteY) { zIndex = 10 this.$refs.bgImage.style.paddingTop = 0 this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px` } else { this.$refs.bgImage.style.paddingTop = ‘70%‘ this.$refs.bgImage.style.height = 0 } if (newY > 0) { scale = 1 + percent zIndex = 10 } this.$refs.bgImage.style.zIndex = zIndex this.$refs.bgImage.style.transform = `scale(${scale})` this.$refs.bgImage.style.webkitTransform = `scale(${scale})` } }
自動判斷瀏覽器加CSS兼容前綴 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) }
- 首先生成基於用戶瀏覽器的div樣式
- 根據 vendor 供應商定義的不同瀏覽器前綴,去測試用戶瀏覽器。
方法就是判斷創建的 div 樣式是否有相應的前綴樣式,如果有,則返回前綴樣式的key,也就是需要的 前綴
- 通過 prefixStyle 函數,參數為我們需要兼容的樣式。如果需要加簽註,返回的格式是 前綴 + 首字母大寫的樣式(應為通常前綴樣式為
-webkit-transform-origin
,JS操作時,不能寫-
,可以采用駝峰寫法,也就是樣式首字母大寫)
播放器 player
把播放器組件放在 App.vue 下,因為它是一個跟任何路由都不相關的東西。在任何路由下,它都可以去播放。切換路由並不會影響播放器的播放。
播放器 vuex 設計
點擊 歌手/歌單 都會進入詳情頁,詳情頁 created() 會根據點擊的歌手請求相應的數據,然後利用 _normalizeSongs 將數據整理,其中很重要的函數是 createSong ,生成自定義 song 類,方便以後讀取
播放器 圖片旋轉
animation-play-state
animation-play-state CSS 屬性定義一個動畫是否運行或者暫停。可以通過查詢它來確定動畫是否正在運行。另外,它的值可以被設置為暫停和恢復的動畫的重放。
恢復一個已暫停的動畫,將從它開始暫停的時候,而不是從動畫序列的起點開始在動畫。修復BUG:ios下safari與chrome瀏覽器,animation-play-state樣式失效 #60
點擊暫停播放的時候,歌曲的圖片會繼續轉動,導致的原因是因為animation-play-state:paused這個樣式失效了
修復具體代碼
核心代碼: -
/** * 計算內層Image的transform,並同步到外層容器 * @param wrapper * @param inner */ syncWrapperTransform(wrapper, inner) { if (!this.$refs[wrapper]) return let imageCdWrapper = this.$refs[wrapper] let image = this.$refs[inner] let wTransform = getComputedStyle(imageCdWrapper)[transform] let iTransform = getComputedStyle(image)[transform] imageCdWrapper.style[transform] = wTransform === ‘none‘ ? iTransform : iTransform.concat(‘ ‘, wTransform) }
解決快速切換歌曲引發的錯誤 這個錯誤是由於切換的太快,歌曲並未獲取到播放地址,而提前播放 利用了H5新api: canplay 當終端可以播放媒體文件時觸發該canplay事件,估計加載足夠的數據來播放媒體直到其結束,而不必停止以進一步緩沖內容。 利用這個api,在audio上監聽 canplay 派發的事件,做成標誌位 後來 api 改至 playing 播放器 進度條 功能 normal 的長形進度條 在 progress 上監聽 touchstart, touchmove, touchend 三個事件 touchstart: 獲取第一次點擊的橫坐標和已播放的進度條長度 touchmove: 獲取移動後的橫坐標,並定義 delta 為 移動後坐標 - 第一次點擊的橫坐標 設置 偏移量 offsetWidth 為 已播放的進度條長度 + delta 在去設置 progress 和 progressBtn 的寬度和transform 量都為 offsetWidth touchend: 一些組件特有的邏輯,和進度條不太相關暫不贅述 而點擊任意位置,移動進度按鈕,則是通過為 progress 進度條添加點擊事件
progressClick(e) { this._offset(e.offsetX - progressBtnWidth / 2) this._triggerPercent() }
mini 的圓形進度條
利用了 SVG 實現,其中有兩個圓,一個是背景圓形,另一個為已播放的圓形進度
-
<div class="progress-circle"> <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> <slot></slot> </div>
修復進度條的 BUG
迷你播放器暫停狀態,進入全屏,按鈕在進度條最左邊
- 原因:當播放器最小化的時候,progress-bar 仍然在監聽 percent 的變化,所以在不斷計算進度條的位置,然而這個時候由於播放器隱藏,進度條的寬度 this.$refs.progressBar.clientWidth 計算為0,因此計算出來的 offset 也是不對的,導致再次最大化播放器的時候,由於播放器是暫停狀態, percent 並不會變化,也不會重新計算這個 offset ,導致 Bug。
- 解決方案:當播放器最大化的時候,手動去計算一次 offset,確保進度條的位置正確。
progress-bar 組件要 watch 下 fullScreen,在進入全屏的時候調用一下 移動按鈕函數
歌詞 lyric
獲取歌詞,雖然我們約定返回數據是 json,但QQ音樂 返回的是依然是 jsonp,所以我們需要做一層數據的處理
const reg = /^\w+\(({.+})\)$/
就是將返回的jsonp格式摘取出我們需要的json字段ret = JSON.parse(matches[1])
將正則分組(就是正則括號內的內容)捕獲的json字符串數據 轉成 json 格式然後我們在 player 組件中監聽 currentSong 的變化,獲取 this.currentSong.getLyric()
-
axios.get(url, { headers: { referer: ‘https://c.y.qq.com/‘, host: ‘c.y.qq.com‘ }, params: req.query }).then((response) => { 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
(我們這次用的是QQ音樂pc版的歌詞,需要解碼base64,而移動版的QQ音樂是不需要的) -
this.lyric = Base64.decode(res.lyric)
之後利用第三方庫:
js-lyric
,解析我們的歌詞,生成方便操作的對象 -
getLyric() { this.currentSong.getLyric() .then(lyric => { this.currentLyric = new Lyric(lyric) }) }
歌詞滾動
當前歌曲的歌詞高亮是利用
js-lyric
會派發的 handle 事件 -
this.currentLyric = new Lyric(lyric, this.handleLyric)
js-lyric
會在每次改變當前歌詞時觸發這個函數,函數的參數為 當前的 lineNum 和 txt而 使當前高亮歌詞保持最中間 是利用了 BScroll 滾動至高亮的歌詞
-
let middleLine = isIphoneX() ? 7 : 5 // 鑒於iphonex太長了,做個小優化 if (lineNum > middleLine) { let lineEl = this.$refs.lyricLine[lineNum - middleLine] this.$refs.lyricList.scrollToElement(lineEl, 1000) } else { this.$refs.lyricList.scrollTo(0, 0, 1000) }
cd 與 歌詞 之間滑動
通過監聽 middle 的 三個 touch 事件
offsetWidth
是為了計算歌詞列表的一個偏移量的,首先它的偏移量不能大於0,也不能小於-window.innerWidth
。
left 是根據當前顯示的是 cd 還是歌詞列表初始化的位置,如果是 cd,那麽 left 為 0 ,歌詞是從右往左拖的,deltaX 是小於 0 的,所以最終它的偏移量就是0+deltaX
;如果已經顯示歌詞了,那麽 left 為-window.innerWidth
,歌詞是從左往右拖,deltaX 是大於 0 的,所以最終它的偏移量就是-window.innerWidth + deltaX
。 -
middleTouchStart(e) { this.touch.initiated = true this.touch.startX = e.touches[0].pageX this.touch.startY = e.touches[0].pageY }, middleTouchMove(e) { if (!this.touch.initiated) return const deltaX = e.touches[0].pageX - this.touch.startX const deltaY = e.touches[0].pageY - this.touch.startY 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) console.log(this.touch.percent) 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 this.$refs.middleL.style[transitionDuration] = 0 }, middleTouchEnd() { let offsetWidth, 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 durationTime = 300 this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)` this.$refs.lyricList.$el.style[transitionDuration] = `${durationTime}ms` this.$refs.middleL.style.opacity = opacity this.$refs.middleL.style[transitionDuration] = `${durationTime}ms` }
優化
Vue 按需加載路由:
當打包構建應用時,Javascript 包會變得非常大,影響頁面加載。如果我們能把不同路由對應的組件分割成不同的代碼塊,然後當路由被訪問的時候才加載對應組件,這樣就更加高效了。
結合 Vue 的異步組件和 Webpack 的代碼分割功能,輕松實現路由組件的懶加載。
- 首先,可以將異步組件定義為返回一個 Promise 的工廠函數 (該函數返回的 Promise 應該 resolve 組件本身):
const Foo = () => Promise.resolve({ /* 組件定義對象 */ })
- 第二,在 Webpack 2 中,我們可以使用動態 import語法來定義代碼分塊點 (split point):
import(‘./Foo.vue‘) // 返回 Promise
在我們的項目中的 router/index.js 是這樣定義的:
-
// Vue 異步加載路由 // 引入5個 一級路由組件 const Recommend = () => import(‘components/recommend/recommend‘) const Singer = () => import(‘components/singer/singer‘) const Rank = () => import(‘components/rank/rank‘) const Search = () => import(‘components/search/search‘) const UserCenter = () => import(‘components/user-center/user-center‘) // 二級路由組件 const SingerDetail = () => import(‘components/singer-detail/singer-detail‘) const Disc = () => import(‘components/disc/disc‘) const TopList = () => import(‘components/top-list/top-list‘)
- 指向子路由,向地址欄加上
-
Vue 實現的音樂項目 music app 知識點總結分享