【音樂App】—— Vue-music 專案學習筆記:歌手頁面開發
阿新 • • 發佈:2018-12-24
前言:以下內容均為學習慕課網高階實戰課程的實踐爬坑筆記。
專案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
- singer.vue —— 資料結構與需求不同:需要兩層陣列結構
- 第一層陣列:將所有歌手以姓名開頭字母Findex——ABCD順序排列
- 第二層陣列:在每一個字母歌手陣列中,按順序再將歌手進行排列
- 熱門資料:簡單將前十條資料取出來
三、歌手資料處理和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` } }
-
JavaScript constructor 屬性返回對建立此物件的陣列函式的引用
-
語法: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
- 引用scroll元件,在<scroll>根標籤中傳入data資料,當data發生變化時,強制BScroll重新計算
- props引數:
props:{ data: { type: Array, default: [] } }
- 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中:
- 引入並註冊listview元件,傳入引數data,繫結singers資料
<div class="singer"> <listview :data="singers"></listview> </div>
- 修改_getSingerList()中的singers為重置資料結構後的singers
this.singers = this._normalizeSinger(res.data.list)
- 優化:使用圖片懶載入技術處理<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) }) } }
- 在<scroll>內層,與歌手列表同級編寫佈局DOM:
<div class="list-shortcut"> <ul> <li v-for="(item, index) in shortcutList" :key="index" class="item">{{item}}</li> </ul> </div>
- CSS樣式:
.list-shortcut position: absolute //絕對定位到右側 right: 0 top: 50%
實現點選定位
- 關鍵:監聽touchstart事件
- 為<li class="item">擴充套件一個屬性變數 :data-index="index"
- 在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) } }
- 在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) }
- listview.vue中:引入getData方法
import {getData} from '@/common/js/dom'
<scroll>根標籤中新增引用: ref="listview";<li class="list-group">中新增引用:ref="listGroup"
- 給快速入口列表新增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事件
- 需求:滑動右側快速入口列表,左側歌手列表隨之滾動
- 坑:快速入口列表下方就是歌手列表,同樣可以滾動,需要避免滑動快速入口列表時,也使歌手列表受到影響
- 解決:阻止事件冒泡,阻止瀏覽器的延伸滾動 @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)//列表滾動定位 } }
- 坑:在touchstart時通過getData獲得的anchorIndex是字串,如果直接和delta相加得到的還是字串,這樣滾動的位置就不對
- 解決:
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中:
- 新增一個props引數,決定要不要監聽BScroll的滾動事件scroll
listenScroll: { type: Boolean, default: false }
- _initScroll方法中:
if(this.listenScroll) { let me = this //箭頭函式中代理this this.scroll.on('scroll', (pos) => { //監聽scroll事件 me.$emit('scroll', pos) //派發一個scroll事件,傳遞pos位置物件:有x和y屬性 }) }
- listview.vue中:
- created()中新增兩個屬性值:
this.listenScroll = true this.listHeight = []
- <scroll>根標籤中傳值 :listenScroll="listenScroll" 監聽scroll事件 @scroll="scroll"
- 常見習慣:私有方法如_scrollTo()一般放在下面,公共方法或繫結事件的方法如scroll()放在上面
- data中觀測兩個資料:
scrollY: -1 //實時滾動的Y軸位置 currentIndex: 0 //當前顯示的第幾個title項
- methods中新增scroll方法,傳入接收的pos物件:
scroll(pos) { this.scrollY = pos.y //實時獲取BScroll滾動的Y軸距離 }
- 新增_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 } }
- 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快速滾動
- 解決:
- 需要在<scroll>中傳遞:probeType="3" 除了實時派發scroll事件,在swipe的情況下仍然能實時派發scroll事件
- 快速入口列表的title項<li class="item"> 動態繫結current class,將currentIndex對映到DOM中:
:class="{'current': currentIndex === index}"
- 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
- 佈局DOM:當fixedTitle不為" "的時候顯示
<div class="list-fixed" v-show="fixedTitle"> <div class="fixed-title">{{fixedTitle}}</div> </div>
- computed中計算fixedTitle:
fixedTitle() { if(this.scrollY > 0){ //判斷邊界,‘熱門’往上拉時,不顯示 return '' } //初始時,data預設為空,此時this.data[this.currentIndex]為undefinded return this.data[this.currentIndex] ? this.data[this.currentIndex].title : '' }
- CSS樣式:
.list-fixed position: absolute //絕對定位到頂部 top: 0 left: 0 width: 100%
- 坑:只有在歌手列表的title從底部穿過fixed title後,fixed title的內容才會發生改變,兩個title沒有過渡效果,體驗不好
- 解決:當歌手列表的title上邊界滾動到fixed title下邊界時,給fixed title新增一個上移效果,使兩個title過渡順滑
- 定義一個數據:
diff: -1 //fixed title的偏移位置
- 在scrollY(newY)中實時得到diff:
this.diff = height2 + newY //得到fixed title上邊界距頂部的偏移距離 = 歌手列表title height下限 + newY(上拉為負值)
- 給<div class="list-fixed">新增引用: ref="fixedTitle"
- 通過樣式設定得到並定義fixed title的div高度: const TITLE_HEIGHT = 30
- 在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元件註冊
- 佈局DOM:
<div class="loading-container" v-show="!data.length"> <loading></loading> </div>
- CSS樣式:
.loading-container position: absolute width: 100% top: 50% transform: translateY(-50%)
注:專案來自慕課網