【音乐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%)


 注:项目来自慕课网

猜你喜欢

转载自www.cnblogs.com/ljq66/p/10166525.html