Vue 實現的音樂專案 music app 知識點總結分享

nanyang24發表於2018-02-15

專案總結

這是我第二個用 Vue 實現的專案,下面內容包括了在實現過程中所記錄的知識點以及一些小技巧

專案演示地址:https://music-vue.n-y.io
原始碼地址:https://github.com/nanyang24/…

其他

此應用的全部資料來自 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,原因是瀏覽器會等待以確定你是否執行雙擊事件

何時不需要使用

  1. FastClick 不會伴隨監聽任何桌面瀏覽器
  2. Android 系統中,在頭部 meta 中設定 width=device-width 的Chrome32+ 瀏覽器不存在300ms 延時,所以,也不需要

<meta name="viewport" content="width=device-width, initial-scale=1">

  1. 同樣的情況也適用於 Android裝置(任何版本),在viewport 中設定 user-scalable=no,但這樣就禁止縮放網頁了
  2. 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-shrinkflex-basis 的簡寫,預設值為 0 1 auto。後兩個屬性可選。

  1. flex-grow 屬性定義專案的放大比例,預設為 0,即如果存在剩餘空間,也不放大。
  2. flex-shrink 屬性定義了專案的縮小比例,預設為 1,即如果空間不足,該專案將縮小。
  3. 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 自動高亮不同錨點

  1. 首先 BScroll 元件 監聽滾動事件,並派發事件以供父元件監聽,將 pos 值傳出去
if (this.listenScroll) {
  let self = this
  this.scroll.on(`scroll`, (pos) => { // 實時監測滾動事件,派發事件:Y軸距離
    self.$emit(`scroll`, pos)
  })
}
  1. 父元件監聽到滾動派發的事件
 @scroll="scroll"

將 pos.y 存在 this.scrollY

scroll(pos) {
  this.scrollY = pos.y    // 實時獲取 BScroll 滾動的 Y軸距離
}
  1. 再用 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)
  }
}
  1. 最後只要在 li 上繫結class就可以實現不同位置的錨點高亮了
:class="{`current`: currentIndex === index}"

這裡的 Vue 用法提示
watchscrollY(newY){}

  1. 當我們在 Vue 裡修改了在 data 裡定義的變數,就會出發這個變數的 setter,經過一系列的處理,會觸發 watch 的回撥函式,也就是 scrollY(newY) {} 這裡的函式會執行,同時,newY 就是我們修改後的值。
  2. scrollY 是定義在 data 裡的,列表滾動的時候,scroll 事件的回撥函式裡有修改 this.scrollY,所以能 watch 到它的變化。
  3. 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

通常的流程為:

  1. 定義 state,考慮專案需要的原始資料(最好為底層資料)
  2. getters,就是對原始資料的一層對映,可以只為底層資料做一個訪問代理,也可以根據底層資料對映為新的計算資料(相當於 vuex 的計算屬性)
  3. 修改資料:mutations,定義如何修改資料的邏輯(本質是函式)。

在定義 mutations 之前 要先定義 mutation-types (通常為動詞+名詞)

actions.js 通常是兩種操作

  1. 非同步操作
  2. 是對mutation的封裝,比如一個動作需要觸發多個mutation的時候,就可以把多個mutation封裝到一個action中,達到呼叫一個action去修改多個mutation的目的。

歌手頁面,資料利用 vuex 傳遞

1. 首先 listview.vue 檢測點選事件,將具體點選的歌手派發出去,以供父元件 singer 監聽

selectItem(item) {
  this.$emit(`select`, item)
},

2. 父元件監聽事件執行 selectSinger(singer)

  1. 指向子路由,向位址列加上 singer.id
  2. 向 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)
}
  1. 首先生成基於使用者瀏覽器的div樣式
  2. 根據 vendor 供應商定義的不同瀏覽器字首,去測試使用者瀏覽器。

方法就是判斷建立的 div 樣式是否有相應的字首樣式,如果有,則返回字首樣式的key,也就是需要的 字首

  1. 通過 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`)

無需改動其他的程式碼

手機聯調

電腦,手機 同一WIFI下

配置 config 的 index.js 裡的 host 為 `0.0.0.0`,手機可以開啟電腦的IP地址+埠檢視

mac下 ifconfig 檢視ip

移動端除錯工具

移動端console:vConsole
移動端抓包工具:charles

結語

以上是在實現這個音樂 Vue 專案中遇到的難點以及一些使用技巧。在這裡記錄下來方便以後自己查閱,還能夠給同樣在前端這個小領域奮鬥的大家提供一小些學習資料~

我的 Github:https://github.com/nanyang24
如果對你有幫助,歡迎 star 和 互粉 ~

相關文章