推荐页面开发
源码
better-scroll插件
之后的轮播图组件和列表滚动组件都会用到这个插件。这个插件还挺牛逼的,9400+stars。
QQ音乐的api
jsonp用到的:
https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg
axios/Ajax用到的:
在webpack.dev.conf.js中配置,见下文
【重点难点一】recommend页面jsonp相关文件的编写
src/common/js/jsonp.js promise封装
本项目该部分使用jsonp技术获取数据,解决跨域问题。
这里使用一个封装好的jsonp插件。这是它的API :
利用这个插件里面提供的api进行Promise的改写—-> jsonp.js的创建。
先npm i jsonp
进行jsonp插件的安装。
之后会经常用到它,把他放在js文件夹下,然后编写src/common/js/jsonp.js:
1 | import originJsonp from 'jsonp' |
我们把请求api数据相关的js放在src/api
目录下
src/api/config.js
这部分主要就是模拟qq音乐api的参数
1 | export const commonParams = { |
src/api/recommend.js:
1 | import jsonp from 'common/js/jsonp' |
webpack.base.conf.js
添加多两个别名:
1 | alias: { |
recommend.vue
1 | <template> |
看一下这时候有没有抓取到数据:
轮播图组件
安装依赖
这里用到一个插件better-scroll
插播一个小技巧:抽出一个“添加类名”组件dom.js
在轮播图组件的编写过程中,编写 _setSliderWidth() 方法的代码中,要遍历某个节点的子节点,给它加上一个类名,这是一个常见的操作。比起直接在 _setSliderWidth() 里面实现,最好的方法是把该操作抽离成为一个js文件。
于是新建了src/common/js/dom.js,在里面实现了添加类名的操作并暴露出去:
1 | /** |
而这个dom.js以后就专门来存放常见的dom操作。
踩坑记录
recommend.vue中引用slider组件,而slider.vue中的
_setSliderWidth()
方法里面循环渲染dom节点。但是我一开始的recommend.vue的代码中没有实现判断是否成功获取到了数据并渲染了节点,因为获取的是真实数据存在异步过程,如果没有加上这句v-if="recommends.length"
的话,可能存在recommend.vue页面还没有成功获取数据渲染节点,而引用slider组件导致slider中_setSliderWidth()
方法里面循环渲染dom节点出错,最终导致页面报销的状况。1
2
3
4
5
6
7
8
9
10
11
12
13<div class="recommend" ref="recommend">
<div class="recommend-content">
<div v-if="recommends.length" class="slider-wrapper"><!-- 注意这里用了v-if来确保只有当成功获取到数据后才将slider组件添加上,从而保证slider中的循环渲染成功 -->
<slider>
<div v-for="item in recommends" :key="item.id">
<a :href="item.linkUrl">
<img :src="item.picUrl" alt="">
</a>
</div>
</slider>
</div>
</div>
</div>better-scroll默认会复制轮播图的首尾两张图(这里说成“图”不太准确,姑且这么说),因此通过api获得的轮播图图片只有5张,但是在使用better-scroll的时候生成了7个节点。相应的在slider.vue的
_setSliderWidth
中要设置增加两张图的宽度:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17_setSliderWidth(isResize) {
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];
addClass(child, "slider-item");
child.style.width = sliderWidth + "px";
width += sliderWidth;
}
if (this.loop && !isResize) {
width += 2 * sliderWidth; /* 加多左右两个轮播图的宽度,为了实现无缝流畅滚动,不会噌的一下跳过去 */
}
this.$refs.sliderGroup.style.width = width + "px";
}有个bug
无法实现自动的无缝轮播,轮播到最后一张的时候就不动了,必须手动划一下才能无缝轮播。暂未解决,怀疑是better-scroll的问题。目前的妥协是最后一张直接跳转到第一张,就很难看。。。
实现窗口改变的时候自适应
在mounted钩子函数中监听一下窗口的改变事件
1
2
3
4
5
6
7
8/* 监听窗口改变事件 */
window.addEventListener('resize', () => {
if (!this.slider) {/* 如果slider还未初始化,直接退出 */
return
}
this._setSliderWidth()
this.slider.refresh()
})优化:减少重复的数据请求
打开项目的时候轮播图会发起数据请求,但是当从首页推荐页切换路由到其他页面,再从其他页面切换回轮播图所在的首页推荐页面时,它又重新刷新发起了数据请求,这是没必要的,而且降低了性能。
可以使用
<keepalive>
标签将<router-view>
包裹起来,将其dom缓存到内存中。这就避免了上述的情况。优化:beforeDestroy清除计时器释放内存
路由切换的时候会触发vue的destroy,那么最好时把slider的计算器清除了以释放内存
1
2
3beforeDestroy() {
clearTimeout(this.timer)
},
【重点难点二】axios与后端接口代理
先安装axios依赖
修改代理
在使用QQ音乐推荐页歌单列表的api时,发现无法通过ajax获取到。关键点是QQ音乐设置了请求头中的Host和Referer进行了限制:
因此需要解决这个跨域的问题。方法是修改代理。
修改webpack.dev.conf.js
1 | devServer: { |
src/api/recommend.js
1 | import jsonp from 'common/js/jsonp' |
然后在recommend.vue中调用getDiscList()方法就能成功获取到数据了。
这时候启动项目,可以在chrome Network中看到发送了Ajax请求并成功获得了数据:
歌单列表组件开发
通过上一步改变qq音乐api的接口代理,在recommend.vue调用getDiscList方法获取到数据,并渲染到页面上。
但是到这一步为止,页面尚无法上下滑动。还是需要better-scroll组件来创建一个歌单列表滚动组件。
src/base/scroll/scroll.vue
创建/src/base/scroll/scroll.vue:
1 | <template> |
src/components/recommend/recommend.vue
1 | <template> |
这里遇到的坑
在上述recommend.vue中提到scroll标签中:data如果没有加的话依旧是不能上下滑动的,因为scroll是在mounted时候初始化,由于数据获取存在异步性,可能mounted的时候尚未取到数据,整个列表dom的高度是没有被撑开的。所以需要传入一个discList数据(discList有说明数据已经获取到了)触发scroll的refresh
那为什么传discList就行?传recommends行不行?答案是不行的。虽然他们都是异步获取数据,但是discList获取的速度比recommends的获取速度慢。如果传入的是recommends,虽然recommends已经获取到了,轮播图的高度也已经撑开了,但是下方的列表discList还没有完全获取到,所以scroll在初始化的时候页面的高度计算是有误的。
还有一个问题,拿到recommends就万事大吉了吗?不是。
对于discList歌单列表,里面的列表项是给定宽高的,所以一拿到discList就能确定宽高撑开dom。但是recommend部分的轮播图图片是没有给定宽高的,是按照百分比来设置的宽高,所以即使获得了recommends的数据,但是由于图片是通过链接引入的,这又是一个异步的过程,所以不能保证拿到了recommends的数据就能立刻加载出图片撑起宽高,在此完成之前就初始化scroll还是会出问题的。所以,可以给img标签监听load事件,一旦load就refresh scroll。
1
2
3
4
5
6loadImage() {
if (!this.checkloaded) { /* 无中生有搞个checkloaded变量来监测加载完成与否,一开始默认为undefined,只要有一张加载成功就改值为true,不用每张图都触发这个方法,加载得到一张图就可以确定宽高了 */
this.checkloaded = true
this.$refs.scroll.refresh()
}
},
使用checkloaded也是常见的一种优化。
这里总结一下better-scroll的渲染原理吧
它根据初始化的时机或者refresh的时机,那个时候scroll的父元素的高度和子元素的高度之差,算到scroll可以滚动的位置,所以我们去实例化或者refresh这个scroll的时候一定要保证dom是渲染完成的,才能正确计算他们的高度。一旦我们有数据变化引起dom变化或者其他dom结构变化,一定要重新refresh这个scroll。
懒加载vue-lazyload
vue-lazyload
代码编写
main.js
1
2
3
4
5import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyLoad, {
loading: require('./common/image/default-ldt.png') //loading时显示的图片
})将recommend.vue中
img
标签的:src
属性替换成v-lazy
计即可
loading组件
效果:
要搞个这个东西,提升用户体验。
src/base/loading.vue
1 | <template> |
然后在recommend.vue中引用并嵌入即可。