【音乐App】—— Vue-music 项目学习笔记:歌手详情页开发(含Vuex使用)
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
项目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%)
注:项目来自慕课网
越是迷茫、浮躁的时候,保持冷静和耐心,尤为重要