1. 程式人生 > >【音樂App】—— Vue-music 專案學習筆記:歌手頁面開發

【音樂App】—— Vue-music 專案學習筆記:歌手頁面開發

前言:以下內容均為學習慕課網高階實戰課程的實踐爬坑筆記。

專案github地址:https://github.com/66Web/ljq_vue_music,歡迎Star。


 

一、歌手頁面佈局與設計
  • 需求:聯絡人列表形式、左右聯動的滾動列表、頂部標題隨列表滾動而改變

 

二、歌手資料介面抓取
  • api目錄下建立singer.js——同recommend.js,依賴jsonp和一些公共引數
    import jsonp from '@/common/js/jsonp'
    import {commonParams, options} from 
    '@/api/config' export function getSingerList() { const url = 'https://c.y.qq.com/v8/fcg-bin/v8.fcg' const data = Object.assign({}, commonParams, { channel: 'singer', page: 'list', key: 'all_all_all', pagesize: 100, pagenum:
    1, hostUin: 0, needNewCode: 0, platform: 'yqq', g_tk: 1664029744, }) return jsonp(url, data, options) }
  • singer.vue —— 資料結構與需求不同:需要兩層陣列結構
  1. 第一層陣列:將所有歌手以姓名開頭字母Findex——ABCD順序排列
  2. 第二層陣列:在每一個字母歌手陣列中,按順序再將歌手進行排列
  3. 熱門資料:簡單將前十條資料取出來
三、歌手資料處理和inger類的封裝
  • 定義_normalizeSinger()方法,規範化singer資料,接收引數list,即資料singers
    const HOT_NAME = '熱門'
    const HOT_SINGER_LEN = 10
    
     _normalizeSinger(list){
            let map = {
                 hot: {
                       title: HOT_NAME,
                       items: []
                 }
            }
            list.forEach((item, index) => {
                    if(index < HOT_SINGER_LEN) {
                        map.hot.items.push({
                            id: item.Fsinger_mid,
                            name: item.Fsinger_name,
                            avatar: `https://y.gtimg.cn/music/photo_new/T001R300x300M000${item.Fsinger_mid}.jpg?max_age=2592000`
                        })
                    }
                    //根據Findex作聚類
                    const key = item.Findex
                      if(!map[key]) {
                         map[key] = {
                            title: key,
                            items: []
                         }
                      }
                    map[key].items.push({
                         id: item.Fsinger_mid,
                         name: item.Fsinger_name,
                         avatar: `https://y.gtimg.cn/music/photo_new/T001R300x300M000${item.Fsinger_mid}.jpg?max_age=2592000`
                    })    
           })
               
          // console.log(map)
                
    }

    問題:avatar需要的資料是通過id計算得到的,且重複多次,重複程式碼太多

  • common->js目錄下建立singer.js: 用面向物件的方法,構造一個Singer類
    export default class Singer {
           constructor({id, name}) {  
                 this.id = id
                 this.name = name
                 this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`
           }
    }
  1. JavaScript constructor 屬性返回對建立此物件的陣列函式的引用

  2. 語法:object.constructor

  • 引入Singer: 
    import Singer from '@/common/js/singer'

    使用new Singer({
                id: item.Fsinger_mid,
                name: item.Fsinger_name
    })代替前面的大段程式碼,減少avatar這樣重複的大段程式碼

  • 為了得到有序列表,需要處理map
    let hot = []   //title是熱門的歌手
    let ret = []   //title是A-Z的歌手
    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陣列進行A-Z排序
    ret.sort((a, b) => {
         return a.title.charCodeAt(0) - b.title.charCodeAt(0)
    })
  • 最後將ret陣列拼接在hot陣列後返回
    return hot.concat(ret)
四、類通訊錄的元件開發——滾動列表實現
  • base->listview目錄下:建立listview.vue
  1. 引用scroll元件,在<scroll>根標籤中傳入data資料,當data發生變化時,強制BScroll重新計算
  2. props引數:
    props:{
         data: {
             type: Array,
             default: []
         }
    } 
  3. DOM佈局:
    <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 :src="item.avatar" class="avatar">
                        <span class="name">{{item.name}}</span>
                    </li>
                 </ul>  
             </li>
         </ul>
    </scroll>   
  • singer.vue中:
  1. 引入並註冊listview元件,傳入引數data,繫結singers資料
    <div class="singer">
         <listview :data="singers"></listview>
    </div>
  2. 修改_getSingerList()中的singers為重置資料結構後的singers
    this.singers = this._normalizeSinger(res.data.list)
  3. 優化:使用圖片懶載入技術處理<img>,:src替換為v-lazy
    <img v-lazy="item.avatar" class="avatar">
五、類通訊錄的元件開發——右側快速入口實現

       獲得title的集合陣列

  • listview.vue中通過computed定義shortcutList()
    computed: {
         shortcutList() {  //得到title的集合陣列,‘熱門’取1個字
              return this.data.map((group) => {
                      return group.title.substr(0, 1) 
               })
         }
    } 
  1. 在<scroll>內層,與歌手列表同級編寫佈局DOM:
    <div class="list-shortcut">
         <ul>
             <li v-for="(item, index) in shortcutList" :key="index" class="item">{{item}}</li>
         </ul>
    </div>
  2. CSS樣式:
    .list-shortcut
        position: absolute //絕對定位到右側 
        right: 0
        top: 50%

       實現點選定位

  • 關鍵:監聽touchstart事件
  1. 為<li class="item">擴充套件一個屬性變數 :data-index="index"
  2. 在dom.js中封裝一個getData函式,得到屬性data-val的值
    export function getData(el, name, val){
           const prefix = 'data-'
           name = prefix + name
           if(val){
              return el.setAttribute(name, val)
           }else{
              return el.getAttribute(name)
           }
    }
  3. scroll.vue中擴充套件兩個方法:
    scrollTo() {
         // 滾動到指定的位置;這裡使用apply 將傳入的引數,傳入到this.scrollTo()
         this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
    },
    scrollToElement() {
         // 滾動到指定的目標元素
         this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
    }
  4. listview.vue中:引入getData方法
    import {getData} from '@/common/js/dom'

    <scroll>根標籤中新增引用: ref="listview";<li class="list-group">中新增引用:ref="listGroup"

  5. 給快速入口列表新增touchstart事件:
    <div class="list-shortcut" @touchstart="onShortcutTouchStart">
    onShortcutTouchStart(e) {
         let anchorIndex = getData(e.target, 'index')//獲取data-index的值 index
         _scrollTo(anchorIndex)
    }
    
    _scrollTo(index){
        this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)//列表滾動定位
    }

       實現滑動聯動

  • 關鍵:監聽touchmove事件
  1. 需求:滑動右側快速入口列表,左側歌手列表隨之滾動
  2. 坑:快速入口列表下方就是歌手列表,同樣可以滾動,需要避免滑動快速入口列表時,也使歌手列表受到影響
  3. 解決:阻止事件冒泡,阻止瀏覽器的延伸滾動 @touchmove.stop.prevent
  • 思路:
    在touchstart事件觸發時,記錄touch處的y值y1和anchorIndex,儲存到this.touch物件中 
    在touchmove事件觸發時,同樣記錄touch處的y值y2,計算(y2-y1)/每個列表項的畫素高 | 0 向下取整,
    得到兩次touch位置列表項的差值delta,使touchmove時的anchorIndex = touch物件的anchorIndex + delta
    呼叫封裝好的_scrollTo方法,傳入anchorIndex,使歌手列表滾動到對應位置
  • 實現:
    const ANCHOR_HEIGHT = 18 //通過樣式設定計算得到
    created() {
           this.touch = {}  //在created中定義touch物件,而不在data或computed中定義,是因為touch物件不用進行監測
    },    
    methods: {
           onShortcutTouchStart(e) {
              let anchorIndex = getData(e.target, 'index')//獲取data-index的值  index 得到的是字串
              let firstTouch = e.touches[0]
              this.touch.y1 = firstTouch.pageY
              this.touch.anchorIndex = anchorIndex
              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  //獲取列表項差值,| 0 向下取整 = Math.floor()
              let anchorIndex = parseInt(this.touch.anchorIndex) + delta
              this._scrollTo(anchorIndex)
           },
           _scrollTo(index){
             //第二個引數表示:要不要滾動動畫緩動時間; 0 瞬間滾動
             this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)//列表滾動定位
           }
     }
  1. 坑:在touchstart時通過getData獲得的anchorIndex是字串,如果直接和delta相加得到的還是字串,這樣滾動的位置就不對
  2. 解決:
    let anchorIndex = parseInt(this.touch.anchorIndex) + delta

       實現聯動效果

  • 需求:滾動歌手列表時,快速入口列表對應的title項高亮顯示
  • 思路:
    監聽scroll事件,拿到pos物件,定義一個變數scrollY,【實時記錄】歌手列表Y軸滾動的位置pos.y,
    監測資料data,每次發生改變時,都重新計算每個group元素的高度height,存在listHeight陣列中
    監測scrollY,保留計算高度後的listHeight陣列,遍歷得到每個group元素的【高度區間】上限height1和下限height2,
    對比scrollY和每個group元素的高度區間height2-height1,確定當前滾動位置【currentIndex】,對映到DOM中
  • scroll.vue中:
  1. 新增一個props引數,決定要不要監聽BScroll的滾動事件scroll
    listenScroll: {
         type: Boolean,
         default: false
    }
  2. _initScroll方法中:
    if(this.listenScroll) {
       let me = this //箭頭函式中代理this
       this.scroll.on('scroll', (pos) => { //監聽scroll事件
          me.$emit('scroll', pos) //派發一個scroll事件,傳遞pos位置物件:有x和y屬性
       })
    }
  • listview.vue中:
  1. created()中新增兩個屬性值:
    this.listenScroll = true
    this.listHeight = []
  2. <scroll>根標籤中傳值 :listenScroll="listenScroll" 監聽scroll事件 @scroll="scroll"
  • 常見習慣私有方法如_scrollTo()一般放在下面,公共方法或繫結事件的方法如scroll()放在上面
  1. data中觀測兩個資料:
    scrollY: -1 //實時滾動的Y軸位置
    currentIndex: 0 //當前顯示的第幾個title項
  2. methods中新增scroll方法,傳入接收的pos物件:
    scroll(pos) {
        this.scrollY = pos.y //實時獲取BScroll滾動的Y軸距離
    }
  3. 新增_calculateHeight私有方法,計算每個group的高度height
    calculateHeight() {
            this.listHight = [] //每次重新計算每個group高度時,恢復初始值
            const list = this.$refs.listGroup
            let height = 0      //初始位置的height為0
            this.listHeight.push(height)
            for(let i=0; i<list.length; i++){
                 let item = list[i] //得到每一個group的元素
                 height += item.clientHeight //DOM元素可以用clientHeight獲取元素高度
                 this.listHeight.push(height)  //得到每一個元素對應的height
            }
    }
  4. watch:{} 監測data的變化,使用setTimeout延時呼叫_calculateHeight,重新計算每個group的高度;監測scrollY的變化,遍歷listHeight陣列得到每個group元素的高度上限height1和下限height2;對比scrollY,確定當前滾動位置對應的title項currentIndex
     watch: {
            data() {
                 setTimeout(() => {  //使用setTimeout延時:因為資料的變化和DOM的變化還是間隔一些時間的
                    this._calculateHeight()
                  }, 20)
            },
            scrollY(newY) {
                 const listHeight = this.listHeight
                 //當滾動到頂部,newY>0
                 if(newY > 0) {
                    this.currentIndex = 0
                    return
                 }
                 //在中間部分滾動,遍歷到最後一個元素,保證一定有下限,listHeight中的height比元素多一個
                 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
                        //  console.log(this.currentIndex)
                        return
                   }
                 }
                 //當滾動到底部,且-newY大於最後一個元素的上限
                 //currentIndex 比listHeight中的height多一個, 比元素多2個
                 this.currentIndex = listHeight.length - 2
            }
     }
  • 坑:scroll元件中設定了probeType的預設值為1:滾動的時候會派發scroll事件,會截流,只能監聽緩慢的滾動,監聽不到swipe快速滾動
  • 解決:
  1. 需要在<scroll>中傳遞:probeType="3" 除了實時派發scroll事件,在swipe的情況下仍然能實時派發scroll事件
  2. 快速入口列表的title項<li class="item"> 動態繫結current class,將currentIndex對映到DOM中:
    :class="{'current': currentIndex === index}"
  3. CSS樣式:
    &.current
       color: $color-theme
  • 坑:點選快速入口列表時,歌手列表會快速滾動,但點選的列表項沒有高亮顯示
  • 原因:高亮沒有依賴點選的點,而是通過scrollY計算得到的,但目前_scrollTo中只是使列表滾動,沒有派發scroll事件,改變scrollY
  • 解決:在_scrollTo中,手動改變scrollY的值,為當前元素的上限height
    this.scrollY = -this.listHeight[index]
  • 坑:touch事件都是加在父元素<div class="list-shortcut">上的,點選頭尾--“熱”“Z”之前和之後的邊緣區塊,會發現也是可以點選的,但它沒有對應顯示的歌手列表,這個點選是沒有意義的
  • 解決:console.log(index)得知邊緣區塊的index都是null,在_scrollTo中設定如果是邊緣區塊,不執行任何操作,直接返回
    if(!index && index !== 0){
       return
    }
  • 坑:console.log(index)時發現滑動時滑到頭部以上時是一個負值,滑到尾部以下時是一個很大的值
  • 原因:touchmove一直在執行,這個事件一直沒有結束,它的Y值就會變大,這樣算出來的delta加上之前的touch.anchorIndex得到的值就可能會超
  • 解決:在_scrollTo中處理index的邊界情況
    if(index < 0){
       index = 0
    }else if(index > this.listHeight.length - 2){
       index = this.listHeight.length - 2
    }
  • 補充:scrollToElement(this.$refs.listGroup[index], 0)中的index沒有出現問題,是因為BScroll中已經做了邊界的處理
六、滾動固定標題實現——fixed title
  • 需求:當滾動到哪個歌手列表,頂部就顯示當前歌手列表的title, 且固定不動,直到滾動到下一個歌手列表,再顯示下一個title
  1. 佈局DOM:當fixedTitle不為" "的時候顯示
    <div class="list-fixed" v-show="fixedTitle">
        <div class="fixed-title">{{fixedTitle}}</div>
    </div>
  2. computed中計算fixedTitle:
    fixedTitle() { 
        if(this.scrollY > 0){ //判斷邊界,‘熱門’往上拉時,不顯示
           return ''
        }
        //初始時,data預設為空,此時this.data[this.currentIndex]為undefinded
        return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ''
    }
  3. CSS樣式:
    .list-fixed
        position: absolute //絕對定位到頂部
        top: 0
        left: 0
        width: 100%
  • 坑:只有在歌手列表的title從底部穿過fixed title後,fixed title的內容才會發生改變,兩個title沒有過渡效果,體驗不好
  • 解決:當歌手列表的title上邊界滾動到fixed title下邊界時,給fixed title新增一個上移效果,使兩個title過渡順滑
  1. 定義一個數據:
    diff: -1 //fixed title的偏移位置
  2. 在scrollY(newY)中實時得到diff: 
    this.diff = height2 + newY
    //得到fixed title上邊界距頂部的偏移距離 = 歌手列表title height下限 + newY(上拉為負值)
  3. 給<div class="list-fixed">新增引用: ref="fixedTitle"
  4. 通過樣式設定得到並定義fixed title的div高度: const TITLE_HEIGHT = 30
  5. 在watch:{}中觀測diff:判斷diff範圍,資料改變DOM
    diff(newVal) {
        let fixedTop = (newVal>0 && newVal<TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
        if(this.fixedTop === fixedTop){
            return 
        }
        this.fixedTop = fixedTop
        this.$refs.fixedTitle.style.transform = `translate3d(0, ${fixedTop}px, 0)`
    }
  • 優化:listview歌手元件也是非同步請求的資料,所以也加一個loading,引入loading元件註冊
  1. 佈局DOM: 
    <div class="loading-container" v-show="!data.length">
         <loading></loading> 
    </div>
  2. CSS樣式:
     .loading-container
        position: absolute
        width: 100%
        top: 50%
        transform: translateY(-50%)

注:專案來自慕課網