Fork me on GitHub
余鸢

Vue音乐播放器开发(四):歌手详情页开发以及Vuex的使用一

歌手详情页开发

singer-detail.vue子组件开发及配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="singer-detail"></div>
</template>
<script type="text/ecmascript-6">
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.slide-enter-active, .slide-leave-active
transition: all 0.3s
.slide-enter, .slide-leave-to
transform: translate3d(100%, 0, 0)
</style>

需要在一级路由中嵌套二级路由,修改index.js

1
2
3
4
5
6
7
8
9
10
{
path: '/singer',
component: Singer,
children: [
{
path: ':id',
component: SingerDetail
}
]
},

使用<router-view></router-view>在singer.vue组件里挂载这个子路由
v24

当点击列表元素时页面跳转至子路由,由于这个列表是基于listview实现的,所以在listview组件里添加selectItem(item)点击事件。listview是个基础组件,它的点击事件不会有任何逻辑相关的,做的事情仅仅是把这个事件触发出去,告诉外部我被点击了以及点击我的元素是什么,只有点击这个事件的人才会决定要做什么事。

v25

$emit:子组件改变父组件的值,通过on将父组件的事件绑定到子组件,在子组件中通过emit来触发​$on绑定的父组件事件

实际情况下,有很多按钮在执行跳转之前,还会执行一系列方法,这时可使用this.$router.push(location) 来修改 url,完成跳转。接着在singer组件里监听这个事件,item在这其实是singer的实例

v26

使用<transition></transition>给子路由singer-detail组件加个动画看上去更美观

singer-detail.vue

1
2
3
4
5
<template>
<transition name="slide">
<music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list>
</transition>
</template>

Vuex

初识vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

当你打算开发大型单页应用(SPA),会出现多个视图组件依赖同一个状态,来自不同视图的行为需要变更同一个状态。遇到以上情况时候,你就应该考虑使用Vuex了,它能把组件的共享状态抽取出来,当做一个全局单例模式进行管理。这样不管你在何处改变状态,都会通知使用该状态的组件做出相应修改。

简单来说,vuex 就是使用一个 store 对象来包含所有的应用层级状态,也就是数据的来源。当然如果应用比较庞大,我们可以将 store 模块化,也就是每个模块都有自己的 store。

store 有四个属性

store 有四个属性:state, getters, mutations, actions

  • state:简单说就是变量,也就是所谓的状态。

  • getters:存放一些公共函数供组件调用。

  • mutations:我们要改变 state 的一些方法,有点像是事件注册。

    一条重要的原则就是要记住 mutation 必须是同步函数。

  • actions:管理触发条件。mutation 像事件注册,需要相应的触发条件。而 Action 就那个管理触发条件的。

    Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。

当 mutation 事件类型比较多的时候,我们可以使用常量替代 mutation 事件类型。同时把这些常量放在单独的文件(mutation-types.js)中可以让我们的代码合作者对整个 app 包含的 mutation 一目了然。

辅助函数

Vuex提供一些辅助函数帮助我们使用这些属性:

  • mapState:当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。

  • mapGetters:仅仅是将 store 中的 getter 映射到局部计算属性。

  • mapMutations:将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
  • mapActions:将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)。

引入Vuex

1、安装 vuex

1
npm install vuex --save

2、新建一个store文件夹(这个不是必须的),并在文件夹下新建index.js文件,文件中引入vue和vuex。目录结构如下:

v28

index.js入口文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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({
actions,
getters,
state,
mutations,
strict: debug,
plugins: debug ? [createLogger()] : []
})

3、在main.js 中引入新建的vuex文件

1
2
3
4
5
6
7
8
import store from './store'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

歌手数据的配置

回归项目,在state定义singer变量,继而做一系列数据处理。

v27

当singer组件跳转时改变singer数据,通过mapMutations做对象映射,把mutation映射成方法名,把setSinger当做vue数据。

singer.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
import {mapMutations} from 'vuex'
export default{
selectSinger (singer) {
this.$router.push({
path: `/singer/${singer.id}`
})
this.setSinger(singer)
},
...mapMutations({
setSinger: 'SET_SINGER'
})
}

在singer-detail组件获取singer数据,通过mapGetters扩展到computed计算属性里,做完这层映射就可以在vue实例中挂载一个singer的属性,然后就可以拿到singer。

singer-detail.vue

1
2
3
4
5
6
7
import {mapGetters} from 'vuex'
computed: {
...mapGetters([
'singer'
])
},

console.log(this.singer)观察一下组件跳转的数据传递

v29

由此,通过vuex解决路由之间数据参数传递的问题。

歌手详情数据抓取

根据传入的singer抓取到歌手详情

singer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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: 1,
platform: 'h5page',
order: 'listen',
begin: 0,
num: 10,
songstatus: 1,
g_tk: 5381,
singermid: singerId
})
return jsonp(url, data, options)
}

singer-detail.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default{
created () {
this._getDetail()
},
methods: {
_getDetail () {
getSingerDetail(this.singer.id).then((res) => {
if (res.code === ERR_OK) {
console.log(res.data.list)
}
})
}
}
}

获取到数据:

v30

这里发现个问题,在当前页面刷新获取不到数据,因为刷新是直接进入到子路由,之前并不知道歌手的相关信息是个空对象。在这里加个判断,也就是说如果在子路由刷新了,就让它会退到上级。

v31

歌手详情数据处理

除了歌手详情数据之外,还有歌单详情数据以及排行榜数据都包含歌曲数据,既然每个列表都有歌曲数据,那么就对歌曲数据进行抽象。

common/js/song.js

1
2
3
4
5
6
7
8
9
10
11
export default class Song {
constructor ({id, mid, singer, name, album, duration, image, url}) {
this.id = id
this.mid = mid
this.singer = singer
this.ablum = album
this.duration = duration
this.image = image
this.url = url
}
}

现在每首歌曲都是musicdata里的数据,需要把这些数据提取成我们需要的部分构造成我们所需要的数据对象。

1
2
3
4
5
6
7
8
9
10
11
12
export function createSong (musicData) {
return new Song({
id: musicData.songid,
mid: musicData.songmid,
singer: filerSinger(musicData.singer),
name: musicData.songname,
album: musicData.albumname,
duration: musicData.interval,
image: `https://y.gtimg.cn/music/photo_new/T002R150x150M000${musicData.albummid}.jpg?max_age=2592000`,
url: `http://ws.stream.qqmusic.qq.com/${musicData.songid}.m4a?fromtag=46`
})
}

singer是个数组,但我们需要的是字符串,数据可以直接运用在dom上,不需要额外再做处理。对singer进行处理。

1
2
3
4
5
6
7
8
9
10
export function filerSinger (singer) {
let ret = []
if (!singer) {
return ''
}
singer.forEach((s) => {
ret.push(s.name)
})
return ret.join('/')
}

对歌手详情数据做最后的处理

v32

music-list组件开发

歌手详情页面、歌单详情页面、排行榜详情页面都是类似的,所以可以把这些类似的页面抽象成一个通用的业务组件music-list.vue。它们不同的是数据差异,这些差异可以通过props数据传入。

music-list.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div class="music-list">
<div class="back">
<i class="icon-back"></i>
</div>
<h1 class="title"></h1>
<div class="bg-image">
<div class="filter"></div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
export default{
props: {
bgImage: {
type: String,
default: ''
},
songs: {
type: Array,
default: []
},
title: {
type: String,
default: ''
}
}
}
</script>

music-list组件注册到singer-detail组件里,同时需要在music-list组件传入songs、title、bgImage三个参数,其中title和bgImage通过计算属性得到。

v33

传入title和bgImage的数据

效果图:

v34

使用scroll组件做歌曲滚动列表

和music-list组件同理,把歌曲页面抽象成一个通用的基础组件song-list.vue

song-list组件需要接收个songs数据,然后遍历songs。

song-list.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class="song-list">
<ul>
<li v-for="song in songs" class="item">
<div class="content">
<h2 class="name">{{song.name}}</h2>
<p class="desc">{{getDesc(song)}}</p>
</div>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default{
props: {
songs: {
type: Array,
default: []
}
},
methods: {
getDesc (song) {
return `${song.singer} 。 ${song.album}`
}
}
}
</script>

展示歌曲数据

music-list组件里运用song-list,首先使用scroll组件包裹歌曲列表,为了控制它的样式包裹一个classsong-list-wrapperdiv,注册song-list组件,传入songs数据,为了正确计算它的高度,在scroll组件传入songs作为它的数据。效果如下:

v35

滚动列表发现scroll的top值不对,根据不同浏览器大小背景图所占高度不一样,我们可以通过计算得到它。在mounted钩子里拿到scroll控制它的top值。

1
2
3
mounted () {
this.$refs.list.$el.style.top = `${this.$refs.bgImage.clientHeight}`
},

scroll组件的top就等于bgImage的高度。

通过css设置背景图的宽高比为70%占位,这样就得到了bgImage的高度。在图片加载之前就已经知道它的高度了,同样就可以设置scroll组件的top值。

v36