前言:以下內容均為學習慕課網高級實戰課程的實踐爬坑筆記。
項目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 } }
五、歌手詳情數據處理和Song類的封裝 |
- api目錄下創建song.js:使用JavaScript constructor 屬性構造一個Song類
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
- data中維護一個數據 songs:[ ]
- 在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('/') }
vue.js最新版獲取QQ音樂播放源
——參考【螞蟻農場博客】 |
- 獲取正確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) }) }) }
注意:配置完之后必須重新啟動!!!
- 在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) }) }
- 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 }
- _getDetail()中:將處理好的數據賦給songs
this.songs = this._normallizeSongs(res.data.list)
六、music-list組件開發 |
- 在components->music-lict目錄下:創建music-list.vue
- 布局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>
- 需要從父組件接收的props參數:
props: { bgImage: { type: String, default: '' }, songs: { type: Array, default: [] }, title: { type: String, default: '' } }
- 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 }
- music-list.vue中:將獲得的數據填入DOM,bgStyle樣式屬性通過computed計算得到
bgStyle() { return `background-image: url(${this.bgImage})` }
- 【歌曲列表】抽象為song-list組件
- base->song-list目錄下:創建song-list.vue
- 布局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
- 需要從父組件接收props參數songs
props: { songs: { type: Array, default: [] } }
- 將得到的數據填入DOM,其中desc通過methods定義getDesc(song)得到
methods: { getDesc(song){ return `${song.singer} 。${song.album}` } }
- 在music-list.vue中應用song-list組件
- 引用並注冊scroll和song-list組件
import Scroll from '@/base/scroll/scroll' import SongList from '@/base/song-list/song-list'
- 布局DOM
<scroll :data="songs" class="list" ref="list"> <div class="song-list-wrapper"> <song-list :songs="songs"></song-list> </div> </scroll>
- 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` }
七、歌手詳情頁交互效果 |
需求:
- 允許列表可以往上滾動,music-list.vue中去掉list的樣式:overflow: hidden
- 需要一個在列表文字下面的層,隨着列表的滾動實現往上推
- 實現:
- <scroll>前添加布局DOM
<div class="bg-layer" ref="layer"></div>
- CSS樣式:
.bg-layer position: relative height: 100% //屏幕高度的100% background: $color-background
- 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
需求:列表從初始位置向下滾動時,圖片隨着滾動實現縮小放大
- 圖片從頂部放大縮小,關鍵樣式:transform-origin: top
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
需求:列表滾動到頂部時,(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%)
注:項目來自慕課網