【音樂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 } }
五、歌手詳情數據處理和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
  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('/') }  

      vue.js最新版獲取QQ音樂播放源

  • 播放源地址: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

——參考【螞蟻農場博客】    

  1. 獲取正確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) }) }) }

    注意:配置完之后必須重新啟動!!!

  2. 在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) }) } 
  3. 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
  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>
  2. 需要從父組件接收的props參數:
     props: { bgImage: { type: String, default: '' }, songs: { type: Array, default: [] }, title: { type: String, default: '' } }
  • singer-detail.vue中:應用music-list組件
  1. 將<div class="singer-detail">及其樣式刪掉,替換為<music-list>
    <music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list>
  2. 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組件
  1. base->song-list目錄下:創建song-list.vue
  2. 布局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>
  3. 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 
    View Code
  4. 需要從父組件接收props參數songs
    props: { songs: { type: Array, default: [] } }
  5. 將得到的數據填入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
    View Code
  • 坑:<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. 需要一個在列表文字下面的層,隨着列表的滾動實現往上推
  • 實現:
  1. <scroll>前添加布局DOM
    <div class="bg-layer" ref="layer"></div>
  2. 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">
  4. 同歌手列表: data中維護一個scrollY數據
    data() { return{ scrollY: 0 } }
  5. 在methods中定義scroll(),實時給scrollY賦值:
    scroll(pos) { this.scrollY = pos.y }
  6. 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固定不再滾動
  • 實現:
  1. mounted中記錄imageHeight,計算得到最小滾動Y
    this.imageHeight = this.$refs.bgImage.clientHeight this.minTranslateY = -this.imageHeight + RESERVED_HEIGHT //最遠滾動位置,不超過minTranslateY
  2. 定義頂部以下偏移常量:
    const RESERVED_HEIGHT = 40 //滾動偏移距離
  3. 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中沒有,需要自己封裝:利用瀏覽器的能力檢測特性
  1. 在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) } 
  2. 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>
  1. 坑:只有當列表數據都加載完成后,播放按鈕才會顯示
  2. 解決:設置按鈕顯示時機 v-show="songs.length>0"
  1. 坑:當列表滾動到頂部時,播放按鈕因為絕對定位還在,體驗不好,應該消失
  2. 解決:給按鈕添加引用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%)

 注:項目來自慕課網


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM