Vue 實現的音樂項目 music app 知識點總結分享

其餘

此應用的所有數據來自 QQ音樂,利用 axios 結合 node.js 代理後端請求抓取css

全局通用的應用級狀態使用 vuex 集中管理html

全局引入 fastclick 庫,消除 click 移動瀏覽器 300ms 延遲前端

頁面是響應式的,適配常見的移動端屏幕,採用 flex 佈局vue

疑難總結 & 小技巧

關於 Vue 知識 & 使用技巧

v-html 能夠轉義字符,處理特定接口頗有用

watch 對象能夠觀測 屬性 的變化

像這種父組件傳達子組件的參數一般都是在data()裏面定義的,爲何這裏要放到created()定義,二者有什麼區別呢?

由於這個變量不須要觀測它的變化,所以不用定義在 data 裏,這樣也會對性能有所優化node

不明白何時要把變量放在data()裏,何時又不須要放 ?

須要監測這個數據變化的時候,放在 data() 裏,會給數據添加 getter 和 setterwebpack

生命週期 鉤子函數

生命週期鉤子函數,好比 mounted 是先觸發子組件的 mounted,再會觸發父組件的 mounted,可是對於 created 鉤子,又會先觸發父組件,再觸發子組件。ios

銷燬計數器

若是組件有計數器,在組件銷燬時期要記得清理,好習慣git

對於 Vue 組件,this.$refs.xxx 拿到的是 Vue 實例,因此須要再經過 $el 拿到真實的 dom

關於 JS 知識 & 技巧

setTimeout(fn, 20)

通常來講 JS 線程執行完畢後一個 Tick 的時間約17ms內 DOM 就能夠渲染完畢因此課程中 setTimeout(fn, 20) 是很是穩妥的寫法es6

關於 webpack 知識 & 技巧

" ~ " 使 SCSS 可使用 webpack 的相對路徑

@import "~common/scss/mixin";
@import "~common/scss/variable";

babel-runtime 會在編譯階段把 es6 語法編譯的代碼打包到業務代碼中,因此要放在dependencies裏。

Fast Click 是一個簡單、易用的庫,專爲消除移動端瀏覽器從物理觸摸到觸發點擊事件之間的300ms延時

爲何會存在延遲呢?

從觸摸按鈕到觸發點擊事件,移動端瀏覽器會等待接近300ms,緣由是瀏覽器會等待以肯定你是否執行雙擊事件github

什麼時候不須要使用

  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-shrink 和 flex-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 的錨點高亮
  2. 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 函數實現

    1. _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就能夠實現不一樣位置的錨點高亮了
      2. :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 偏移

      3. 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 路由裏配置

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

        singer.vue 裏設定跳轉路由 this.$router.push({})
        html:

      5. <router-view></router-view>

        js:

      6. 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 監聽

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

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

        1. 指向子路由,向地址欄加上 singer.id
        2. 向 mutation 提 SET_SINGER 的 commit
        3. 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 的邏輯爲

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

          3. singer-detail 取 vuex 中存好的數據

        5. computed: {
            ...mapGetters([
              'singer'
            ])
          }

          getters 內 singer 的邏輯爲

          singer = state => state.singer

          musiclist 與 songlist

           
        6. 滑動 songlist 與背景圖的聯動

          主要是 監聽滾動距離,根據不一樣的距離條件發生不一樣的效果

        7. 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

        8. 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這個樣式失效了
          修復具體代碼
          核心代碼:

        9. /**
           * 計算內層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 實現,其中有兩個圓,一個是背景圓形,另外一個爲已播放的圓形進度

        10. <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()

        11. 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音樂是不須要的)

        12. this.lyric = Base64.decode(res.lyric)

          以後利用第三方庫: js-lyric ,解析咱們的歌詞,生成方便操做的對象

        13. getLyric() {
            this.currentSong.getLyric()
              .then(lyric => {
                this.currentLyric = new Lyric(lyric)
              })
          }

          歌詞滾動

          當前歌曲的歌詞高亮是利用 js-lyric 會派發的 handle 事件

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

          js-lyric 會在每次改變當前歌詞時觸發這個函數,函數的參數爲 當前的 lineNum 和 txt

          而 使當前高亮歌詞保持最中間 是利用了 BScroll 滾動至高亮的歌詞

        15. 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

        16. 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 是這樣定義的:

        17. // 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')
相關文章
相關標籤/搜索