第6章 歌手详情页开发

包括子路由的配置及转场动画实现、Vuex 的介绍、Vuex 初始化歌手数据的配置、歌手详情页数据抓取和处理、Song 类的封装、music-list 组件开发。

6-1 歌手详情页布局和设计详解

在这里插入图片描述

6-2 子路由配置以及转场动画实现

src\router\index.js
 {
      path:'/singer',
      component:singer,
      name:'歌手',
      children:[
        {
          path:'/singer/:id',
          component:singerDetail
        }
      ]
    },

给歌手项绑定点击事件,派发数据
src\components\base\listview\listview.vue

 <li class="list-group-item" v-for="singer in item.items" :key="singer.id"  @click="selectItem(singer)">
  <img class="avatar" v-lazy="singer.avatar">
  <span class="name">{{singer.name}}</span>
</li>
selectItem(singer){
    this.$emit('select',singer)
  },

src\components\singer\singer.vue

  <listview :data="singerList" @select="selectSinger"></listview>

singer-detail.vue监听到点击的事件,改变路由

 //监听到点击的事件,改变路由
 selectSinger(singer){
     this.$router.push({
         path:`/singer/${singer.id}`
     })
},

注意:子路由并不是一个页面,只是一个层,使用z-index将之前的层全部盖住

转场动画:从右向左滑动。添加transition

 <transition name="slide">
    <router-view class="singer-detail"></router-view>
</transition>
.slide-enter-active,.slide-leave-active{
    transition: all .3s
}
.slide-enter,.slide-leave-to{
    transform: translate3d(-100%,0,0);
}

6-3 初识 Vuex

vuex是什么

vue是一种设计思想,我们把组件的数据放在同一个内存空间去管理,这个内存空间我门称它为state。state的数据能非常方便映射到组件上,来渲染组件。

当组件的数据发生变化的时候,它可以dispatch一个action(action通常是一些异步操作,和后端的一些交互,或者同时改变多个mutation时,对mutation的封装)

action可以commit一个mutation(注意也可以直接在组件中commit一个mutation的)。除了这两种方式,其他的改变state的途径都时非法的。

然后在mutations里面修改state
在这里插入图片描述
vuex这样设计的目的就是为了state的状态可以预测,当state被修改之后,它又会直接映射到组件上,这样就形成了一个闭环

在vuex中,我们必须要通过dispatch来commit一个mutation。或者直接在组件上commit一个mutation来修改state的数据,这样比起不用vuex直接在组件上修改数据复杂多了,那么我们为什么还需要使用vuex呢

vuex的应用场景

  1. 解决多个组件之间的状态共享问题(比如多个兄弟组件,多级组件之间的通讯)
  2. 解决路由之间复杂的数据传递(当在路由中,需要传递的参数过多的时候)

vuex的文件结构

文件名称描述
index.js入口文件
state.js管理多个组件公用的数据状态
mutation更改 Vuex 的 store 中状态state的唯一方法
mutation-types.js管理所有mutation 事件类型(type)–字符串常量
actions.js处理异步操作和修改、以及对mutation的封装
getters.js对获取的state 做一些映射

为什么要用mutation-types呢

  1. 便于书写方便
  2. IDE能够够帮我们检测出没我们写的对不对。通常mutations和mutation-types和actions是关联的

6-4 Vuex 初始化及歌手数据的配置

这里我们在需要在上一个路由中获取到歌手的相关信息,歌手的相关信息较多,我们不应该直接放在路由参数中传递过来,而是,我们应该通过vuex来管理singer数据

(1)确定状态
src\store\state.js

const state={
    singer:{}
}
export default state

(2)映射state里面的数据,做一层getters的封装,这样我们可以直接从getters拿到数据到组件上
src\store\getters.js

export  const singer=state=>state.singer

(3)在确定mutation之前,我们需要先确定mutation-types,在这里定义一些常量
src\store\mutation-types.js

export const SET_SINGER='SET_SINGER'

(4)在mutations里定义一些修改state的操作
src\store\mutations.js
每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)

import * as types from './mutation-types'
const mutations={
    [types.SET_SINGER](state,singer){
        state.singer=singer
    }
}
export default mutations

(5)export store 出去,并且需要在main.js里面注册,这样每个组件都可以访问到vuex管理的状态了
src\store\index.js

import Vue from 'vue'
import Vuex  from 'vuex'
Vue.use( Vuex)
import * as getters from './getters'
import * as actions from './actions'
import state  from './state'
import mutations from './mutations'
export default new Vuex.Store({
    state,
    getters,
    actions,
    mutations
})

(6)应用vuex
这里我们使用的是vuex的语法糖mapMutations,引入之后,我们需要在methods里面获取到SET_SINGER这个mutation。并且在组件上发起一个mutation
src\components\singer\singer.vue

import {mapMutations} from 'vuex'
 ...mapMutations({
    set_singer:'SET_SINGER'
})

selectSinger(singer){
     this.$router.push({
         path:`/singer/${singer.id}`
     })
     this.set_singer(singer);
},

在这里插入图片描述

6-5 歌手详情数据抓取

//获取到歌手详情页面列表数据,根据id来获取数据,所以要传入singerId
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: 'yqq',
        order: 'listen',
        begin: 0,
        num: 100,
        songstatus: 1,
        singermid: singerId
    })
    return jsonp(url,data,options)
}

src\components\singer-detail\singer-detail.vue

import {mapGetters}  from 'vuex'
export default {
    computed: {
        ...mapGetters([
            'singer'
        ])
    },
    created() {
        console.log(this.singer);
    },
}

得到vuex的singer的相关数据,我们需要获取到详情页的数据,当在当前页面刷新的时候,this.singer.id是取不到的,我们在这里需要做路由处理,当我们在当前页面刷新的时候,让页面回到歌手页

 created() {
        this._getSingerDetail();
    },

 _getSingerDetail(){
      //当在当前页面刷新的时候,跳转回singer页
      if(!this.singer.id){
          this.$router.push('/singer')
          return
      }
      //获取到列表
      getSingerDetail(this.singer.id).then(res=>{
         if(res.ERR_OK){
             //对得到的数据进行处理
            this._normallizeSingerDetail(res.data.list)
         }
      }).catch(err=>{
          console.log(err);
      })
  }

6-6 歌手详情数据处理和Song类的封装(上)

src\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=namem,
        this.album=album,
        this.duration=duration,
        this.image=image,
        this.url=url
    }
}

在singer-detail\singer-detail.vue中维护一个song引入song类文件,我们将musicData的每一项都抽象成Song类,但是传入的参数过多了,而且每次使用的时候都需要new Song。我们希望能做到的是直接传入musicData返回一个Song实例数组。

 _normallizeSingerDetail(list){
    let ret=[];//返回值
    list.forEach(musicData => {
        ret.push(new Song({
            //...传入的参数过多
        }))
    });
    return ret
    
}

6-7 歌手详情数据处理和Song类的封装(下)

在song.js中:处理musicData数据抽象出工厂方法,返回song实例

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
    }
}

//抽象出一个工厂方法:传入musicData对象参数,实例化一个Song
export default function createSong(musicData,songVkey){
    return new Song({
        id: musicData.songid,
        mid: musicData.songmid,
        singer: filterSinger(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`,
        //播放源这里的songVkey需要动态获取。
        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 ''
    }else{
        singer.forEach(item => {
            ret.push(item);
        });
    }
    return ret.join('/');
    
}


播放源songVkey的获取

播放源路径
播放源

config\index.js配置代理

//获取到播放源地址
'/api/getMusicVkey':{
   target:'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg',
   secure:false,
   changeOrigin:true,
   bypass:function(req,res,proxyOptions){
     req.headers.referer='https://y.qq.com/'
     req.headers.host='c.y.qq.com'
   },
   pathRewrite:{
    '^/api/getMusicVkey': ''
   }
}

src\api\singer.js

//获取到播放源的url
import axios  from 'axios'
export function getMusicVkey(songmid) {
    const url = '/api/getMusicVkey'
    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 new Promise(function(resolve,reject){
        axios.get(url,{ params: data}).then(res=>{
            resolve(res.data)
        }).catch(err=>{
            reject(err)
        })
   })   
}

src\components\singer-detail\singer-detail.vue

import createSong  from 'common/js/song.js'

 _normallizeSingerDetail(list){
        let ret=[];//返回值
        list.forEach(item => {
            let {musicData} = item   //得到每一项的musicData对象
            if(musicData.songid && musicData.albummid){ 
                //在这里要获取到valkey
                getMusicVkey(musicData.songmid).then(res=>{
                    if(res.code==ERR_OK){
                         let vkey=res.data.items[0].vkey
                         //传入musicData和vkey
                         ret.push(createSong(musicData,vkey))
                    }
                })
            }
        });
        return ret
    }

拿到数据

  _getSingerDetail(){
            //当在当前页面刷新的时候,跳转回singer页
            if(!this.singer.id){
                this.$router.push('/singer')
                return
            }
            //获取到列表
            getSingerDetail(this.singer.id).then(res=>{
               if(res.code==ERR_OK){
                   //对得到的数据进行处理
                  this.songs=this._normallizeSingerDetail(res.data.list)
               }
            }).catch(err=>{
                console.log(err);
            })
        },

在这里插入图片描述
拿到需要的数据之后我们可以进行开发了啦

6-8 music-list 组件发-基本布局(1)

因为歌手详情和歌单页布局上是极为相似的,在这里我们可以在singer-detail抽取出来一个music-list的业务组件,业务组件我们放在components中,当是基础组件我们放在base中
src\components\music-list\music-list.vue

<template>
   <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>
</template>

props:{
        bgImage:{
            type:String,
            default:'' 
        },
        songs:{
            type:Array,
            default:[]
        },
        title:{
            type:String,
            default:''
        }
    },
    computed: {
        bgStyle(){
            return `background-image:url(${this.bgImage})`
        }
    },

6-9 music-list 组件开发-抽取基本歌曲基本组件(2)

我们发现设计稿的好多页面都反复使用了歌曲列表的布局样式,所以在这我们将歌曲列表项抽取出来,作为基础组件
src\components\base\song-list\song-list.vue

<template>
    <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>
</template>

<script>
export default {
    props:{
        songs:{
            type:Array,
            default:[]
        }
    },
    methods: {
        getDesc(song){
            return `${song.singer}${song.album}`
        }
    },
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
  @import "~common/stylus/variable"
  @import "~common/stylus/mixin"
  .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
</style>

src\components\music-list\music-list.vue
在music-list引入scroll组件,song-list组件
在mounted中我们计算scroll的top值=图片的高度,让scroll可以滚动

<template>
   <div class="music-list">
        <div class="back">
                <i class="icon-back"></i>
        </div>
        <h1 class="title" v-html="title">{{title}}</h1>
        <div class="bg-image" :style="bgStyle"  ref="bgImage">
                <div class="filter"></div>
        </div>

        <scroll :data="songs" class="list" ref="list">
            <div class="song-list-wrapper">
                    <song-list :songs="songs"></song-list>
            </div>
        </scroll>
    </div>
    
</template>

<script>
import SongList  from 'components/base/song-list/song-list'
import scroll from 'components/base/scroll/scroll'
export default {
    props:{
        bgImage:{
            type:String,
            default:'' 
        },
        songs:{
            type:Array,
            default:[]
        },
        title:{
            type:String,
            default:''
        }
    },
    components:{
        SongList,
        scroll
    },
    computed: {
        bgStyle(){
            return `background-image:url(${this.bgImage})`
        }
    },
    mounted() {
        //因为这个得到的是VueComponent,而不是元素,所以我们这里还需要加上$el
        this.$refs.list.$el.style.top=this.$refs.bgImage.clientHeight+'px'
    }

}
</script>



<style scoped lang="stylus" rel="stylesheet/stylus">
  @import "~common/stylus/variable"
  @import "~common/stylus/mixin"
  .music-list 
    position: fixed
    z-index: 100
    top: 0
    left: 0
    bottom: 0
    right: 0
    background: $color-background
    .back
      position absolute
      top: 0
      left: 6px
      z-index: 50
      .icon-back
        display: block
        padding: 10px
        font-size: $font-size-large-x
        color: $color-theme
    .title
      position: absolute
      top: 0
      left: 10%
      z-index: 40
      width: 80%
      no-wrap()
      text-align: center
      line-height: 40px
      font-size: $font-size-large
      color: $color-text
    .bg-image
      position: relative
      width: 100%
      height: 0
      padding-top: 70%
      transform-origin: top
      background-size: cover
      .play-wrapper
        position: absolute
        bottom: 20px
        z-index: 50
        width: 100%
        .play
          box-sizing: border-box
          width: 135px
          padding: 7px 0
          margin: 0 auto
          text-align: center
          border: 1px solid $color-theme
          color: $color-theme
          border-radius: 100px
          font-size: 0
          .icon-play
            display: inline-block
            vertical-align: middle
            margin-right: 6px
            font-size: $font-size-medium-x
          .text
            display: inline-block
            vertical-align: middle
            font-size: $font-size-small
      .filter
        position: absolute
        top: 0
        left: 0
        width: 100%
        height: 100%
        background: rgba(7, 17, 27, 0.4)
    .bg-layer
      position: relative
      height: 100%
      background: $color-background
    .list
      position: fixed
      top: 0
      bottom: 0
      width: 100%
      overflow hidden
      background: $color-background
      .song-list-wrapper
        padding: 20px 30px
      .loading-container
        position: absolute
        width: 100%
        top: 50%
        transform: translateY(-50%)
</style>

6-10 music-list 滚动效果优化

在这里插入图片描述
当我们在滚动页面的时候,我们只能滚动蓝色区域的列表,这并不是我们期望的效果,我们希望的是列表可以往上滚动,列表文字下面的黑色的层,随着列表的滚动往上推

首先我们得让列表可以滚动到顶部,这时,我们需要将 .list的overflow hidden
样式去掉,这样列表可以滚动了,但是文字底部的黑色的层我们也想跟着文字滚动,我们可以通过监听scrollY的值来获得滚动的距离,让文字底部的黑色的层跟着滚动
在这里插入图片描述

那么怎么让文字底部的黑色的跟着列表滚动呢?

create()中初始化probeType,和listenScroll

created() {
    this.probeType =3,
    this.listenScroll =true
},

将probeType,和listenScroll等传递给子组件,监听滚动

<scroll :data="songs" class="list" ref="list" :probeType='probeType' :listenScroll='listenScroll' @scroll="scroll">
    <div class="song-list-wrapper">
            <song-list :songs="songs"></song-list>
    </div>
</scroll>

得到scrollY

methods: {
       scroll(pos){
           this.scrollY=pos.y//获取scroll滚动的值
       }
   }     

给页面加上黑色的层bg-layer

<!-- 加上黑色的层 -->
<div class="bg-layer" ref="layer"></div>

<scroll :data="songs" class="list" ref="list" :probeType='probeType' :listenScroll='listenScroll' @scroll="scroll">
    <div class="song-list-wrapper">
            <song-list :songs="songs"></song-list>
    </div>
</scroll>

bg-layer设置了样式position: relative;
接着我们watch scrollY的变化。动态改变 bg-layer的值

 scrollY(newY){
        console.log(newY)
        this.$refs.layer.style['transform']=`translate3d(0,${newY}px,0)`
         this.$refs.layer.style['-webkit-transform']=`translate3d(0,${newY}px,0)`
    }

这样黑色的层可以跟着列表滚动了,但是,当滚动一定的距离之后,黑色的层并不能无限滚动了,因为它的高度只有屏幕高度的100%,超过屏幕高度的部分会露出来

解决:我们需要限制bg-layer的滚动范围,当滚动都标题以下的范围就不让它滚动了

const RESERVED_HEIGHT = 40 //滚动偏移距离,顶部标题的距离

最大的滚动的距离,就是背景图片的距离-头部标题的距离
在mouted中

this.imageHeight = this.$refs.bgImage.clientHeight
this.minTranslateY = -this.imageHeight + RESERVED_HEIGHT //最远滚动位置,不超过
  watch: {
        //监听scrollY的变化
        scrollY(newY){
            //让文字底部黑色的层跟着滚动
            let translateY=Math.max(newY,this.minTranslateY)//获取到最大滚动量
            this.$refs.layer.style['transform']=`translate3d(0,${translateY}px,0)`
            this.$refs.layer.style['webkitTransform']=`transilate3d(0,${translateY}px,0)` 
        }
    },  

这样黑色的层可以跟着文字滚动而滚动了
但是当滚动到顶部的时候,我们希望白色的字不要盖着背景图片,我们应该怎么处理呢、
在这里插入图片描述

6-11 music-list 组件开发-顶部图片处理

档滚动到超过顶部40px的时候将图片的z-index变成大,将图片的高度变为40px

const RESERVED_HEIGHT=40
scrollY(newY){
            //...省略
            //对顶部样式进行处理
             let zIndex=0;//对图片的zindex做处理
             if(newY<this.minTranslateY){
                 zIndex=10;//让z-index变大
                 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['z-index']=zIndex;
        
    }

6-12 music-list 组件开发-图片放大处理

列表从初始位置向下滚动时,图片随着滚动实现放大

 scrollY(newY){
 //...省略
 // //当往下拖的时候,让图片跟着等比放大
 let scale=1;
 let percent=Math.abs(newY/this.$refs.bgImage.clientHeight)//计算比例
 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['z-index']=zIndex
 }

6-13 music-list 组件开发-高斯模糊效果实现

在bg-image里加上

 <!-- iphone手机下高斯模糊效果 -->
  <div class="filter" ref="filter"></div>
 .filter
        position: absolute
        top: 0
        left: 0
        width: 100%
        height: 100%
        background: rgba(7, 17, 27, 0.4)

高斯模糊下,必须要给当前的高斯的图层加上透明的背景,然后再back-filter:blur(20px)
在监听scrollY的时候

当往上推的时候,希望图片出现高斯模糊效果,给ref="filter"加上高斯模糊效果

backdrop-filter CSS 属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。 因为它适用于元素背后的所有元素,为了看到效果,必须使当前元素透明。

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['-webkit-backdrop-filter']=`blur(${blur}px)`

6-14 music-list 播放按钮

在图片部分bg-image里面加上播放按钮

<!-- 播放按钮 -->
    <div class="play-wrapper">
        <div class="play">
            <i class="icon-play"></i>
            <span class="text">随机播放全部</span>
        </div>
    </div>

这个播放按钮需要当有数据的时候才展示,所以我们需要加上

<div class="play-wrapper" v-show="songs.length">

但是当我们滚动到顶部的时候,播放按钮也跟着滚动上去了,当我们滚动到上方的时候,我们应该让播放按钮隐藏

if(newY < this.minTranslateY) {
    this.$refs.playBtn.style.display = 'none'
}else{
    this.$refs.playBtn.style.display = ''
}

优化:封装JS的prefixStyle

CSS中不用写prefix是因为vue-loader用到了autoprefix插件自动添加
jS中没有,需要自己封装:利用浏览器的能力检测特性
在dom.js中封装一个prefixStyle方法

src\common\js\dom.js

//封装一个prefixStyle的方法
//能力检测: 查看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)     
 }

在scrollY中将,将transform的部分替换掉

const transform = prefixStyle('transform')
this.$refs.layer.style[transform]=`translate3d(0,${translateY}px,0)`
this.$refs.bgImage.style[transform]=`scale(${scale})`
const backdrop = prefixStyle('backdrop-filter') this.$refs.filter.style[backdrop]=`blur(${blur}px)`

loading处理
因为songs的是异步获取的在获取到songs之前我们加上loading

 <scroll :data="songs" class="list" ref="list" :probeType='probeType' :listenScroll='listenScroll' @scroll="scroll">
    <div class="song-list-wrapper">
            <song-list :songs="songs"></song-list>
    </div>
    
    <!-- loading部分 -->
    <div class="loading-container" v-show="!songs.length">
        <loading></loading>
    </div>
</scroll>

.loading-container
        position: absolute
        width: 100%
        top: 50%
        transform: translateY(-50%)

github chapter6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值