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

【音樂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中:
  1. 給<li class="list-group-item">新增點選事件:
    @click="selectItem(item)"
  2. 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}`
        })
    }
  1. 注意子路由並不是一個頁面,只是一個層,使用z-index將之前的層全部蓋住

  2. CSS樣式:
    singer-detail
         position: fixed
         z-index: 100
         top: 0
         bottom: 0
         left: 0
         right: 0
         background: $color-background
     
  • 轉場動畫:從右向左滑動
  1. 給singer-detail新增transition:
    <transition name="slide">
         <div class="singer-detail"></div>
    </transition>
  2. 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 應用程式開發的【狀態管理模式】。
  1. 它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化
  2. 適用情況:構建一箇中大型單頁應用,考慮如何更好地在元件外部管理狀態時,使用Vuex
三、Vuex初始化及歌手資料的配置

       Vuex安裝及檔案

  • 安裝
    npm install vuex --save
  • src->store目錄下新建:
  1. index.js:入口檔案
  2. state.js:管理所有狀態 state
  3. mutations.js:管理所有mutation —— 更改 Vuex 的 store 中狀態state的唯一方法
  4. mutation-types.js:管理所有mutation 事件型別(type)--字串常量
  5. actions.js:處理非同步操作和修改、以及對mutation的封裝
  6. getters.js:對獲取的state 做一些對映
  • Vuex 中的 mutation 非常類似於事件:
  1. 每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)
  2. 這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受 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()] : []
    })
  1. 在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤。這能保證所有的狀態變更都能被除錯工具跟蹤到。
  2. 不要在釋出環境下啟用嚴格模式嚴格模式會深度監測狀態樹來檢測不合規的狀態變更——請確保在釋出環境下關閉嚴格模式,以避免效能損失。
  •  main.js中:引入Store,並在new Vue例項中注入
    import store from './store'
  • singer.vue中:
  1. 引用vuex提供的【寫入資料】語法糖 
    import {mapMutations} from 'vuex'
  2. 在methods屬性中呼叫mapMutations作物件對映:把mutation的修改對映為一個方法名setSinger
    ...mapMutations({
       setSinger: 'SET_SINGER' //對應mutation-types中定義的常量
    })
  3. 在selectSinger(singer)方法中將singer傳入this.setSinger()
    selectSinger(singer){
         this.$router.push({
              path: `/singer/${singer.id}`
         })
         this.setSinger(singer)//實現對mutation的提交,向state【寫入資料】
    }
  • singer-detail.vue中:
  1. 引用vuex提供的【取出資料】語法糖
    import {mapGetters} from 'vuex'
  2. 在computed中通過mapGetters掛載singer屬性:
    computed: {
        ...mapGetters([
            'singer' //拿到getters.js中的singer
       ])
    } 
  3. 在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中:
  1. 引入getSingerDetail方法和ERR_OK常量
    import {getSingerDetail} from '@/api/singer'
    import {ERR_OK} from '@/api/config'
  2. 在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%)


 注:專案來自慕課網