{{song.name}}
{{getDesc(song)}}
歌手详情界面的开发:
singer-detail组建的开发:创建并编写简单的页面
在index.js中重新配置路由,点击歌手就可以转到详情列表了
{
path: '/singer',
component: Singer,
children: [
{
path: ':id',
component: SingerDetail
}
]
},
配置好子路由之后,要有一个routerview去承载这个子路由,我们回到singer组件,在list-view(歌手列表组件)的下边挂载子router-view
添加路由跳转逻辑,我们希望我们点击歌手列表的时候,可以进行路由跳转,歌手列表基于list-view实现,所以在listview组件中,添加一个点击事件selectItem();
-
{{item.name}}
在listview中添加点击事件并将item派发出去,点击事件没有业务逻辑,仅仅是将数据传出去
selectItem(item) {
this.$emit('select', item) // 将点击的是哪一位歌手派发出去
},
回到singer.vue界面监听函数
methods中实现监听函数
selectSinger(singer) {
this.$router.push({
path: `/singer/${singer.id}` // // router的编程式跳转接口
})
// this.setSinger(singer)
console.log(`${singer.id}`)
},
点击歌手出现歌手详情,但是界面的跳转略显生硬,我们为singer-detail的跳转增加一个transition动画
.slide-enter-active, .slide-leave-active
transition all 0.3s
.slide-enter, .slide-leave-to
transform translate3d(100%, 0, 0)
当我们点击歌手列表切换到歌手详情页的时候,需要把歌手的数据,歌手的名称,图片等,通过参数的传递也是可以解决的,利用vuex解决这个问题也可。
state:所有组件的所有状态和数据,放在同一的内存空间来管理
vue Components:state中的数据可以映射到组件上来渲染组件
Action:当组件中的数据发生变化的时候,Component可以通过dispatch一个action,action会做一些异步操作,
mutation:Action commit到mutation,他是唯一一个可以修改state的途径,其他任何方式修改state都是非法的
使用场景:解决多个组件之间的状态共享,这些组件可能是一些关联度很低的组件,所以我们想要共享数据就比较困难
比如遇到一些路由跳转场景,如果我们传递的参数很复杂的话,vuex是很好的选择
3.vuex可以解决多个组件之间的状态共享,路由间的复杂数据传递,
index.js: vuex的入口函数src->store->index.js,
state.js: 状态管理:src->store->state.js,
mutation建立mutations.js,
mutation-types: 与mutation相关的mutation名字的文件叫mutation-types,存储于mutation相关的字符串常亮,
actions.js:异步修改,或者对mutation做一些封装,
getters.js对state数据进行映射,
首先,定义state.js
const state = {
singer: {},
playing: false // 播放状态
}
export default state
mutation-type:定义常量
// 定义mutation的常量
export const SET_SINGER = 'SET_SINGER'
然后在mutations.js中定义修改的操作:
import * as types from './mutation-types'
const mutations = { // mutation相关的修改方法
[types.SET_SINGER](state, singer) { // 当前状态树的state,提交mutation时传的payload
state.singer = singer
}
}
有了mutation修改数据,我们怎么映射到state中去呢,通常从getters中取state数据到vue components中
export const singer = state => state.singer // 使用getter取到组件里的数据
action.js暂时不用初始化,因为还没有相关action的修改
接下来将入口js,即index.js进行初始化
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'
import createLogger from 'vuex/dist/logger'
Vue.use(Vuex) // 注册插件
// 调试工具
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({ // 我们要去export store的一个实例,单例模式
actions,
getters,
state,
mutations,
strict: debug, // 检测state的修改是不是来源于mutation
plugins: debug ? [createLogger()] : [] // 通过mutation修改state的时候会在控制台打印logger
})
在main.js中修改,将store注入到vue中,此时,vuex的初始化配置完成
import store from './store'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
4.回到singer.vue界面:当singer.vue组件进行跳转的时候,我没去去set这个singer,我们把这个singer当成vuex的一个数据
import {mapMutations} from 'vuex'
mapMutation是对mutation进行了一层封装,我们import Mapmutation之后,我们只要在methods函数的最后,通过扩展运算符的方式调用mapMutations,去做一个对象的映射,将mutation的修改映射成一个方法名,setSinger,他实际上就是mutation-types中的SET_SINGER常量
...mapMutations({
setSinger: 'SET_SINGER'
})
之后再代码中,我们就可以调用setSinger方法往state中写数据了
selectSinger(singer) {
this.$router.push({ // router的编程式跳转接口
path: `/singer/${singer.id}`
})
// 在setSinger中将数据(singer)传进来
// 实现了对一个mutation的提交,修改了state,实际上执行了mutations.js中的types.SER_SINGER函数
// state.singer = singer,实现了对state的修改,这里是往state中写了singer数据
this.setSinger(singer)
// console.log(`${singer.id}`)
}
将singer数据提交到state之后,然后在详情界面singer-detail.vue中去获取数据
import {mapGetters} from 'vuex' // vuex为取出数据提供的语法糖
在computed中执行mapGetters函数
computed: { // getter映射的就是computed
...mapGetters([
// export const singer = state => state.singer // 使用getter取到组件里的数据
'singer' // 对应到store中的getters定义的singer
])
}
做了这一层映射以后,我们就相当于在vue实例中挂载了一个叫singer的属性,然后我们就可以拿到singer,在created的时候进行consloe.log()进行测试
created() {
console.log(this.singer) // 测试输出singer数据
}
}
5.歌曲详情数据的抓取:singermid不同,歌手详情数据就不同,其他的都一样
src->api>singer.js
// 歌手详情数据
export function getSingerDetail(singerId) { // id不同获取的数据就不同
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->common->singer-detail.vue中,在created中定义获取数据的方法
import {getSingerDetail} from 'api/singer'
import {ERR_OK} from 'api/config'
created() {
// console.log(this.singer) // 测试输出singer数据
// 调用方法获取数据
this._getDetail()
}
在_getDetail()中调用刚才的api方法getSingerDetail,之前我们已经通过...mapGetter()获取到了singer,这里讲singer的id传入getSingerDetail函数;找不到歌手id的时候,刷新会退回到singer路由
_getDetail() {
if (!this.singer.id) { // 在详情界面刷新找不到id的情况,回退到歌手列表界面
this.$router.push('/singer')
return
}
// 因为我们已经获取到了singer数据,此处可以通过this.singer直接调用获取歌手的id
getSingerDetail(this.singer.id).then((res) => {
if (res.code === ERR_OK) {
this.songs = this._normalizeSongs(res.data.list)
console.log(this.songs)
}
})
}
_normalizeSongs函数是对歌手数据进行格式化处理:跟之前的singer类中进行new singer处理相同,在这里,我们创建一个song类,对song的相关属性进行封装,参数很多的场景就可以设计成类,没有设计成对象,是因为累的扩展性要比对象好很多
6,创建common->js->song.js,类似于singer的id,name,avatar
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
}
在singerDetail里面我们去维护一个data,return一个song
data() {
return {
songs: []
}
}
接着刚才的_normalizeSongs,在_normalizeSongs中对歌手详情list数据做一个规范化处理
_normalizeSongs(list) {
let ret = []
list.forEach((item) => {
let {musicData} = item // 对象解构赋值,只取musicData数据
if (musicData.songid && musicData.albummid) {
ret.push(createSong(musicData)) // 将musicData转化为song,直接new的方式代码量较多,编写一个工厂方法代替
}
})
return ret
}
}
createSong(musicData)) // 将获取到的musicData源数据转化为我们定义好的song类,直接new的方式代码量较多,编写一个工厂方法代替,所以在js->song.js中
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, // 歌曲的时长
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=32`
// url: `http://ws.stream.qqmusic.qq.com/C100${musicData.songid}.m4a?fromtag=38`
// url: `http://dl.stream.qqmusic.qq.com/C400${musicData.songmid}.m4a?guid=106795008&vkey=AE1D90F43963ABF97FAE0968D8581710AF21B640A32A93C45FF503192B6D352DCCE278E904C9FCC09D883B9AA30BEEDDBE7112C05DA7603D&fromtag=38`
url: `http://dl.stream.qqmusic.qq.com/C400${musicData.songmid}.m4a?guid=6925689185&vkey=FC8A2D541E751BD37E80AA099D4762196B6B7E9C26B8AFB70F25C16D30A73E39FC8E4597AFB47483D4747FB5774740FFCACEAE5288D188A3&fromtag=66`
})
}
singer展示的是一个歌手,如果有两个的话就用/来区分两个及以上的歌手,在元数据中singer是一个数组,这不是我们想要的格式,我们希望数据可以直接应用到DOM上,所以对singer数据进行过滤
function filterSinger(singer) {
let ret = []
if (!singer) {
return ''
}
singer.forEach((s) => {
ret.push(s.name)
})
return ret.join('/')
}
歌手的图片和url地址是根据id进行拼接的,拿到url v-key地址经常变化,不知道怎么回事儿
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=32`
// url: `http://ws.stream.qqmusic.qq.com/C100${musicData.songid}.m4a?fromtag=38`
// url: `http://dl.stream.qqmusic.qq.com/C400${musicData.songmid}.m4a?guid=106795008&vkey=AE1D90F43963ABF97FAE0968D8581710AF21B640A32A93C45FF503192B6D352DCCE278E904C9FCC09D883B9AA30BEEDDBE7112C05DA7603D&fromtag=38`
url: `http://dl.stream.qqmusic.qq.com/C400${musicData.songmid}.m4a?guid=6925689185&vkey=FC8A2D541E751BD37E80AA099D4762196B6B7E9C26B8AFB70F25C16D30A73E39FC8E4597AFB47483D4747FB5774740FFCACEAE5288D188A3&fromtag=66`
回到singer-detail.vue引入createSong,接着对数据进行规范化处理
import {createSong} from 'common/js/song'
_normalizeSongs(list) {
let ret = []
list.forEach((item) => {
let {musicData} = item // 对象解构赋值,只取musicData数据
if (musicData.songid && musicData.albummid) {
ret.push(createSong(musicData)) // 将musicData转化为song,直接new的方式代码量较多,编写一个工厂方法代替
}
})
return ret
}
}
这样,ret数组中所有的Song对象,都是我们对源数据进行处理后提取的我们需要的数据格式了,之后将_normalizeSongs函数应用到getSingerDetail函数中,输出之后,数据就是我们想要的格式了
_getDetail() {
if (!this.singer.id) { // 在详情界面刷新找不到id的情况,回退到歌手列表界面
this.$router.push('/singer')
return
}
// 因为我们已经获取到了singer数据,此处可以通过this.singer直接调用获取歌手的id
getSingerDetail(this.singer.id).then((res) => {
if (res.code === ERR_OK) {
this.songs = this._normalizeSongs(res.data.list)
console.log(this.songs)
}
})
}
created() {
// console.log(this.singer) // 测试输出singer数据
// 调用方法获取数据
this._getDetail()
}
我们发现歌手的详情页的歌单部分和歌单详情页歌单部分都是类似的,所以我们将其抽象成一个musiclist组件,通过props传入不同的数据来实现不同的歌单列表
7.获取完数据并进行规范化处理之后,编写music-list.vue组件,写好主体框架,
在props中接受父组件传递过来的数据:
props: {
bgImage: {
type: String,
default: ''
},
songs: {
type: Array,
default: []
},
title: {
type: String,
default: ''
}
}
将其引入到父组件singer-detail.vue中,singer-detail.vue没有其他dom结构了,只需要调用music-list组件就成,父组件singer-detail主要负责给子组件singer-detail传递数据即可,不仅要忘了引用注册组件
songs直接将当当前的songs传入即可
data() {
return {
songs: []
}
}
通过计算属性拿到title和bgImage的值,singer在mapGetter中得到
computed: { // getter映射的就是computed
title() {
return this.singer.name
},
bgImage() {
return this.singer.avatar
},
...mapGetters([ // export const singer = state => state.singer // 使用getter取到组件里的数据
'singer' // 对应到store中的getters定义的singer
])
}
8. 回到list-music.vue处理拿到的数据,填充DOM
显示歌手名:
显示歌手图片:
bgStyle可以通过一个计算属性得到
bgStyle() {
return `background-image:url(${this.bgImage})`
}
这样我们就将图片加载到了组件的上方,,图片下边的歌单样式都是复用的,可以复用,抽象成song-list组件
9. 为了方便复用歌曲列表,将列表抽象成一个基础组件src->base->song-list->song-list.vue,
首先在props中定义要接受的数据
props: { // 需要接收的数据
songs: {
type: Array,
default: []
}
}
拿到基础数据之后去填充DOM,基础框架如下
-
{{song.name}}
{{getDesc(song)}}
然后去获得歌曲的描述: getDesc();
getDesc(song) {
return `${song.singer}.${song.album}`
}
歌曲列表组件song.vue开发完成,我们回到music-list(song.vue+背景)中去应用这个组件
将其引入到父组件music-list.vue中,并添加scroll,为控制内部样式,在song-list.vue的外边添加一个wrapper,将songs作为data传入scroll中,在scroll组件中规定只有data中含有数据时才会滚动,并将songs的值传递给song-list组件
之后,指定歌手的歌曲列表实现并可以滚动,但是高度不对,歌手的图片消失了,所以在music-list.vue组件中,为scroll添加一个ref,拿到sroll的DOM,为其添加一个top值,为图片腾出空间(因为不同的设备背景图的高度是不一样的)
我们在mounted的生命周期下控制它的top值
mounted() {
// list是一个component对象,要通过$el取得它的DOM,将scoll的高度设置为背景图的高度
this.$refs.list.$el.style.top = `${this.imageHeight}px`
}
我们希望列表向上滑动的时候,上部图片可以向上移动
1)先将music-list的overflow属性去掉,这样歌曲列表(song-list.vue)就可以滚动上去盖住图像部分了
2) 这时需要一个位于歌单列表下方的图层(bg-layer),当歌单列表向上滑动的时候filter也跟着向上滑动,盖住下方的图像,
接下来我们要实现的是scroll层(song-list.vue)滚动的时候,bg-layer层也跟着向上滚动,所以我们要监听scroll层,也就是song-list层滚动的位置
首先在music-list中设置scroll开启监听,之后scroll就会将pos派发出来,然后再用scroll监听事件,获取自定义参数scrollY的值
created () {
this.probeType = 3
this.listenScroll = true // 监听滚动
}
然后将比变量传到scroll中,设置监听方法scroll,因为只要listenScroll为ture,scroll中就会派发函数,music-list中就可以监听到scroll的pos
在scroll中initScroll的时候已经定义好了派发函数
// 如果要监听滚动事件,在初始化列BScroll之后要派发一个监听事件
if (this.listenScroll) {
// BScroll 中的this是默认指向scroll的,所以要在me中保留vue实例的this
let me = this
// 监听scroll,拿到pos后,派发一个函数将pos传出去
this.scroll.on('scroll', (pos) => {
me.$emit('scroll', pos)
})
}
这样,我们只需要在music-list函数在编写监听函数,获得scrollY的位置即可
data() {
return {
scrollY: 0
}
}
methods: {
scroll(pos) {
this.scrollY = pos.y // 实时拿到scrollY的值
}
拿到scrollY的值之后就可以设置bg-laye的偏移量
监听scrollY的变化,scrollY发生变化时,bg-layer层跟着滚动scrollY的位置
watch: {
scrollY(newY) {
this.$refs.layer.style['transform'] = `translate3D(0, ${newY}px, 0)`
this.$refs.layer.style['webkitTransform'] = `translate3D(0, ${newY}px, 0)`
}
}
3)bg-layer层高度是100%的,当by-layer滚动高度的超过屏幕高度时,图片又会漏出来,我们希望bg-layer滚动到一定位置就停止,所以要设置一下他的最大滚动距离,在mounted中设置一个最小滚动距离,在watch的时候当scrollY超过最小滚动距离时就不动了
首先在mounted中记录一下最大滚动高度imageHeight
mounted() {
// 记录一下bg-layer的最大滚动位置
this.imageHeight = this.$refs.bgImage.clientHeight
// 最小可以滚动到的位置,这是一个负值,并为顶部留出了一个text的空间
this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT
// list是一个component对象,要通过$el取得它的DOM,将scoll的高度设置为背景图的高度
this.$refs.list.$el.style.top = `${this.imageHeight}px`
}
然后在watch中观测到scrollY值变化时就就设置layer层跟随滚动到一定位置
watch: {
scrollY(newY) {
let translateY = Math.max(this.minTranslateY, newY) // 当newY大于最小滚动值的时候就把高度设为最小滚动值,因为他们都是负值,所以取最大的值,其绝对值最小
this.$refs.layer.style['transform'] = `translate3D(0, ${translateY}px, 0)`
this.$refs.layer.style['webkitTransform'] = `translate3D(0, ${translateY}px, 0)`
}
}
4)我们希望文字在达到预留窗口时隐藏,也就是歌单的名字不会盖住歌手的名字和返回按钮
原来图片的布局也是relative布局的,我们可以为其设置一个relative:10,但是又会出现一个问题,就是图片部分完全盖住了歌单列表,也就是bg-layer滚动到最大距离并且还没有盖住歌手名字之前,我们改变其z-index和高度,没有滚动到最大高度时和之前一样就行
原来的图片布局是靠padding-top来支撑的
.bg-image
position: relative
width: 100%
height: 0
padding-top: 70%
所以在watchscrollY的时候,我们对图片的高度和index做一个限定
let zIndex = 0
if (newY < this.minTransalteY) {
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 // if中为10,else中就不变还是为0
5)拉动歌曲列表的时候,实现图片的一个放大缩小(scale)的效果和模糊度的效果(blur)
首先watch scrollY的时候,先定义一个scale为1
let scale = 1
let zIndex = 0
let blur = 0
const percent = Math.abs(newVal / this.imageHeight)
if (newVal > 0) { // 向下拉的时候
scale = 1 + percent // 相当于加了一个nweY的高度
// 向下拉的时候图片被盖住一部分,设置其index大于歌名列表
zIndex = 10
} else {
blur = Math.min(20, percent * 20)
}
this.$refs.bgImage.style[transform] = `scale(${scale})`
this.$refs.filter.style[backdrop] = `blur(${blur}px)`
向下拉的时候,我们希望有一个高斯模糊的效果,往上滑的时候越高它的模糊度越大,定义一个blur,如上所示
6)对transition等css属性进行封装,在dom.js中进行扩展,以兼容浏览器
// 创建了一个div的style
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 // 所有的类型都不支持的返回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中引入:
import {prefixStyle} from 'common/js/
const transform = prefixStyle('transform')
const backdrop = prefixStyle('backdrop-filter')
这样,就可以替换css style中的一下兼容性代码了(webkit-transform),他可以自动根据我们浏览器的兼容情况自动添加前缀
8)制作返回按钮
back() {
this.$router.back()
}
,9)添加随机播放的按钮,在bg-image层中添加,添加逻辑v-show,保证歌曲列表渲染完成才会出现随机播放按钮
随机播放全部
按钮的出现应该在数据渲染完成之后,这里我们为按钮添加一个v-show的条件,如上所示 v-show="songs.length"
play-wrapper是一个绝对定位,当我们滚动列表的时候,按钮也会滚动到页面的顶部,滚动到顶部的时候,我们实际上是修改了bg-image的高度,
if (newVal < this.minTransalteY) { // 滚动到顶部
zIndex = 10
this.$refs.bgImage.style.paddingTop = 0 // 这两句直接将高度写死就成
this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`
this.$refs.playBtn.style.display = 'none' // 按钮消失
} else {
this.$refs.bgImage.style.paddingTop = '70%'
this.$refs.bgImage.style.height = 0
this.$refs.playBtn.style.display = ''
}
10)在scroll的最后添加loading组件,不要忘了import和components
父子组件的过程:
singer.vue(歌手列表,含有list-view组件,并添加view-router可以挂在不同歌手详情界面的路由)-->singer-detail.vue(点开某一个歌手之后的详情页,由一个transitin动画包裹,主要含有一个music-list.vue组件,抽象成组件是为了提高复用性)-->music-list.vue(含有一个返回符号,一张歌手背景图和一个song-list.vue组件)-->song-list.vue(某一位歌手下的所有歌曲列表)