【音樂App】—— Vue-music 專案學習筆記:歌手詳情頁開發
前言:以下內容均為學習慕課網高階實戰課程的實踐爬坑筆記。
專案github地址:https://github.com/66Web/ljq_vue_music,歡迎Star。
一、子路由配置以及轉場動畫實現 |
- components->singer-detail目錄下:建立singer-detai.vue
- route->index.js中:引入並配置Singer子路由SingerDetail
import SingerDetail from '@/components/singer-detail/singer-detail' { path: '/singer', component: Singer, children: [ { path: ':id', component: SingerDetail } ] }
- singer.vue中:新增<router-view></router-view>
- listview.vue中:
- 給<li class="list-group-item">新增點選事件:
@click="selectItem(item)"
- methods中定義selectItem方法,將item作為事件引數,派發出去:
selectItem(item){ this.$emit('select', item) }
- singer.vue中的<listview>監聽select事件,觸發selectSinger,執行業務邏輯:
@select="selectSinger"
selectSinger(singer){ this.$router.push({ //動態新增路由地址 path: `/singer/${singer.id}` }) }
-
注意:子路由並不是一個頁面,只是一個層,使用z-index將之前的層全部蓋住
- CSS樣式:
singer-detail position: fixed z-index: 100 top: 0 bottom: 0 left: 0 right: 0 background: $color-background
- 轉場動畫:從右向左滑動
- 給singer-detail新增transition:
<transition name="slide"> <div class="singer-detail"></div> </transition>
- CSS樣式:
.slide-enter-active, .slide-leave-active transition: all 0.3s .slide-enter, .slide-leave-to transform: translate3d(100%, 0, 0) //100% 完全移動到螢幕右側 動畫開始後向左滑入
二、Vuex |
- 問題:子路由SingerDetail需要從父路由頁面Singer獲取很多資料,都用引數獲取內容太多
- 解決:使用Vuex實現路由之間引數資料的獲取
- Vuex GitBook地址:https://vuex.vuejs.org/zh/
- 什麼是Vuex:Vuex 是一個專為 Vue.js 應用程式開發的【狀態管理模式】。
- 它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化
- 適用情況:構建一箇中大型單頁應用,考慮如何更好地在元件外部管理狀態時,使用Vuex
三、Vuex初始化及歌手資料的配置 |
Vuex安裝及檔案
- 安裝
npm install vuex --save
- src->store目錄下新建:
- index.js:入口檔案
- state.js:管理所有狀態 state
- mutations.js:管理所有mutation —— 更改 Vuex 的 store 中狀態state的唯一方法
- mutation-types.js:管理所有mutation 事件型別(type)--字串常量
- actions.js:處理非同步操作和修改、以及對mutation的封裝
- getters.js:對獲取的state 做一些對映
- Vuex 中的 mutation 非常類似於事件:
- 每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)
- 這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個引數
歌手資料配置:
- state.js中:定義singer資料
const state = { singer: {} } export default state
- mutation-types.js中:定義設定singer資料的字串常量
export const SET_SINGER = 'SET_SINGER'
- mutations.js中:對state進行修改,引入mutation-types作關聯
import * as types from './mutation-types' const mutations = { [types.SET_SINGER](state, singer){ state.singer = singer } } export default mutations
- getter.js中:對state進行包裝和輸出,獲得state.singer
export const singer = state => state.singer //state => state.singer 箭頭函式的簡寫,state是一個function,return返回一個state.singer
- 同步修改,只需要通過mutation修改,不需要action進行非同步操作
- 初始化 index.js入口檔案:
import Vue from 'vue' import Vuex from 'vuex' // * as 是es6的新import語法 import * as actions from './actions' import * as getters from './getters' import state from './state' import mutations from './mutations' //Vuex 內建日誌外掛用於一般的除錯 import createLogger from 'vuex/dist/logger' Vue.use(Vuex) //只在開發環境時啟動嚴格模式 const debug = process.env.NODE_ENV !== 'production' //工廠方法輸出一個單例Vuex.Store模式 export default new Vuex.Store({ actions, getters, state, mutations, strict: debug, plugins: debug ? [createLogger()] : [] })
- 在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤。這能保證所有的狀態變更都能被除錯工具跟蹤到。
- 不要在釋出環境下啟用嚴格模式:嚴格模式會深度監測狀態樹來檢測不合規的狀態變更——請確保在釋出環境下關閉嚴格模式,以避免效能損失。
- main.js中:引入Store,並在new Vue例項中注入
import store from './store'
- singer.vue中:
- 引用vuex提供的【寫入資料】語法糖
import {mapMutations} from 'vuex'
- 在methods屬性中呼叫mapMutations作物件對映:把mutation的修改對映為一個方法名setSinger
...mapMutations({ setSinger: 'SET_SINGER' //對應mutation-types中定義的常量 })
- 在selectSinger(singer)方法中將singer傳入this.setSinger()
selectSinger(singer){ this.$router.push({ path: `/singer/${singer.id}` }) this.setSinger(singer)//實現對mutation的提交,向state【寫入資料】 }
- singer-detail.vue中:
- 引用vuex提供的【取出資料】語法糖
import {mapGetters} from 'vuex'
- 在computed中通過mapGetters掛載singer屬性:
computed: { ...mapGetters([ 'singer' //拿到getters.js中的singer ]) }
- 在created()中打印出this.singer,檢視vuex中資料的傳遞是否成功
created() { console.log(this.singer) }
四、歌手詳情資料抓取 |
- api->singer.js中:
export function gerSingerDetail(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: 'yqq', order: 'listen', begin: 0, num: 100, songstatus: 1, singermid: singerId }) return jsonp(url, data, options) }
- singer-detail.vue中:
- 引入getSingerDetail方法和ERR_OK常量
import {getSingerDetail} from '@/api/singer' import {ERR_OK} from '@/api/config'
- 在methods中定義_getDetail()私有方法,通過呼叫getSingerDetail()返回promise物件,獲取singer資料
_getDetail() { getSingerDetail(this.singer.id).then((res) => { if(res.code === ERR_OK){ console.log(res.data.list) } }) }
- 坑:只有從singer頁面選擇歌手跳轉到對應singer-detail路由中,才能得到singer資料;在singer-detail路由頁面重新整理時不會得到資料,這樣也是沒有意義的
- 解決: 在_getDetail()中新增判斷,當獲取不到singer.id時,呼叫this.$route.push,使頁面回退到singer路由
if(!this.singer.id){ this.$router.push('/singer') return } }
5.歌手詳情資料處理和Song類的封裝
①api目錄下建立song.js:
使用JavaScript constructor 屬性構造一個Song類
設計為類而不是物件的好處:
1.可以把程式碼集中的一個地方維護
2.類的擴充套件器比物件的擴充套件器強很多,而且它是一種面向物件的程式設計方式
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
}
}
這樣就可通過遍歷res.data.list資料,得到經過Song類封裝的物件
②歌手詳情資料處理: singer-detail.vue
1)data中維護一個數據songs:[]
2)在song.js中處理musicData資料抽象出工廠方法,返回song例項:
//抽象出一個工廠方法:傳入musicData物件引數,例項化一個Song
export function createSong(musicData){
return new Song({
id: musicData.songid,
mid: musicData.songmid,
singer: filterSinger(musicData.singer),
name: musicData.songname,
album: musicData.albumname,
duration: musicData.interval, //歌曲時長s
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`
//注意guid以實時資料為主
url: `http://dl.stream.qqmusic.qq.com/C400${musicData.songmid}.m4a?vkey=${songVkey}&guid=6319873028&uin=0&fromtag=66`
})
}
//格式化處理singer資料
function filterSinger(singer){
let ret = []
if(!singer){
return ''
}
singer.forEach((s) => {
ret.push(s.name)
})
return ret.join('/')
}
guid=6319873028 songmid: "001Qu4I30eVFYb"
播放源地址:http://dl.stream.qqmusic.qq.com/C400001apXAh2mHRub.m4a?guid=6319873028
&vkey=6DAE080C291DECFDC9A3C532879658439F66EBA6C588813C8A1C12917030F
A050C2352C15343CCCAC8FDE731383C2489026145978797D513&uin=0&fromtag=66
拼接的url: http://dl.stream.qqmusic.qq.com/C400${musicData.songmid}.m4a?vkey=${songVkey}&guid=6319873028&uin=0&fromtag=66
注意:guid是會變化的,以自己抓取的實際值為準,需改動的有兩處:①song.js中拼接的url ②singer.js中引數guid
抓取詳解地址:https://blog.csdn.net/qq_33026699/article/details/80777015
3)獲取正確url需要反向代理的方式請求vkey: webpack.dev.config.js中配置
app.get('/api/music', function(req, res){
var url="https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg"
axios.get(url, {
headers: { //通過node請求QQ介面,傳送http請求時,修改referer和host
referer: 'https://y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query //把前端傳過來的params,全部給QQ的url
}).then((response) => {
res.json(response.data)
}).catch((e) => {
console.log(e)
})
})
}
注意:配置完之後必須重新啟動
4)在api->singer.js中定義getMusic方法獲取vkey:
export function getMusic(songmid) {
const url = '/api/music'
const data = Object.assign({}, commonParams, {
songmid: songmid,
filename: 'C400' + songmid + '.m4a',
guid: 6319873028, //會變,以實時抓取的資料為準
platform: 'yqq',
loginUin: 0,
hostUin: 0,
needNewCode: 0,
cid:205361747,
uin: 0,
format: 'json'
})
return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
}
5) methods中定義方法_normallizeSongs(list)按需求重新處理資料:
_normallizeSongs(list){
let ret = [] //返回值
list.forEach((item) => {
let {musicData} = item //得到music物件
// console.log(musicData)
//createSong必傳兩個引數
if(musicData.songid && musicData.albummid){
// console.log(getMusic(musicData.songmid))
getMusic(musicData.songmid).then((res) => {
// console.log(res)
if(res.code === ERR_OK){
// console.log(res.data)
const svkey = res.data.items
const songVkey = svkey[0].vkey
const newSong = createSong(musicData, songVkey)
ret.push(newSong)
}
})
}
})
// console.log(ret)
return ret
}
6) _getDetail()中:將處理好的資料賦給songs
this.songs = this._normallizeSongs(res.data.list)
6. music-list元件開發
①在components->music-lict目錄下:建立music-list.vue
1)佈局DOM:
<div class="music-list">
<div class="back">
<i class="icon-back"></i>
</div>
<h1 class="title" v-html="title"></h1>
<div class="bg-image" :style="bgStyle">
<div class="filter"></div>
</div>
</div>
CSS樣式:
2)需要從父元件接收的props引數:
props: {
bgImage: {
type: String,
default: ''
},
songs: {
type: Array,
default: []
},
title: {
type: String,
default: ''
}
}
3)singer-detail.vue中應用music-list元件:
將<div class="singer-detail">及其樣式刪掉,替換為<music-list>:
<music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list>
title和bgImage資料通過computed計算得到:
title() {
return this.singer.name
},
bgImage() {
return this.singer.avatar
}
4)music-list.vue中將獲得的資料填入DOM,bgStyle樣式屬性通過computed計算得到:
bgStyle() {
return `background-image: url(${this.bgImage})`
}
②歌曲列表抽象為song-list元件:
base->song-list目錄下:建立song-list.vue
1)佈局DOM:
<div class="song-list">
<ul>
<li 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>
</li>
</ul>
</div>
CSS樣式:
.song-list
.item
display: flex
align-items: center
box-sizing: border-box
height: 64px
font-size: $font-size-medium
.content
flex: 1
line-height: 20px
overflow: hidden
.name
no-wrap()
color: $color-text
.desc
no-wrap()
margin-top: 4px
color: $color-text-d
2)需要從父元件接收props引數songs
props: {
songs: {
type: Array,
default: []
}
}
3)將得到的資料填入DOM,其中desc通過methods定義getDesc(song)得到:
methods: {
getDesc(song){
return `${song.singer} 。${song.album}`
}
}
③在music-list.vue中應用song-list元件
1)引用並註冊scroll和song-list元件:
import Scroll from '@/base/scroll/scroll'
import SongList from '@/base/song-list/song-list'
2)佈局DOM
<scroll :data="songs" class="list" ref="list">
<div class="song-list-wrapper">
<song-list :songs="songs"></song-list>
</div>
</scroll>
3) CSS樣式:
.list
position: fixed
top: 0
bottom: 0
width: 100%
overflow: hidden
background: $color-background
.song-list-wrapper
padding: 20px 30px
坑:<scroll class="list">的top值不能寫死,因為不同瀏覽器不同視口中bgImage的高度是不同的
解決:給bgImage和list都新增ref引用,在mounted中得到當前載入好的bgImage的高度,動態賦值給top
<div class="bg-image" :style="bgStyle" ref="bgImage">
<scroll :data="songs" class="list" ref="list">
mounted() {
this.$refs.list.$el.style.top = `${this.$refs.bgImage.clientHeight}px`
}
④ 歌手詳情頁互動效果:
需求:
1)允許列表可以往上滾動,music-list.vue中去掉list的樣式:overflow: hidden
2)需要一個在列表文字下面的層,隨著列表的滾動實現往上推:
<scroll>前添加布局DOM: <div class="bg-layer" ref="layer"></div>
CSS樣式:
.bg-layer
position: relative
height: 100% //螢幕高度的100%
background: $color-background
3)create()中新增屬性,監聽滾動:
created() {
this.probeType = 3
this.listenScroll = true
}
將屬性傳入<scroll>中,並監聽scroll事件,實時監聽scroll位置:
<scroll :data="songs"
class="list"
ref="list"
:probe-type="probeType"
:listen-scroll="listenScroll"
@scroll="scroll">
同歌手列表: data中維護一個scrollY資料
data() {
return{
scrollY: 0
}
}
在methods中定義scroll(),實時給scrollY賦值:
scroll(pos) {
this.scrollY = pos.y
}
watch:{}中監測scrollY,為layer新增引用,設定layer的transform:
watch: {
scrollY(newY) {
this.$refs.layer.style['transform'] = `translate3d(0, ${newY}px, 0)`
this.$refs.layer.style['webkitTransform'] = `translate3d(0, ${newY}px, 0)`
}
}
坑:bg-layer的高度只有螢幕高度的100%,並不能無限滾動,當超出螢幕高度後下面的內容會露出來
解決:限制bg-layer的滾動位置,最遠只能滾動到標題以下,再往上滾動列表時,bg-layer固定不再滾動
實現:①mounted中記錄imageHeight,計算得到最小滾動Y:
this.imageHeight = this.$refs.bgImage.clientHeight
this.minTranslateY = -this.imageHeight + RESERVED_HEIGHT //最遠滾動位置,不超過minTranslateY
②定義頂部以下偏移常量:const RESERVED_HEIGHT = 40 //滾動偏移距離
scrollY(newY)中得到最大滾動量,修改transform替換newY:
watch: {
scrollY(newY) {
let translateY = Math.max(this.minTranslateY, newY) //最大滾動量
this.$refs.layer.style['transform'] = `translate3d(0, ${translateY}px, 0)`
this.$refs.layer.style['webkitTransform'] = `translate3d(0, ${translateY}px, 0)`
}
}
坑:當滾動到頂部時,列表文字會遮住圖片,需要圖片遮住文字
解決:scrollY(newY)中新增判斷,當滾到頂部時,改變圖片的z-index和高度,否則,重置回初始位置
//滾動到頂部時,圖片遮住文字
let zIndex = 0
if(newY < this.minTranslateY) {
zIndex = 10
this.$refs.bgImage.style.paddingTop = 0
this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`
}else{
this.$refs.bgImage.style.paddingTop = '70%'
this.$refs.bgImage.style.height = 0
}
this.$refs.bgImage.style.zIndex = zIndex
需求:列表從初始位置向下滾動時,圖片隨著滾動實現縮小放大
let scale = 1
const percent = Math.abs(newY / this.imageHeight)
if(newY > 0) {
scale = 1 + percent
zIndex = 10
}
this.$refs.bgImage.style['transform'] = `scale(${scale})`
this.$refs.bgImage.style['webkitTransform'] = `scale(${scale})`
this.$refs.bgImage.style.zIndex = zIndex
圖片從頂部放大縮小,關鍵樣式:transform-origin: top
需求:列表滾動到頂部時,(iphone手機中)圖片有一個高斯模糊的變化
<div class="bg-image" :style="bgStyle" ref="bgImage">
<div class="filter" ref="filter"></div>
</div>
let blur = 0
if(newY <= 0){
blur = Math.min(20 * percent, 20)
}
this.$refs.filter.style['backdrop-filter'] = `blur(${blur}px)`
this.$refs.filter.style['webkitBackdrop-filter'] = `blur(${blur}px)`
⑤ 優化:封裝JS的prefixStyle
CSS中不用寫prefix是因為vue-loader用到了autoprefix外掛自動新增
JS中沒有,需要自己封裝:利用瀏覽器的能力檢測特性
在dom.js中擴充套件一個方法:
//能力檢測: 檢視elementStyle支援哪些特性
let elementStyle = document.createElement('div').style
//供應商: 遍歷查詢瀏覽器的字首名稱,返回對應的當前瀏覽器
let vendor = (() => {
let transformNames = {
webkit: 'webkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
ms: 'msTransform',
standard: 'transform'
}
for (let key in transformNames) {
if(elementStyle[transformNames[key]] !== undefined) {
return key
}
}
return false
})()
export function prefixStyle(style) {
if(vendor === false){
return false
}
if(vendor === 'standard'){
return style
}
return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}
music-list.vue中引用prefixStyle,並定義常量代替原始屬性,刪掉手動新增prefix的語句
import {prefixStyle} from '@/common/js/dom'
const transform = prefixStyle('transform')
const backdrop = prefixStyle('backdrop-filter')
this.$refs.layer.style[transform] = `translate3d(0, ${translateY}px, 0)`
this.$refs.bgImage.style[transform] = `scale(${scale})`
this.$refs.filter.style[backdrop] = `blur(${blur}px)`
⑥ 返回按鈕:@click="back"
back(){
this.$router.back() //回退到上一級路由
}
播放按鈕:
<div class="play-wrapper">
<div class="play">
<i class="icon-play"></i>
<span class="text">隨機播放全部</span>
</div>
</div>
坑:只有當列表資料都載入完成後,播放按鈕才會顯示
解決:設定按鈕顯示時機 v-show="songs.length>0"
坑:當列表滾動到頂部時,播放按鈕因為絕對定位還在,體驗不好,應該消失
解決:給按鈕新增引用ref="playBtn",在scrollY(newY)中判斷滾動到頂部時修改display為none,正常顯示時重置為空
if(newY < this.minTranslateY) {
this.$refs.playBtn.style.display = 'none'
}else{
this.$refs.playBtn.style.display = ''
}
⑦ 優化:非同步獲取的歌曲資料顯示之前,新增loading
<div class="loading-container" v-show="!songs.length">
<loading></loading>
</div>
.loading-container
position: absolute
width: 100%
top: 50%
transform: translateY(-50%)
注:專案來自慕課網