Vue-Music
一| 前期工作
1.項(xiàng)目初始化
- npm install -g vue-cli
- vue init webpack vue-music
- npm install stylus stylus-loader -D
- 修改eslint.js
- 修改webpack.base.conf.js resolve配置項(xiàng)簡化路徑
2.裝包
- npm install fastclick --save 取消默認(rèn)300ms延遲
import fastClick from 'fastclick'
fastClick.attach(document.body)
- npm install babel-polyfill
對es6的高級語法進(jìn)行轉(zhuǎn)義當(dāng)運(yùn)行環(huán)境中并沒有實(shí)現(xiàn)的一些方法颂鸿,babel-polyfill 會給其做兼容
需要在main.js中引入 - npm install babel-runtime --save 輔助編譯 不需要引入即可用
babel-runtime 是供編譯模塊復(fù)用工具函數(shù)寒跳。是錦上添花
babel-polyfil是雪中送炭绣的,是轉(zhuǎn)譯沒有的api.
二| 頂部tab導(dǎo)航 && Recommend 頁面組件開發(fā)
1. 頂部導(dǎo)航欄 tab
建立基本的頁面骨架恋昼,基本的組件引入
header rank recommend search singer tab 這幾個組件組成頁面骨架
2. recommend組件
-
數(shù)據(jù)獲取
qq音樂
Jsonp
Jsonp發(fā)送的不是一個ajax請求,他動態(tài)創(chuàng)建一個script標(biāo)簽,script沒有同源策略限制,所以能跨域 有一個返回參數(shù) callback , 后端解析url樟遣,返回一個方法。
- 安裝: npm install jsonp@0.2.1
jsonp github倉庫 - 以后需要多出引用jsonp跨域請求身笤,將其創(chuàng)建在
scr/common/jsonp.js
中
jsonp promise化
import originJSONP from 'jsonp'
export default function jsonp(url, data, option) {
// jsonp的三個參數(shù)
// - 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() 函數(shù)可把字符串作為 URI 組件進(jìn)行編碼豹悬。
return url ? url.substring(1) : ''
}
注意:當(dāng)路徑報(bào)錯的時候,我們要想到webpack.base.conf.js配置文件中的 alias 選項(xiàng) 確保路徑是否匹配
Recommend的數(shù)據(jù)獲取
- 在
recommend.vue
中的created
生命周期鉤子中調(diào)用_getRecommend
方法 -
_getRecommend
方法調(diào)用recommend.js
中暴露出來的getRecommend
方法 - 而
getRecommend
方法調(diào)用了Jsonp
方法液荸, Jsonp方法抓取接口瞻佛,從而獲得數(shù)據(jù)
- 有的jsonp接口url很長,但是真正的url知識前面的部分
- 大公司一般用0來代表一切正常
輪播圖組件
-
輪播圖數(shù)據(jù)獲取完成后娇钱,就下來做的就是搭建輪播頁面 伤柄,接下來編寫一個輪播組件
slider.vue
- 新建
base
文件夾,儲存如同slider.vue的基礎(chǔ)組件
在silder.vue中文搂,我們使用了slot插槽适刀,外部引用slider的時候slider標(biāo)簽里面包裹的dom會被插入到slot插槽部分。 - 在recommend.vue中 引入
import Slider from 'base/slider/slider'
,并在components中注冊Slider煤蹭,之后就可以使用Slider標(biāo)簽了 - 將jsonp返回的slider數(shù)據(jù)存儲到recommend數(shù)組中笔喉,然后遍歷recommned 數(shù)組項(xiàng)循環(huán)渲染內(nèi)容
- 新建
這個時候我們打開項(xiàng)目,會發(fā)現(xiàn)已有數(shù)據(jù)疯兼,但是樣式還不行然遏,在props中添加loop贫途,autoplay吧彪,interval(滾動間隔),
-
使用了第三方輪播 better-scroll 來進(jìn)一步實(shí)現(xiàn) slider
新版的BS中snap屬性集合成了一個對象選項(xiàng) 而舊版的是單獨(dú)的屬性名丢早,這點(diǎn)要注意
- 初始化BS姨裸,在什么時候初始化秧倾?
我們要保證渲染的時機(jī)是正確的,通常在mounted
生命周期鉤子中初始化傀缩,保證BS正常渲染的話我們通常在mounted里面加一個延遲
mounted () { setTimeout(() => { // 瀏覽器17ms刷新一次那先, 這里延遲20ms 確保組件已經(jīng)渲染完成 this._setSliderWidth() // 設(shè)置slider寬度 this._initDots() // 初始話dots this._initSlider() // 初始化slider }, 20)
_setSliderWidth
方法 -- 輪播圖組件的寬度計(jì)算
這里要注意,這時候執(zhí)行玩寬度方法之后赡艰,可能無效售淡,這是因?yàn)樵趯挾扔?jì)算的時候,slot插槽里面的東西還未加載慷垮,為了解決這個問題揖闸,我們可以在recommend.vue中 給slider 的父元素 加上v-if="recommends.length"
,確保渲染時機(jī)正確_initSlider()
方法 -- 使用new BScroll
創(chuàng)建輪播實(shí)例,設(shè)置無限滾動及其他的相關(guān)初始化配置料身,至此汤纸,我們的輪播頁面已經(jīng)可以無縫滾動了-
添加dots導(dǎo)航
五個數(shù)據(jù),dom有七個芹血,因?yàn)閘oop為ture的時候贮泞,bs會自動在前后各拷貝一份。我們想要添加dots幔烛,必須保證和數(shù)據(jù)數(shù)一樣啃擦,所以我們應(yīng)該在bs初始化之前完成dots的初始化
初始化dots為一個長度為childern.length的數(shù)組
this.dots = new Array(this.children.length)
在slider.vue中循環(huán)
v-for="(item,index) of dots"
添加選中樣式
:class="{active:currentPageIndex === index}"
在bs滾動的時候 會派發(fā)一個事件 在初始化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
方法來獲取滾動的當(dāng)前頁面
在autoplay中使用了bs 的goToPage
方法來實(shí)現(xiàn)輪播
- 初始化BS姨裸,在什么時候初始化秧倾?
監(jiān)聽窗口大小改變自動改變 && 優(yōu)化slider
之前的slider基本完成,但是此時如果改變窗口大小说贝,頁面就會亂掉
使用resize窗口監(jiān)聽事件议惰,配合bs的refresh刷新方法 實(shí)現(xiàn)每一次改變窗口大小都能重置寬度
window.addEventListener('resize', () => {
if (!this.slider) { // slider還沒有初始化的時候
return
}
this._setSliderWidth(true)
this.slider.refresh()
})
在app.vue 中使用keepalive標(biāo)簽,來避免重復(fù)請求
我們在跳轉(zhuǎn)到其他頁面的時候乡恕,要記得清理定時器言询,優(yōu)化效率
destroyed() {
clearTimeout(this.timer) // 性能優(yōu)化小習(xí)慣
}
歌單組件
歌單組件數(shù)據(jù)獲取
在pc版的qq音樂中獲取請求接口
由于QQ音樂的歌單數(shù)據(jù)時,請求接口host和refer規(guī)定了必須是qq音樂的地址傲宜,我們本地就會請求失敗运杭。為了解決這個問題,我們可以使用 手動代理 偽裝成qq音樂地址請求接口 欺騙接口
Vue proxyTable代理 后端代理接口
在項(xiàng)目開發(fā)的時候函卒,接口聯(lián)調(diào)的時候一般都是同域名下辆憔,且不存在跨域的情況下進(jìn)行接口聯(lián)調(diào),但是當(dāng)我們現(xiàn)在使用vue-cli進(jìn)行項(xiàng)目打包的時候报嵌,我們在本地啟動服務(wù)器后虱咧,比如本地開發(fā)服務(wù)下是 http://localhost:8080 這樣的訪問頁面,但是我們的接口地址是 http://xxxx.com/save/index 這樣的接口地址锚国,我們這樣直接使用會存在跨域的請求腕巡,導(dǎo)致接口請求不成功,因此我們需要在打包的時候配置一下血筑,我們進(jìn)入 config/index.js 代碼下如下配置即可:
dev: {
// 靜態(tài)資源文件夾
assetsSubDirectory: 'static',
// 發(fā)布路徑
assetsPublicPath: '/',
// 代理配置表绘沉,在這里可以配置特定的請求代理到對應(yīng)的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接口煎楣,需要配置這個參數(shù)
changeOrigin: true, // 如果接口跨域,需要進(jìn)行這個參數(shù)配置
pathRewrite: {
'^/api': '/'
},
headers: {
referer: 'https://c.y.qq.com'
}
}
}
注意: '/api' 為匹配項(xiàng)车伞,target 為被請求的地址择懂,因?yàn)樵?ajax 的 url 中加了前綴 '/api',而原本的接口是沒有這個前綴的另玖,所以需要通過 pathRewrite 來重寫地址困曙,將前綴 '/api' 轉(zhuǎn)為 '/'。如果本身的接口地址就有 '/api' 這種通用前綴谦去,就可以把 pathRewrite 刪掉赂弓。
表單組件開發(fā)
我們通過代理獲得ajax數(shù)據(jù)后,將其賦值給 discList
this.discList = res.data.list
之后將disclist渲染到組件中
v-for="item of discList"
- 滾動組件 Scroll.vue
由于 滾動 是一個很基礎(chǔ)的組件 所以在common里創(chuàng)建scroll.vue組件哪轿,使代碼結(jié)構(gòu)化
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
在Recommend.vue中 一定要綁定data數(shù)據(jù)盈魁,因?yàn)閟croll.vue中 watch 監(jiān)聽data數(shù)據(jù)的變化來刷新better-scroll 這里的可以綁定recommend.vue中的 discList 數(shù)組來座位 data
這里的 recommends 和 discList 數(shù)據(jù)獲取是有先后順序的,一般都是先recommends再discList窃诉,如果先獲取到的是discList的話 歌單列表就會出現(xiàn)滾動不到底部的問題
為了確保recommend數(shù)據(jù)后加載的情況下我們的表單還能正常滾動發(fā)杨耙,我們可以給slider中的img添加一個loadImage方法@load="loadImage"
,方法調(diào)用一個 refresh方法即可 this.$refs.scroll.refresh()
為了避免請求的每一張圖片都執(zhí)行一次飘痛,我們可以設(shè)置一個bool標(biāo)志位來控制 珊膜,只要有一張圖片加載完成即可,如下:
loadImage() {
if (!this.checkLoaded) {
this.$refs.scroll.refresh()
this.checkLoaded = true
}
}
表單組件優(yōu)化
- 圖片的懶加載
節(jié)省流量宣脉,提升加載速度
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="">
解決圖片點(diǎn)擊失效
有些情況下點(diǎn)擊事件之間互相沖突车柠,我們在使用fastclick的時候,可以給點(diǎn)擊的dom添加一個fastclick里的一個cssneedsclick
的類名塑猖,來確保點(diǎn)擊事件可以正常執(zhí)行loading組件
為了增加交互體驗(yàn)竹祷,在表單還未渲染之前,我們可以使用一個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>
三| 歌手組件開發(fā)
1.歌手首頁開發(fā)
數(shù)據(jù)獲取
-
數(shù)據(jù)獲取依舊從qq音樂官網(wǎng)獲取
-
創(chuàng)建singer.js
我們和以前一樣,利用我們封裝的jsonp等發(fā)放凉倚,來請求我們的接口兼都,返回給singer.vue。
成功獲取數(shù)據(jù)以后稽寒,我們發(fā)現(xiàn)扮碧,官網(wǎng)的數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)和我們想要的不一樣,所以我們下一步進(jìn)行數(shù)據(jù)結(jié)構(gòu)的聚合處理
數(shù)據(jù)處理
我們希望的數(shù)據(jù)結(jié)構(gòu)是數(shù)據(jù)按照字母排序的數(shù)組再加上一個熱門的數(shù)組的集合瓦胎,顯然我們在官網(wǎng)的到的數(shù)據(jù)不是這樣的芬萍,我們構(gòu)造一個_normalizeSinger方法來完成:
_normalizeSinger(list) { // 處理數(shù)據(jù)結(jié)構(gòu) 形參為list
let map = { // 把數(shù)據(jù)都存在map對象中
hot: { // 熱門城市
title: HOT_NAME,
items: [] // 初始化空數(shù)組
}
}
list.forEach((item, index) => { // 循環(huán)數(shù)組中的每一項(xiàng)
if (index < HOT_SINGER_LENGTH) { // 因?yàn)樵紨?shù)據(jù)是按照熱度排列的,所以獲取前十的熱門
map.hot.items.push(new Singer({ // push到我們的hot數(shù)組中
// new Singer: 為了模塊化和減少代碼的復(fù)用搔啊,我們在common > js 創(chuàng)建了一個singer.js
// 來創(chuàng)建一個類構(gòu)造器 里面包括歌手頭像的拼接
id: item.Fsinger_mid,
name: item.Fsinger_name
}))
}
const key = item.Findex // 歌手姓氏字首字母
if (!map[key]) { // 如果不存在
map[key] = { // 創(chuàng)建
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) { // 循環(huán)
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城市 返回給外部
}
細(xì)節(jié)點(diǎn)注意
關(guān)于歌手圖片的獲取柬祠,通過官網(wǎng)觀察,我們發(fā)現(xiàn)圖片是有一個網(wǎng)址拼接
item.Fsinger_mid
來完成的负芋,所以我們在common >js >singer.js中 使用了${}
來拼接漫蛔,獲取歌手圖片地址,拼接url語法是使用的是 `` 而不是' '
listview.vue開發(fā)
數(shù)據(jù)我們獲取到了旧蛾,我們接下來開發(fā)listview.vue組件莽龟,因?yàn)檫@個列表組件我們后面有很多頁面也要用到,所以我們在base下創(chuàng)建基礎(chǔ)組件 listview.vue
在listview.vue中引入 我們之前封裝好的scroll組件
import Scroll from 'base/scroll/scroll'
通過獲取的數(shù)據(jù)锨天,進(jìn)行兩次遍歷渲染毯盈,就能得到我們想要的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字母導(dǎo)航器
接下來,開始我們的字母導(dǎo)航器的樣式制作
我們可以在listview.vue中創(chuàng)建一個計(jì)算屬性shortcutList
computed: {
shortcutList() {
return this.data.map((group) => {
return group.title.substr(0, 1)
})
}
},
之后在頁面中v-for渲染shortcutList即可 配合css樣式 實(shí)現(xiàn)邊欄的字母導(dǎo)航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>
靜態(tài)的字母導(dǎo)航在頁面中已經(jīng)展現(xiàn)出來了
接下來 來給導(dǎo)航器添加滑動點(diǎn)擊等事件病袄,使其動態(tài)化
-
滑動右邊字母導(dǎo)航 listview實(shí)時滾動
在字母html標(biāo)簽中加入touch事件**
@touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove"
在循環(huán)中遍歷index值搂赋,在后面的touch中獲取索引,由于蕾類似此類獲取數(shù)據(jù)的方法是很多地方都能用到的益缠,我們在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組件添加 跳轉(zhuǎn)方法
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] // 剛開始觸碰的位置坐標(biāo)
this.touch.y1 = firstTouch.pageY
this.touch.anchorIndex = anchorIndex
this._scrollTo(anchorIndex) // 通過使用_scrollTo方法來跳轉(zhuǎn)到我們的字母所在位置
},
onShortcutTouchMove(e) { // 屏幕滑動方法 要明確開始滾動和結(jié)束滾動的兩個位置脑奠,然后計(jì)算出滾動到哪一個字母
let firstTouch = e.touches[0] // 停止?jié)L動時的位置坐標(biāo)
this.touch.y2 = firstTouch.pageY // 保存到touch對象中
let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0 // 計(jì)算滾動了多少個字母
let anchorIndex = parseInt(this.touch.anchorIndex) + delta // this.touch.anchorIndex 字符串轉(zhuǎn)化為整型
this._scrollTo(anchorIndex) // 跳轉(zhuǎn)到字母位置
}
注意:通過getData方法的到的anchorIndex是一個字符串,記得要用parseInt轉(zhuǎn)化為數(shù)字
至此 滑動字母導(dǎo)航器 左邊的list已經(jīng)可以實(shí)現(xiàn)滾動了
-
滾動左邊list 右邊字母導(dǎo)航高亮
解決這個問題 幅慌,就要知道左邊listview滾動到的相對位置
在data中增加scrollY 和 currentIndex來實(shí)時監(jiān)聽listview滾動的位置 和 應(yīng)該滾動到的具體索引
-
在scroll標(biāo)簽組件綁定@scroll='scroll' 來將滾動的實(shí)時位置賦值給this.scrollY
scroll(pos) { this.scrollY = pos.y console.log(pos) // 測試 }
-
在listview中添加監(jiān)視屬性data
watch: { data() { setTimeout(() => { // 數(shù)據(jù)變化到dom變化有一個延遲宋欺,所以這個加一個定時器 this._calculateHeight() // 計(jì)算每一個group的高度 }, 20) }
每次data變化,都會重新計(jì)算group的高度
-
_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高度的數(shù)組 } }
這樣 就能得到一個包含所有g(shù)rroup高度的一個數(shù)據(jù)
-
在watch里監(jiān)聽scrollY
拿到了每組的位置胰伍,我們可以監(jiān)聽scrollY 聯(lián)合兩者判斷字母導(dǎo)航器應(yīng)該滾動到的位置
scrollY(newY) { const listHeight = this.listHeight // 當(dāng)滾動到頂部 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為負(fù)值 return } } // 當(dāng)滾動到底部齿诞,且-newY 大于最后一個元素的上線 this.currentIndex = listHeight.length - 2 }
-
currentIndex 綁定類 實(shí)現(xiàn)字母高亮
:class="{'current': currentIndex === index}"
?
-
細(xì)節(jié)優(yōu)化
-
完善_scrollTo方法
_scrollTo(index) { if (!index && index !== 0) { // 點(diǎn)擊以外的部分 無反應(yīng) return } if (index < 0) { // 滑動到頂部時 index為負(fù) index = 0 } else if (index > this.listHeight.length - 2) { // 滑動到尾部 index = this.listHeight.length - 2 } this.scrollY = -this.listHeight[index] // 每次點(diǎn)擊都更改scrollY以實(shí)現(xiàn)同步 this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 300) }
-
fixedTitle
計(jì)算屬性
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標(biāo)題就做好了 但是我們發(fā)現(xiàn)兩個title在重合的時候 并不是很完美,下面我們就來添加一個頂上去的動畫來優(yōu)化
在scrollY函數(shù)中 我們可以輕松獲取一個 diff 值
this.diff = height2 + newY // 注意 newY為負(fù)值
通過監(jiān)聽diff 我們可以來實(shí)現(xiàn)我們的要求
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.歌手詳情頁
歌手詳情使用二級子路由來開發(fā)
字路由 / 二級路由設(shè)置
路由是由組件承載的
在router -- index.js中 寫入代碼
添加字路由
{
path: '/singer',
name: 'Singer',
component: Singer,
children: [
{
path: ':id',
component: SingerDetail
}
]
}
如代碼所示骂租,在Singer component組件路由選項(xiàng)中掌挚,添加children
實(shí)現(xiàn)二級路由,然后需要在頁面上加上router-view
標(biāo)簽來掛在這個二級路由顯示頁面
編寫跳轉(zhuǎn)邏輯
在次頁面中菩咨,二級路由的跳轉(zhuǎn)是在listview.vue中通過點(diǎn)擊事件向外派發(fā)事件來實(shí)現(xiàn)的
selectItem(item) {
this.$emit('select', item) // 向外派發(fā)事件
}
因?yàn)閘istview.vue是一個基礎(chǔ)組件吠式,不會編寫業(yè)務(wù)邏輯,所以把點(diǎn)擊事件派發(fā)出去抽米,讓外部實(shí)現(xiàn)業(yè)務(wù)邏輯的編寫
在singer.vue 中特占,我們監(jiān)聽到這個派發(fā)出來的select
<list-view :data="singers" @select="selectSinger"></list-view>
然后在selectSinger方法里面使用vue-router的 編程式跳轉(zhuǎn)接口
selectSinger(singer) {
this.$router.push({
path: `/singer/${singer.id}` // 跳轉(zhuǎn)頁面
})
}
添加轉(zhuǎn)場動畫
將singer-detail.vue 組件用transition標(biāo)簽包裹
并在css中添加動畫
.slide-enter-active, .slide-leave-active
transition: all 0.3s
.slide-enter, .slide-leave-to
transform: translate3d( 0, 100%, 0)
就下來,開始正式開發(fā)singer-detail組件,在這之前云茸,我們先了解一下Vuex 跳轉(zhuǎn)到vuex筆記
獲取singer-detail數(shù)據(jù)
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)
}
當(dāng)在singer-detail頁面上刷新的時候是目,會獲取不到數(shù)據(jù),因?yàn)槲覀兊臄?shù)據(jù)是通過跳轉(zhuǎn)得到的标捺,如果我們在singer-detail數(shù)據(jù)上刷新懊纳,將返回上一級signer
this.$router.push('/singer')
整理獲取的數(shù)據(jù)結(jié)構(gòu)
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('/')
}
通過方法調(diào)用類構(gòu)造器揉抵,我們就能通過createSong(musicData)
來整理獲得我們需要的結(jié)構(gòu)數(shù)據(jù)
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
}
}
這樣 通過調(diào)用_normalizeSongs方法 --> createSong 來得到songs數(shù)據(jù)
開發(fā)MusicList.vue組件
在props中接受變量 bgImgae songs title
在singer-detail
通過計(jì)算屬性拿到title 和 bgImage ,
<music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list>
這樣就完成了父組件的singer-detail向子組件的music-list的傳值
因?yàn)楦枨斜硎菨L動的 我們在music-list中復(fù)用了scroll組件
我們還需要編寫一個song-lsit組件嗤疯,為接下來所用 跳轉(zhuǎn)到song-list組件開發(fā)
在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>
至此冤今,打開頁面,我們可以看到歌單列表已經(jīng)可以正常滾動
1. 解決圖片撐開問題
這是我們發(fā)現(xiàn)我們的頁面上全部被歌單列表所占用茂缚, 要計(jì)算圖片的位置把歌手背景圖展現(xiàn)出來
在mounted生命周期鉤子里添加
this.$refs.list.$el.style.top = `${this.$refs.bgImage.clientHeight}px`
這樣就能實(shí)現(xiàn)歌手海報(bào)圖的展示了
2. 實(shí)現(xiàn)海報(bào)圖跟著滾動的效果
我們在music-list.vue中加入一個layer層戏罢,用于跟著跟單一起滾動,來覆蓋我們的bg-image脚囊,這樣就能視覺上達(dá)到我們想要的效果了
<div class="bg-layer" ref="layer"></div>
監(jiān)聽滾動距離
為scroll組件傳入probeType值和listenScroll值
created() {
this.probeType = 3
this.listenScroll = true
}
為scroll添加scroll方法來監(jiān)聽滾動距離
scroll(pos) {
this.scrollY = pos.y
}
并監(jiān)聽scrollY數(shù)據(jù)
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相關(guān)操作
4. 下滑的時候bg-image圖片放大
處理見上代碼 bgImage scale相關(guān)的操作
5. 加入loading組件
在scroll結(jié)尾復(fù)用loading 即可
<span id="jumpvuex">開發(fā)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 應(yīng)用程序開發(fā)的狀態(tài)管理模式龟糕。它采用集中式存儲管理應(yīng)用的所有組件的狀態(tài),并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化悔耘。Vuex 也集成到 Vue 的官方調(diào)試工具 devtools extension讲岁,提供了諸如零配置的 time-travel 調(diào)試、狀態(tài)快照導(dǎo)入導(dǎo)出等高級調(diào)試功能衬以。
簡單的說催首,當(dāng)我們的vue項(xiàng)目比較復(fù)雜的時候,有的時候兩個兄弟組件泄鹏,或者相關(guān)度聯(lián)系很低的組件相互之間需要同時獲取或監(jiān)聽同一個數(shù)據(jù)或狀態(tài)郎任,這個時候我們就要使用vuex
vuex 就像是一個大的機(jī)房,里面存著共享數(shù)據(jù)备籽。這個房間我們可以讓任何一個組件進(jìn)來獲取數(shù)據(jù)或者更新數(shù)據(jù)
如何使用vuex
安裝vuex
npm install vuex --save
在項(xiàng)目的根目錄下舶治,我們一般會新建一個store文件夾,里面添加新建文件:
入口文件 index.js
存放狀態(tài) state.js
存放Mutations mutations.js
存放mutations相關(guān)數(shù)據(jù)的 mutation-types.js
數(shù)據(jù)修改 執(zhí)行Mutations actions.js
-
數(shù)據(jù)映射 getters.js
getters
和 vue 中的computed
類似 , 都是用來計(jì)算 state 然后生成新的數(shù)據(jù) ( 狀態(tài) ) 的车猬。
以此項(xiàng)目為例子霉猛,需要各個組件之間共享一個singer數(shù)據(jù)
state.js
const state = {
singer: {}
}
export default state
mutation-types.js
export const SET_SINGER = 'SET_SINGER'
使用常量替代 mutation 事件類型在各種 Flux 實(shí)現(xiàn)中是很常見的模式。這樣可以使 linter 之類的工具發(fā)揮作用珠闰,同時把這些常量放在單獨(dú)的文件中可以讓你的代碼合作者對整個 app 包含的 mutation 一目了然
mutations.js
import * as types from './mutation-types'
// import * as obj from "xxx" 會將 "xxx" 中所有 export 導(dǎo)出的內(nèi)容組合成一個對象返回惜浅。
const mutations = {
[types.SET_SINGER](state, singer) {
state.singer = singer
}
}
export default mutations
mutations.js 可以理解為是一個修改數(shù)據(jù)的方法的集合
getter.js
有時候我們需要從 store 中的 state 中派生出一些狀態(tài),如果有多個組件需要用到此屬性伏嗜,我們要么復(fù)制這個函數(shù)坛悉,或者抽取到一個共享函數(shù)然后在多處導(dǎo)入它——無論哪種方式都不是很理想。
Vuex 允許我們在 store 中定義“getter”(可以認(rèn)為是 store 的計(jì)算屬性)承绸。就像計(jì)算屬性一樣裸影,getter 的返回值會根據(jù)它的依賴被緩存起來,且只有當(dāng)它的依賴值發(fā)生了改變才會被重新計(jì)算军熏。
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' // 線下調(diào)試的時候 debug 為 ture
export default new Vuex.Store({ // new一個實(shí)例
actions,
getters,
state,
mutations,
strict: debug, // 開啟嚴(yán)格模式轩猩,用于下面來控制是否開啟插件
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
輔助函數(shù)將組件中的 methods 映射為 store.commit
調(diào)用(需要在根節(jié)點(diǎn)注入 store
)均践。
import {mapMutations} from 'vuex'
在methods結(jié)尾添加
...mapMutations({
setSinger: 'SET_SINGER' // 將 `this.setSinger()` 映射為 `this.$store.commit('SET_SINGER')`
})
通過this.setSinger(singer)
實(shí)現(xiàn)了對Mutations的提交
singer-detail.vue 取出state數(shù)據(jù)
引入
import {mapGetters} from 'vuex'
在computed中
computed: {
......
...mapGetters([
'singer' // 把 `this.signer` 映射為 `this.$store.getters.singer`
])
}
至此晤锹,singer-detail 和 singer 之間就實(shí)現(xiàn) singer 的共享了