Fork me on GitHub
余鸢

Vue音乐播放器开发(二):轮播图和滚动的实现

推荐页面开发

推荐页面分为两个页面:首页和歌单详情页面

首页

首页分为:轮播图、歌单列表

轮播图

参考QQ音乐抓取轮播图数据,以jsonp的形式返回数据。首先,对jsonp进行封装。

了解jsonp:https://github.com/webmodules/jsonp

jsonp.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import originJsonp from 'jsonp'
export default function jsonp (url, data, option) {
url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)
return new Promise((resolve, reject) => {
originJsonp(url, option, (err, data) => {
if (!err) {
resolve(data)
} else {
reject(err)
}
})
})
}
function param (data) {
let url = ''
for (var k in data) {
let value = data[k] !== undefined ? data[k] : ''
url += '&' + k + '=' + encodeURIComponent(value)
}
return url ? url.substring(1) : ''
}
编写轮播图组件

slider.vue

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="slider">
<div class="slider-group">
<slot></slot>
</div>
<div class="dots"></div>
</div>
</template>
<script type="text/ecmascript-6">
</script>
抓取轮播图数据

recommend.js

1
2
3
4
5
6
7
8
9
10
11
12
import jsonp from 'common/js/jsonp'
import {commonParams, options} from './config'
export function getRecommend () {
const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
const data = Object.assign({}, commonParams, {
platform: 'h5',
uin: 0,
needNewCode: 1
})
return jsonp(url, data, options)
}

recommend.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="recommend">
<div class="recommend-content">
<div class="slider-wrapper">
<slider>
<div v-for="item in recommends">
<a :href="item.linkUrl">
<img :src="item.picUrl">
</a>
</div>
</slider>
</div>
<div class="recommend-list">
<h1 class="list-title">热门歌单推荐</h1>
<ul>
</ul>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import Slider from 'base/slider/slider'
import {getRecommend} from 'api/recommend'
import {ERR_OK} from 'api/config'
export default {
data () {
return {
recommends: []
}
},
created () {
this._getRecommend()
},
methods: {
_getRecommend () {
getRecommend().then((res) => {
if (res.code === ERR_OK) {
this.recommends = res.data.slider
}
})
}
},
components: {
Slider
}
}
</script>

效果如图:

v7

如上图很不符合我们的要求,我在这里使用第三方插件better-scroll来实现轮播图滚动功能。

better-scroll

better-scroll 是什么

better-scroll 是一款重点解决移动端(未来可能会考虑 PC 端)各种滚动场景需求的插件。它是基于原生 JS 实现的,不依赖任何框架。

better-scroll滚动原理:浏览器的滚动条大家都会遇到,当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也就是当我们的视口展示不下内容的时候,会通过滚动条的方式让用户滚动屏幕看到剩余的内容。

安装:

1
$npm install better-scrll

了解better-scroll具体参考:http://npm.taobao.org/package/better-scroll

设置slider的属性

slider.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
props: {
loop: {
type: Boolean,
default: true
},
autoPlay: {
type: Boolean,
default: true
},
interval: {
type: Number,
default: 4000
}
}

有时候会出现初始化后不能滚动或者报错,是因为在初始化时没有渲染或者宽度或高度计算出错,为了保证渲染正确使用mounted钩子做一些初始化操作_setSliderWidth()_initSlider()方法

mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。

slider.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
31
32
33
34
35
36
37
moundted () {
this._setSliderWidth()
this._initSlider()
},
methods: {
_setSliderWidth () {
this.children = this.$refs.sliderGroup.children
let width = 0
let sliderWidth = this.$refs.slider.clientWidth
for (let i = 0; i < this.children.length; i++) {
let child = this.children[i]
// 给每个元素添加slider-item名称
addClass(child, 'slider-item')
child.style.width = sliderWidth + 'px'
width += sliderWidth
}
if (this.loop) {
width += 2 * sliderWidth
}
this.$refs.sliderGroup.style.width = width + 'px'
},
_initSlider () {
this.slider = new BScroll(this.$refs.slider, {
scrollX: true,
scrollY: false,
momentum: false,
snap: true,
snapLoop: this.loop,
snapThreshold: 0.3,
snapSpeed: 400,
click: true
})
}
}

这儿没有渲染成功,可以思考个问题:我们在mounted 的时候<slot></slot>里有没有东西?

在渲染recommends的时候,初始化_getRecommend()getRecommend()是个异步过程,它可能会有几秒的延迟,所以当recommends还没有获取到的时候也就是<slot></slot>里没有东西,mounted钩子已经执行了,为了确保<slot></slot>里有东西再执行mounted,对recommends进行判断。

v8

dots滚动实现

mounted初始化dots,在data里定义dots实现页面渲染。

v9

定义currentPageIndex,把滚动到当前页的dotcurrentPageIndex绑定起来:class="{active: currentPageIndex === index}",better-scroll在滚动的时候实现一个事件,所以在初始化slider的时候绑定一个scrollEnd事件,来实现dotcurrentPageIndex的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="dots">
<span class="dot" :class="{active: currentPageIndex === index}" v-for="(item, index) in dots"></span>
</div>
_initSlider () {
this.slider = new BScroll(this.$refs.slider, {
scrollX: true,
scrollY: false,
momentum: false,
snap: true,
snapLoop: this.loop,
snapThreshold: 0.3,
snapSpeed: 400
})
this.slider.on('scrollEnd', () => {
let pageIndex = this.slider.getCurrentPage().pageX
if (this.loop) {
pageIndex -= 1
}
this.currentPageIndex = pageIndex
})
}
实现循环播放
1
2
3
4
5
6
7
8
9
_play () {
let pageIndex = this.currentPageIndex + 1
if (this.loop) {
pageIndex += 1
}
this.timer = setTimeout(() => {
this.slider.goToPage(pageIndex, 0, 400)
}, this.interval)
}

同时初始化slider的时候要判断是否是自动播放,做些逻辑处理

v11

最终效果图:

v12

我们改变窗口大小时会发生图片错乱的错误,所以我们在mounted的时候去监听一个窗口改变的事件适应窗口宽度大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mounted () {
setTimeout(() => {
this._setSliderWidth()
this._initDots()
this._initSlider()
if (this.autoPlay) {
this._play()
}
}, 20)
window.addEventListener('resize', () => {
if (!this.slider) {
return
}
this._setSliderWidth(true)
this.slider.refresh()
})
},

还有个需要注意的一点,我们在切换页面的时候会重新发数据请求,所有元素也会重新渲染,大大增加了性能消耗。我们这里需要缓存整个站点的所有页面,而页面一般一进去都要触发请求的。vue2.0提供了一个keep-alive组件用来缓存组件,避免多次加载相应的组件,减少性能消耗。

v13

当我们的组件有类似于计时器这种资源的时候,我们在销毁这些资源的时候一定要做清理工作,有利于内存释放。

1
2
3
destroyed () {
clearTimeout(this.timer)
}

歌单详情页面

获取歌单数据
后端接口代理

有时候我们使用jsonp获取资源被拒绝,有host和Referer限制,前端不能直接去修改host,就需要通过后端代理的方式解决问题

后端代理时用到axios,详情见https://github.com/axios/axios

1
$ npm install axios

dev-server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var apiRoutes = express.Router()
apiRoutes.get('/getDiscList', function (req, res) {
var url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'
axios.get(url, {
headers: {
referer: 'https://y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query
}).then((response) => {
res.json(response.data)
}).catch((e) =>{
console.log(e)
})
})
app.use('/api', apiRoutes)

recommends.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function getDiscList () {
const url = '/api/getDiscList'
const data = Object.assign({}, commonParams, {
platform: 'yqq',
needNewCode: 0,
categoryId: 10000000,
sortId: 5,
sin: 0,
ein: 29,
hostUin: 0,
rnd: Math.random(),
format: 'json'
})
return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
}
获取歌单列表及页面展示
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
31
32
33
34
<template>
<div class="recommend">
<div class="recommend-content"...>
<div>
<div v-if="recommends.length" class="slider-wrapper"...>
</div>
<div class="recommend-list">
<h1 class="list-title">热门歌单推荐</h1>
<ul>
<li class="item" v-for="item in discList">
<div class="icon">
<img width="60" height="60" :src="item.imgurl">
</div>
<div class="text">
<h2 class="name" v-html="item.creator.name"></h2>
<p class="desc" v-html="item.dissname"></p>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
_getDiscList () {
getDiscList().then((res) => {
if (res.code === ERR_OK) {
this.discList = res.data.list
}
})
}
</script>

效果如下:

v14

封装scroll滚动组件
1
2
3
4
5
6
7
8
9
10
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
<script type="text/ecmascript-6">
import BScroll from 'better-scroll'
....
</script>

引入到recommend.vue使用组件。

1
2
3
4
components: {
Slider,
Scroll
}

优化

在scroll组件里传:data="discList",为什么不是:data="recommends"或者diacList和recommends一起传给它?我们知道它们都是异步获取的,原因是轮播图数据获取接口要优先于歌单列表数据接口,当歌单列表获取到数据渲染出来再调scroll.refresh(),轮播图数据才被渲染出来,这时候高度已经被撑开,也就是说better-scroll能正确计算出高度。

当recommends获取时这个高度不一定会有,因为轮播图的高度是由图片的高度撑开的,也就是说当recommends获取到时item.imgurl会去请求图片,这是异步过程,我们事先不知道高度,完全是由图片的高度决定的,这里不能用计算属性,使用@load触发一个事件,一旦有一个图片触发load,我们就调用loadImage()方法。

1
2
3
4
5
6
loadImage () {
if (!this.checkloaded) {
this.checkloaded = true
this.$refs.scroll.refresh()
}
}

懒加载

歌单是由很多图片组成的,当我们刷新页面时会发出很多请求,其实刷新只需要展示首屏的图片,而其他图片是当滚动到它们时再加载,使用图片懒加载技术,这里使用vue-lazyload第三方懒加载插件。

1
$npm install vue-lazyload

使用方法:在main.js引入

1
2
3
4
5
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
loading: require('common/image/default.png')
})

:src="item.imgurl"改成v-lazy="item.imgurl",如图:

v15

better-scroll和fastclick的冲突

我们发现点击slider的时候点不动,是因为better-scroll和fastclick是有冲突的。scroll组件初始化时click默认为true,歌单列表是需要被点击的,所以click设置为true。解决这个冲突问题:
点击slider是点击一张图片,所以可以给图片添加一个属性class="needsclick"。fastclick监听到img的点击事件发现class是needsclick的话就不会去拦截,这样就不会阻止这个点击。

v16

基础组件loading组件的开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="loading">
<img src="./loading.gif" width="24" height="24">
<p class="desc">{{title}}</p>
</div>
</template>
<script type="text/ecmascript-6">
export default{
props: {
title: {
type: String,
default: '加载中...'
}
}
}
</script>

引入recommend.vue组件里

1
2
3
<div class="loading-container" v-show="!discList.length">
<loading></loading>
</div>

效果:

v17

具体源代码

参考:https://github.com/kakajing/vue-music