vue-music(1)音樂播發器 項目開發記錄

Vue-Music

跟學一個網課老師作的仿原生音樂APP跟學的筆記,記錄點滴,也但願對學習vue初學小夥伴有點幫助

項目展現

一| 前期工做

1.項目初始化

  • npm install -g vue-cli
  • vue init webpack vue-music
  • npm install stylus stylus-loader -D
  • 修改eslint.js
  • 修改webpack.base.conf.js resolve配置項簡化路徑

2.裝包

  • npm install fastclick --save 取消默認300ms延遲
import fastClick from 'fastclick'
fastClick.attach(document.body)
  • npm install babel-polyfill

對es6的高級語法進行轉義當運行環境中並無實現的一些方法,babel-polyfill 會給其作兼容
須要在main.js中引入javascript

  • npm install babel-runtime --save 輔助編譯 不須要引入便可用
babel-runtime 是供編譯模塊複用工具函數。是錦上添花
babel-polyfil是雪中送炭,是轉譯沒有的api.

二| 頂部tab導航 && Recommend 頁面組件開發

1. 頂部導航欄 tab

創建基本的頁面骨架,基本的組件引入
header rank recommend search singer tab 這幾個組件組成頁面骨架css

2. recommend組件

  • 數據獲取

qq音樂html

Jsonp

Jsonp發送的不是一個ajax請求,他動態建立一個script標籤,script沒有同源策略限制,因此能跨域 有一個返回參數 callback , 後端解析url,返回一個方法。
  • 安裝: npm install jsonp@0.2.1

jsonp github倉庫vue

  • 之後須要多出引用jsonp跨域請求,將其建立在 scr/common/jsonp.js

jsonp promise化

import originJSONP from 'jsonp'
export default function jsonp(url, data, option) {
  // jsonp的三個參數
  // - url-->一個純淨的url地址
  // - data --> url中的 query 經過 data 拼到url上
  // - option
  url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)
  return new Promise((resolve, reject) => {
    originJSONP(url, option, (err, data) => {
      if (!err) {
        resolve(data)
      } else {
        reject(err)
      }
    })
  })
}
 // 拼接data到url
function param (data) {
  let url = ''
  for (var k in data) {
    let value = data[k] !== undefined ? data[k] : ''
    url += `&${k}=${encodeURIComponent(value)}`
  }
  // encodeURIComponent() 函數可把字符串做爲 URI 組件進行編碼。
  return url ? url.substring(1) : ''
}
注意:當路徑報錯的時候,咱們要想到webpack.base.conf.js配置文件中的 alias 選項 確保路徑是否匹配

Recommend的數據獲取

  1. recommend.vue 中的 created 生命週期鉤子中調用_getRecommend 方法
  2. _getRecommend 方法調用recommend.js中暴露出來的getRecommend方法
  3. getRecommend 方法調用了 Jsonp 方法, Jsonp方法抓取接口,從而得到數據
  • 有的jsonp接口url很長,可是真正的url知識前面的部分
  • 大公司通常用0來表明一切正常

輪播圖組件

  • 輪播圖數據獲取完成後,就下來作的就是搭建輪播頁面 ,接下來編寫一個輪播組件 slider.vuejava

    1. 新建base文件夾,儲存如同slider.vue的基礎組件

在silder.vue中,咱們使用了slot插槽,外部引用slider的時候slider標籤裏面包裹的dom會被插入到slot插槽部分。webpack

  1. 在recommend.vue中 引入 import Slider from 'base/slider/slider',並在components中註冊Slider,以後就可使用Slider標籤了
  2. 將jsonp返回的slider數據存儲到recommend數組中,而後遍歷recommned 數組項循環渲染內容
這個時候咱們打開項目,會發現已有數據,可是樣式還不行,在props中添加loop,autoplay,interval(滾動間隔),
  • 使用了第三方輪播 better-scroll 來進一步實現 slidergit

    新版的BS中snap屬性集合成了一個對象選項 而舊版的是單獨的屬性名,這點要注意
    1. 初始化BS,在何時初始化?

咱們要保證渲染的時機是正確的,一般在mounted生命週期鉤子中初始化,保證BS正常渲染的話咱們一般在mounted裏面加一個延遲es6

mounted () {
    setTimeout(() => { // 瀏覽器17ms刷新一次, 這裏延遲20ms 確保組件已經渲染完成
      this._setSliderWidth() // 設置slider寬度
      this._initDots() // 初始話dots
      this._initSlider() // 初始化slider
    }, 20)
  1. _setSliderWidth方法 -- 輪播圖組件的寬度計算

這裏要注意,這時候執行玩寬度方法以後,可能無效,這是由於在寬度計算的時候,slot插槽裏面的東西還未加載,爲了解決這個問題,咱們能夠在recommend.vue中 給slider 的父元素 加上v-if="recommends.length",確保渲染時機正確github

    1. _initSlider()方法 -- 使用new BScroll 建立輪播實例,設置無限滾動及其餘的相關初始化配置,至此,咱們的輪播頁面已經能夠無縫滾動了
    2. 添加dots導航web

      五個數據,dom有七個,由於loop爲ture的時候,bs會自動在先後各拷貝一份。咱們想要添加dots,必須保證和數據數同樣,因此咱們應該在bs初始化以前完成dots的初始化

      初始化dots爲一個長度爲childern.length的數組
      this.dots = new Array(this.children.length)
      在slider.vue中循環
      v-for="(item,index) of dots"
      添加選中樣式
      :class="{active:currentPageIndex === index}"
      在bs滾動的時候 會派發一個事件 在初始化slider 綁定一個事件

      this.slider.on('scrollEnd', () => {
          let pageIndex = this.slider.getCurrentPage().pageX
          if (this.loop) {
            pageIndex -= 1
            this.currentPageIndex = pageIndex
            if (this.autoplay) {
              clearTimeout(this.timer)
              this._play()
            }
          }
        })

      使用了 bs中的 getCurrentPage 方法來獲取滾動的當前頁面
      在autoplay中使用了bs 的 goToPage 方法來實現輪播

    • 監聽窗口大小改變自動改變 && 優化slider
    以前的slider基本完成,可是此時若是改變窗口大小,頁面就會亂掉

    使用resize窗口監聽事件,配合bs的refresh刷新方法 實現每一次改變窗口大小都能重置寬度

    window.addEventListener('resize', () => {
        if (!this.slider) { // slider尚未初始化的時候
          return
        }
        this._setSliderWidth(true)
        this.slider.refresh()
      })

    在app.vue 中使用keepalive標籤,來避免重複請求

    咱們在跳轉到其餘頁面的時候,要記得清理定時器,優化效率

    destroyed() {
        clearTimeout(this.timer) // 性能優化小習慣
      }

    歌單組件

    歌單組件數據獲取

    在pc版的qq音樂中獲取請求接口

    因爲QQ音樂的歌單數據時,請求接口host和refer規定了必須是qq音樂的地址,咱們本地就會請求失敗。爲了解決這個問題,咱們可使用 手動代理 假裝成qq音樂地址請求接口 欺騙接口

    Vue proxyTable代理 後端代理接口

    在項目開發的時候,接口聯調的時候通常都是同域名下,且不存在跨域的狀況下進行接口聯調,可是當咱們如今使用vue-cli進行項目打包的時候,咱們在本地啓動服務器後,好比本地開發服務下是 http://localhost:8080 這樣的訪問頁面,可是咱們的接口地址是 http://xxxx.com/save/index 這樣的接口地址,咱們這樣直接使用會存在跨域的請求,致使接口請求不成功,所以咱們須要在打包的時候配置一下,咱們進入 config/index.js 代碼下以下配置便可:
    dev: {
        // 靜態資源文件夾
        assetsSubDirectory: 'static',
        // 發佈路徑
        assetsPublicPath: '/',
        // 代理配置表,在這裏能夠配置特定的請求代理到對應的API接口
        // 例如將'localhost:8080/api/xxx'代理到'www.example.com/api/xxx'
        // 使用方法:https://vuejs-templates.github.io/webpack/proxy.html
        proxyTable: {
          '/': {
            target: 'https://c.y.qq.com', // 接口的域名
            secure: false, // 若是是https接口,須要配置這個參數
            changeOrigin: true, // 若是接口跨域,須要進行這個參數配置
            pathRewrite: {
              '^/api': '/'
            },
            headers: {
              referer: 'https://c.y.qq.com'
            }
          }
        }
    注意: '/api' 爲匹配項,target 爲被請求的地址,由於在 ajax 的 url 中加了前綴 '/api',而本來的接口是沒有這個前綴的,因此須要經過 pathRewrite 來重寫地址,將前綴 '/api' 轉爲 '/'。若是自己的接口地址就有 '/api' 這種通用前綴,就能夠把 pathRewrite 刪掉。

    表單組件開發

    咱們經過代理得到ajax數據後,將其賦值給 discList
    this.discList = res.data.list
    以後將disclist渲染到組件中
    v-for="item of discList"

    • 滾動組件 Scroll.vue

    因爲 滾動 是一個很基礎的組件 因此在common裏建立scroll.vue組件,使代碼結構化

    <template>
      <div ref="wrapper">
        <slot></slot>
      </div>
    </template>
    在Recommend.vue中 必定要綁定data數據,由於scroll.vue中 watch 監聽data數據的變化來刷新better-scroll 這裏的能夠綁定recommend.vue中的 discList 數組來座位 data

    這裏的 recommends 和 discList 數據獲取是有前後順序的,通常都是先recommends再discList,若是先獲取到的是discList的話 歌單列表就會出現滾動不到底部的問題

    爲了確保recommend數據後加載的狀況下咱們的表單還能正常滾動發,咱們能夠給slider中的img添加一個loadImage方法@load="loadImage",方法調用一個 refresh方法便可 this.$refs.scroll.refresh()
    爲了不請求的每一張圖片都執行一次,咱們能夠設置一個bool標誌位來控制 ,只要有一張圖片加載完成便可,以下:

    loadImage() {
            if (!this.checkLoaded) {
              this.$refs.scroll.refresh()
              this.checkLoaded = true
            }
          }

    表單組件優化

    • 圖片的懶加載
    節省流量,提高加載速度
    npm 安裝
    npm install vue-lazyload
    在main.js中添加代碼
    import VueLazyLoad from 'vue-lazyload'
    Vue.use(VueLazyLoad, {
      loading: require('common/images/touxiang.png')
    })

    在Recommend.vue中使用
    <img v-lazy="item.imgurl" alt="">

    • 解決圖片點擊失效

    有些狀況下點擊事件之間互相沖突,咱們在使用fastclick的時候,能夠給點擊的dom添加一個fastclick裏的一個css needsclick的類名,來確保點擊事件能夠正常執行

    • loading組件

    爲了增長交互體驗,在表單還未渲染以前,咱們可使用一個loading來佔位。

    在base中新建loading組件

    <template>
        <div class="loading">
          <img src="./loading.gif" alt="">
          <p class="desc">{{title}}</p>
        </div>
    </template>
    
    <script>
    export default {
      props: {
        title: {
          type: String,
          default: '許文瑞正在吃屎。。。。'
        }
      }
    }
    </script>

    在recommend.vue中添加以下代碼:

    <div class="loading-content" v-show="!discList.length">
        <loading></loading>
      </div>

    三| 歌手組件開發

    1.歌手首頁開發

    數據獲取

    • 數據獲取依舊從qq音樂官網獲取

      歌手接口

    • 建立singer.js

      咱們和之前同樣,利用咱們封裝的jsonp等發放,來請求咱們的接口,返回給singer.vue。

    成功獲取數據之後,咱們發現,官網的數據的數據結構和咱們想要的不同,因此咱們下一步進行數據結構的聚合處理

    數據處理

    咱們但願的數據結構是數據按照字母排序的數組再加上一個熱門的數組的集合,顯然咱們在官網的到的數據不是這樣的,咱們構造一個_normalizeSinger方法來完成:

    _normalizeSinger(list) { // 處理數據結構 形參爲list
          let map = { // 把數據都存在map對象中
            hot: { // 熱門城市
              title: HOT_NAME,
              items: [] // 初始化空數組
            }
          }
          list.forEach((item, index) => { // 循環數組中的每一項
            if (index < HOT_SINGER_LENGTH) { // 由於原始數據是按照熱度排列的,因此獲取前十的熱門
              map.hot.items.push(new Singer({ // push到咱們的hot數組中
              // new Singer: 爲了模塊化和減小代碼的複用,咱們在common > js 建立了一個singer.js
              // 來建立一個類構造器 裏面包括歌手頭像的拼接
                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 Singer({ // 追加到map.items中
              id: item.Fsinger_mid,
              name: item.Fsinger_name
            }))
          })
    
          // 爲了獲得有序列表 咱們須要處理map
          let hot = [] // 熱門城市
          let ret = [] // 字母表城市
          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) // 熱門城市
            }
          }
          ret.sort((a, b) => {
            return a.title.charCodeAt(0) - b.title.charCodeAt(0) // 把字母城市按charcode字母排序
          })
    
          return hot.concat(ret) // 將字母城市追加到hot城市 返回給外部
        }

    細節點注意

    關於歌手圖片的獲取,經過官網觀察,咱們發現圖片是有一個網址拼接 item.Fsinger_mid 來完成的,因此咱們在common >js >singer.js中 使用了 ${}來拼接,獲取歌手圖片地址,拼接url語法是使用的是 `` 而不是' '

    listview.vue開發

    數據咱們獲取到了,咱們接下來開發listview.vue組件,由於這個列表組件咱們後面有不少頁面也要用到,因此咱們在base下建立基礎組件 listview.vue

    在listview.vue中引入 咱們以前封裝好的scroll組件
    import Scroll from 'base/scroll/scroll'

    經過獲取的數據,進行兩次遍歷渲染,就能獲得咱們想要的dom頁面了

    html代碼以下

    <template>
      <scroll class="listview" :data="data">
        <ul>
          <li v-for="(group, index) in data" :key="index" class="list-group">
            <h2 class="list-group-title">{{group.title}}</h2>
            <ul>
              <li v-for="(item, index) in group.items" :key="index" class="list-group-item">
                <img v-lazy="item.avatar" class="avatar">
                <span class="name">{{item.name}}</span>
              </li>
            </ul>
          </li>
        </ul>
        <div class="list-shortcut">
          <ul>
            <li class="item" v-for="(item, index) in shortcutList" :key="index">
              {{item}}
            </li>
          </ul>
        </div>
      </scroll>
    </template>

    至此 歌手頁面就能正常滾動了

    shortcutList字母導航器

    接下來,開始咱們的字母導航器的樣式製做

    咱們能夠在listview.vue中建立一個計算屬性shortcutList

    computed: {
          shortcutList() {
            return this.data.map((group) => {
              return group.title.substr(0, 1)
            })
          }
        },

    以後在頁面中v-for渲染shortcutList便可 配合css樣式 實現邊欄的字母導航dom的製做

    <div
          class="list-shortcut"
          @touchstart="onShortcutTouchStart"
          @touchmove.stop.prevent="onShortcutTouchMove"
        >
          <ul>
            <li
              class="item"
              v-for="(item, index) in shortcutList"
              :key="index"
              :data-index="index"
              :class="{'current': currentIndex === index}"
            >
              {{item}}
            </li>
          </ul>
        </div>

    靜態的字母導航在頁面中已經展示出來了

    接下來 來給導航器添加滑動點擊等事件,使其動態化

    • 滑動右邊字母導航 listview實時滾動

      在字母html標籤中加入touch事件**

      @touchstart="onShortcutTouchStart"
        @touchmove.stop.prevent="onShortcutTouchMove"

      在循環中遍歷index值,在後面的touch中獲取索引,因爲蕾相似此類獲取數據的方法是不少地方都能用到的,咱們在dom.js中添加getData方法

      export function getData(el, name, val) {
        const perfix = 'data-'
        name = perfix + name
        if (val) {
          return el.setAttribute(name, val)
        } else {
          return el.getAttribute(name)
        }
      }

      接下來 爲scroll組件添加 跳轉方法

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

      完整的touch方法代碼以下:

    onShortcutTouchStart(e) {
          let anchorIndex = getData(e.target, 'index') // 獲取data
          let firstTouch = e.touches[0] // 剛開始觸碰的位置座標
          this.touch.y1 = firstTouch.pageY
          this.touch.anchorIndex = anchorIndex
          this._scrollTo(anchorIndex) // 經過使用_scrollTo方法來跳轉到咱們的字母所在位置
        },
        onShortcutTouchMove(e) { // 屏幕滑動方法 要明確開始滾動和結束滾動的兩個位置,而後計算出滾動到哪個字母
          let firstTouch = e.touches[0] // 中止滾動時的位置座標
          this.touch.y2 = firstTouch.pageY // 保存到touch對象中
          let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0 // 計算滾動了多少個字母
          let anchorIndex = parseInt(this.touch.anchorIndex) + delta // this.touch.anchorIndex 字符串轉化爲整型
          this._scrollTo(anchorIndex) // 跳轉到字母位置
        }
    注意:經過getData方法的到的anchorIndex是一個字符串,記得要用parseInt轉化爲數字

    至此 滑動字母導航器 左邊的list已經能夠實現滾動了

    • 滾動左邊list 右邊字母導航高亮

      解決這個問題 ,就要知道左邊listview滾動到的相對位置
      1. 在data中增長scrollY 和 currentIndex來實時監聽listview滾動的位置 和 應該滾動到的具體索引
      2. 在scroll標籤組件綁定@scroll='scroll' 來將滾動的實時位置賦值給this.scrollY

        scroll(pos) {
              this.scrollY = pos.y
              console.log(pos) // 測試
            }
      3. 在listview中添加監視屬性data

        watch: {
            data() {
              setTimeout(() => { // 數據變化到dom變化有一個延遲,因此這個加一個定時器
                this._calculateHeight() // 計算每個group的高度
              }, 20)
            }
        每次data變化,都會從新計算group的高度
      4. _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) // 獲得一個包含每個group高度的數組
              }
            }

        這樣 就能獲得一個包含全部grroup高度的一個數據

      5. 在watch裏監聽scrollY

        拿到了每組的位置,咱們能夠監聽scrollY 聯合二者判斷字母導航器應該滾動到的位置
        scrollY(newY) {
              const listHeight = this.listHeight
              // 當滾動到頂部 newY > 0
              if (newY > 0) {
                this.currentIndex = 0
                return
              }
        
              // 在中間部分滾動
              for (let i = 0; i < listHeight.length; i++) {
                let height1 = listHeight[i]
                let height2 = listHeight[i + 1]
                if (-newY >= height1 && -newY < height2) {
                  this.currentIndex = i
                  this.diff = height2 + newY // 注意 newY爲負值
                  return
                }
              }
              // 當滾動到底部,且-newY 大於最後一個元素的上線
              this.currentIndex = listHeight.length - 2
            }
      6. currentIndex 綁定類 實現字母高亮

        :class="{'current': currentIndex === index}"

    • 細節優化

      1. 完善_scrollTo方法

        _scrollTo(index) {
              if (!index && index !== 0) { // 點擊之外的部分 無反應
                return
              }
              if (index < 0) { // 滑動到頂部時 index爲負
                index = 0
              } else if (index > this.listHeight.length - 2) { // 滑動到尾部
                index = this.listHeight.length - 2
              }
              this.scrollY = -this.listHeight[index] // 每次點擊都更改scrollY以實現同步
              this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 300)
            }
      2. fixedTitle

        計算屬性

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

        頁面html

        <div class="list-fixed" v-show="fixedTitle" ref="fixed">
              <h1 class="fixed-title">{{this.fixedTitle}}</h1>
            </div>
        至此 頂部的fixedtitle標題就作好了 可是咱們發現兩個title在重合的時候 並非很完美,下面咱們就來添加一個頂上去的動畫來優化

        在scrollY函數中 咱們能夠輕鬆獲取一個 diff 值

        this.diff = height2 + newY // 注意 newY爲負值

        經過監聽diff 咱們能夠來實現咱們的要求

        diff(newVal) {
              let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
              if (this.fixedTop === fixedTop) {
                return
              }
              this.fixedTop = fixedTop
              this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)`
            }

    2.歌手詳情頁

    歌手詳情使用二級子路由來開發

    字路由 / 二級路由設置

    路由是由組件承載的

    在router -- index.js中 寫入代碼

    添加字路由

    {
          path: '/singer',
          name: 'Singer',
          component: Singer,
          children: [
            {
              path: ':id',
              component: SingerDetail
            }
          ]
        }

    如代碼所示,在Singer component組件路由選項中,添加children 實現二級路由,而後須要在頁面上加上router-view

    標籤來掛在這個二級路由顯示頁面

    編寫跳轉邏輯

    在次頁面中,二級路由的跳轉是在listview.vue中經過點擊事件向外派發事件來實現的

    selectItem(item) {
          this.$emit('select', item) // 向外派發事件
        }
    由於listview.vue是一個基礎組件,不會編寫業務邏輯,因此把點擊事件派發出去,讓外部實現業務邏輯的編寫

    在singer.vue 中,咱們監聽到這個派發出來的select

    <list-view :data="singers" @select="selectSinger"></list-view>

    而後在selectSinger方法裏面使用vue-router的 編程式跳轉接口

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

    添加轉場動畫

    將singer-detail.vue 組件用transition標籤包裹

    並在css中添加動畫

    .slide-enter-active, .slide-leave-active
        transition: all 0.3s
     .slide-enter, .slide-leave-to
        transform: translate3d( 0, 100%, 0)

    就下來,開始正式開發singer-detail組件,在這以前,咱們先了解一下Vuex 跳轉到vuex筆記

    獲取singer-detail數據

    export function getSingerDetail(singerId) {
      const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg'
      const data = Object.assign({}, commonParams, {
        hostUin: 0,
        needNewCode: 0,
        platform: 'h5page',
        order: 'listen',
        begin: 0,
        num: 50,
        songstatus: 1,
        g_tk: 649509476,
        singermid: singerId // 注意是mid而不是id 不要出錯
      })
    
      return jsonp(url, data, options)
    }
    當在singer-detail頁面上刷新的時候,會獲取不到數據,由於咱們的數據是經過跳轉獲得的,若是咱們在singer-detail數據上刷新,將返回上一級signer this.$router.push('/singer')

    整理獲取的數據結構

    common>js>song.js

    export default class Song {
      constructor({id, mid, singer, name, album, duration, image, url}) {
        this.id = id
        this.mid = mid
        this.singer = singer
        this.name = name
        this.album = album
        this.duration = duration
        this.image = image
        this.url = url
      }
    }
    
    export function createSong(musicData) {
      return new Song({
        id: musicData.songid,
        mid: musicData.songmid,
        singer: filterSonger(musicData.singer),
        name: musicData.songname,
        album: musicData.albumname,
        duration: musicData.interval,
        image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${musicData.albummid}.jpg?max_age=2592000`,
        url: `http://ws.stream.qqmusic.qq.com/C100${musicData.songmid}.m4a?fromtag=0&guid=126548448&crazycache=1`
      })
    }
    
    function filterSonger(singer) {
      let ret = []
      if (!singer) {
        return ''
      }
      singer.forEach((s) => {
        ret.push(s.name)
      })
      return ret.join('/')
    }

    經過方法調用類構造器,咱們就能經過createSong(musicData)來整理得到咱們須要的結構數據

    singer-detail

    methods: {
        _getDetail() {
          if (!this.singer.id) {
            this.$router.push('/singer')
          }
          getSingerDetail(this.singer.id).then((res) => {
            if (res.code === ERR_OK) {
              console.log(res.data.list)
              this.songs = this._normalizeSongs(res.data.list)
            }
          })
        },
        _normalizeSongs(list) {
          let ret = []
          list.forEach((item) => {
            let {musicData} = item
            if (musicData.songid && musicData.albummid) {
              ret.push(createSong(musicData)) 
            }
          })
          return ret
        }
      }

    這樣 經過調用_normalizeSongs方法 --> createSong 來獲得songs數據

    開發MusicList.vue組件

    在props中接受變量 bgImgae songs title

    在singer-detail

    經過計算屬性拿到title 和 bgImage ,

    <music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list>

    這樣就完成了父組件的singer-detail向子組件的music-list的傳值

    由於歌曲列表是滾動的 咱們在music-list中複用了scroll組件

    咱們還須要編寫一個song-lsit組件,爲接下來所用 跳轉到song-list組件開發

    在music-list編寫代碼:

    <scroll
          class="list"
          ref="list"
          :data="songs"
          :probe-type="probeType"
          :listen-scroll="listenScroll"
          @scroll="scroll"
        >
          <div class="song-list-wrapper">
            <song-list :songs="songs"></song-list>
          </div>
          <div class="loading-container" v-show="!songs.length">
            <loading></loading>
          </div>
        </scroll>
    至此,打開頁面,咱們能夠看到歌單列表已經能夠正常滾動
    1. 解決圖片撐開問題
    這是咱們發現咱們的頁面上所有被歌單列表所佔用, 要計算圖片的位置把歌手背景圖展示出來

    在mounted生命週期鉤子裏添加

    this.$refs.list.$el.style.top = `${this.$refs.bgImage.clientHeight}px`

    這樣就能實現歌手海報圖的展現了

    2. 實現海報圖跟着滾動的效果

    咱們在music-list.vue中加入一個layer層,用於跟着跟單一塊兒滾動,來覆蓋咱們的bg-image,這樣就能視覺上達到咱們想要的效果了

    <div class="bg-layer" ref="layer"></div>

    監聽滾動距離

    爲scroll組件傳入probeType值和listenScroll值

    created() {
          this.probeType = 3
          this.listenScroll = true
        }

    爲scroll添加scroll方法來監聽滾動距離

    scroll(pos) {
            this.scrollY = pos.y
          }

    並監聽scrollY數據

    watch: {
          scrollY(newY) {
            let translateY = Math.max(this.minTranslateY, newY)
            let zIndex = 0
            let scale = 1
            let blur = 0
            this.$refs.layer.style[transform] = `translate3d(0, ${translateY}px, 0)`
            const percent = Math.abs(newY / this.imageHeight)
            if (newY > 0) {
              scale = 1 + percent
              zIndex = 10
            } else {
              blur = Math.min(20 * percent, 20)
            }
            this.$refs.filter.style[backdrop] = `blur(${blur}px)`
            if (newY < this.minTranslateY) {
              zIndex = 10
              this.$refs.bgImage.style.paddingTop = 0
              this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`
              this.$refs.pbtn.style.display = 'none'
            } else {
              this.$refs.bgImage.style.paddingTop = '70%'
              this.$refs.bgImage.style.height = 0
              this.$refs.pbtn.style.display = ''
            }
            this.$refs.bgImage.style.zIndex = zIndex
            this.$refs.bgImage.style[transform] = `scale(${scale})`
          }
        }
    3. 處理滾動到頂部的時候歌手title被歌單覆蓋的問題
    處理方法見上面代碼zIndex相關操做
    4. 下滑的時候bg-image圖片放大
    處理見上代碼 bgImage scale相關的操做
    5. 加入loading組件
    在scroll結尾複用loading 便可

    <span id="jumpvuex">開發song-list組件</span>

    <template>
      <div class="song-list">
        <ul v-for="(song, index) in songs" :key="index" class="item">
          <div class="content">
            <h2 class="name">{{song.name}}</h2>
            <p class="desc">{{getDesc(song)}}</p>
          </div>
        </ul>
      </div>
    </template>
    
    <script type="text/ecmascript-6">
    export default {
      props: {
        songs: {
          type: Array,
          default: () => []
        }
      },
      methods: {
        getDesc(song) {
          return `${song.singer} - ${song.album}`
        }
      }
    }
    </script>

    在music-list中傳入song值

    <song-list :songs="songs"></song-list>

    <span id="jumpvuex">a. Vuex</span>

    什麼是vuex

    Vuex 是一個專爲 Vue.js 應用程序開發的 狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。

    簡單的說,當咱們的vue項目比較複雜的時候,有的時候兩個兄弟組件,或者相關度聯繫很低的組件相互之間須要同時獲取或監聽同一個數據或狀態,這個時候咱們就要使用vuex

    vuex 就像是一個大的機房,裏面存着共享數據。這個房間咱們可讓任何一個組件進來獲取數據或者更新數據

    如何使用vuex

    安裝vuex

    npm install vuex --save

    在項目的根目錄下,咱們通常會新建一個store文件夾,裏面添加新建文件:

    • 入口文件 index.js
    • 存放狀態 state.js
    • 存放Mutations mutations.js
    • 存放mutations相關數據的 mutation-types.js
    • 數據修改 執行Mutations actions.js
    • 數據映射 getters.js

      getters 和 vue 中的 computed 相似 , 都是用來計算 state 而後生成新的數據 ( 狀態 ) 的。

    以此項目爲例子,須要各個組件之間共享一個singer數據

    state.js

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

    mutation-types.js

    export const SET_SINGER = 'SET_SINGER'
    使用常量替代 mutation 事件類型在各類 Flux 實現中是很常見的模式。這樣可使 linter 之類的工具發揮做用,同時把這些常量放在單獨的文件中可讓你的代碼合做者對整個 app 包含的 mutation 一目瞭然

    mutations.js

    import * as types from './mutation-types'
    // import * as obj from "xxx" 會將 "xxx" 中全部 export 導出的內容組合成一個對象返回。
    const mutations = {
      [types.SET_SINGER](state, singer) {
        state.singer = singer
      }
    }
    export default mutations
    mutations.js 能夠理解爲是一個修改數據的方法的集合

    getter.js

    有時候咱們須要從 store 中的 state 中派生出一些狀態,若是有多個組件須要用到此屬性,咱們要麼複製這個函數,或者抽取到一個共享函數而後在多處導入它——不管哪一種方式都不是很理想。

    Vuex 容許咱們在 store 中定義「getter」(能夠認爲是 store 的計算屬性)。就像計算屬性同樣,getter 的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變纔會被從新計算。

    export const singer = state => state.singer

    index.js

    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) // 註冊插件
    
    const debug = process.env.NODE_ENV !== 'production' // 線下調試的時候 debug 爲 ture
    
    export default new Vuex.Store({ // new一個實例
      actions,
      getters,
      state,
      mutations,
      strict: debug, // 開啓嚴格模式,用於下面來控制是否開啓插件
      plugins: debug ? [createLogger()] : [] // 開啓插件
    })

    main.js

    在vue的main.js 中 註冊 vuex

    import store from './store'
    ....
    
    new Vue({
      el: '#app',
      render: h => h(App),
      router,
      store
    })

    以上,vuex的初始化就完成了

    singer.vue 寫入 state

    在組件中提交 Mutation

    你能夠在組件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 輔助函數將組件中的 methods 映射爲 store.commit 調用(須要在根節點注入 store)。

    import {mapMutations} from 'vuex'

    在methods結尾添加

    ...mapMutations({
        setSinger: 'SET_SINGER' // 將 `this.setSinger()` 映射爲 `this.$store.commit('SET_SINGER')`
    })

    經過this.setSinger(singer) 實現了對Mutations的提交

    singer-detail.vue 取出state數據

    引入

    import {mapGetters} from 'vuex'

    在computed中

    computed: {
        ......
        
        ...mapGetters([
          'singer'  // 把 `this.signer` 映射爲 `this.$store.getters.singer`
        ])
      }

    至此,singer-detail 和 singer 之間就實現 singer 的共享了

    相關文章
    相關標籤/搜索