【音樂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%)

注:項目來自慕課網


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM