第4章 推荐页面开发
包括 jsonp 原理介绍和 Promise 封装、轮播图组件开发、歌单接口数据分析和抓取、axios 介绍和后端接口代理、歌单列表组件开发和数据应用、scroll 组件的抽象和应用、vue-lazyloader 懒加载插件的介绍和应用、loading 基础组件开发和应用。
4-1 页面简介+轮播图数据分析
4-2 jsonp原理介绍+Promise封装
jsonp原理介绍
动态生成一个JavaScript标签,其src由接口url、请求参数、callback函数名拼接而成;利用js标签没有跨域限制的特性实现跨域请求
api/config.js
配置与接口统一的参数
为了和QQ音乐接口一致,配置一些公用的参数、options和err_num码
//为了和QQ音乐接口一致,配置一些公用的参数、options和err_num码
export const options={
param: 'jsonpCallback'
}
export const commonParams={
g_tk: 5381,
uin: 0,
inCharset:' utf-8',
outCharset:' utf-8',
notice: 0,
platform:'h5',
needNewCode: 1,
format: 'jsonp'
}
export const ERR_OK =0
common/js/jsonp.js
Promise封装
- 引入jsonp库
- 封装一个jsonp的函数,传三个参数,url,data,和callback
- 对传递进来的data和url进行拼接
- 用jsonp请求,返回promise实例
//引入jsonp https://github.com/webmodules/jsonp
import originjsonp from 'jsonp'
//返回一个函数。传入url。data 和callback(放在option里面的callback)
export default function jsonp(url,data,option){
//对url和data进行拼接,拼接成一个新的url
//判断原来的url中是否包含有?号 ,有问号加上 & 没有 则加上? 并且拼接上data参数部分
url+=(url.indexOf('?')>-1 ? '&' : '?')+param(data);
//返回一个promise的实例。可以直接用这个实例的then方法来获取到resolve和reject状态的回调
return new promise(function(resolve,reject){
//第一个参数是url,因为jsop是get请求,在此之前腰将基本的url和data进行拼接组成新的url
//第二个参数,option是配置好的callback的参数
//第三个参数,是回调函数
originjsonp(url, option,(err, data)=>{
if(!err){
resolve(data);
}else{
reject(data);
}
})
})
//对传进来的data做拼接处理
function param(data){
let str='';
// data.keys().forEach(key => {
// str+=`${key}=${data[key]}&`
// });
// str.substring(0,str.length-1)//从第一位开始截取到最后一位
for(var key in data){
let value=data[key] !== undefined ? data[key] : ''
str+=`&${key}=${encodeURIComponent(value)}`
}
return str ? str.substring(1) :''
}
4-3 jsonp的应用+轮播图数据抓取
在api/recommend.js中封装获取推荐页的数据
//在这份js里面写请求recommend的异步请求方法
//引入需要的config配置的参数
import {commonParams,options} from './config'
import jsonp from 'common/js/jsonp.js'
//获取到推荐的信息谁
export function getRecommend(){
const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
const data=Object.assign({},commonParams,{
platfrom: 'h5',
notice: 0,
uin: 0,
needNewCode: 1
})
return jsonp(url,data,options)
}
在recommend.vue中调用并获取数据
//引入异步请求函数
import {getRecommend} from 'api/recommend.js'
//引入常用的配置参数的ERR_OK
import {ERR_OK} from 'api/config.js'
export default {
created(){
//用——开头的方法名
this._getRecommend();
},
methods: {
//一般在methods里面写方法的具体实现,不要在created或者mounted等生命周期中写,方便管理
_getRecommend(){
getRecommend().then(res=>{
if(res.code==ERR_OK){
console.log(res.data.slider);
}
})
}
},
}
4-4 轮播图组件实现(上)
开发一个slide组件,具体的实现在slide组件上实现,父组件只需要传递数据就可以
轮播库
cnpm install better-scroll@next -S
4-5 轮播图组件实现(中)
在slider组件上用slot来接收父组件传递过来的数据
在props定义可能会发生改变的配置,比如是否循环,是否自动播放,切换的时长
我们在slider组件下位每个传递进来的元素 加上类 addClass(item,‘slide-item’) 来控制样式
addClass 我们定义在common/js/dom.js中
common/js/dom.js
//判断元素中是否有className
export function hasClass(el,className){
let reg=new RegExp('(^|\\s)' + className + '(\\s|$)');
return reg.test(el.className);
}
//如果没有类添加类
export function addClass(el,className){
if(hasClass(el,className)){
return
}
//将el的className拆开
let newClass= el.className.split(' ');
//去除空格之后将新的类参加的newClass的数组中
newClass.push(className);
//给el加上类
el.className=newClass.join(' ');
}
src\components\base\slider\slider.vue
实现过程
-
计算slider的宽度和slider-content 和slider-content里面的列表的宽度
props接收可能要修改的参数,loop为循环列表,当loop为true的时候,会在轮播的列表中copy前后两个li做无缝处理,所以我们在计算slider-content的宽度时,要判断当前是否是无缝滚动 -
当窗口resize时,要重新计算宽度,同时refresh一下,refresh 重新计算 BetterScroll,当 DOM 结构发生变化的时候务必要调用确保滚动的效果正常。
-
当用手指滑动送开时,将会派发一个scrollEnd事件。在这里我们计算当前的currentPageIndex.,让焦点正确active
let pageIndex = this.slider.getCurrentPage().pageX
this.currentPageIndex = pageIndex
- 当自动播放的时候,我们先清除定时器,再启动。让页面自动播放到下一页
this.slider.next()
- 当点击焦点的时候,务必要设置滚动配置的click为true,不然点击不起效果
this.slider.goToPage(index,0)
整体代码
<template>
<div class="slider" ref="slider">
<div class="slider-content" ref="slidergroup">
<slot></slot>
</div>
<div class="dots">
<span class="dot" :class="{active:index===currentPageIndex}" v-for="(item,index) in dots" :key="index" @click="_gotopage(index);"></span>
</div>
</div>
</template>
<script>
import {addClass} from 'common/js/dom'
import BTscroll from 'better-scroll'
export default {
props:{
autoplay:{//是否自动播放
type:Boolean,
default:true
},
loop:{//是否无缝滚动
type:Boolean,
default:true
},
intervalTime:{
type:Number,
default:4000
}
},
data(){
return{
dots:0,
currentPageIndex:0
}
},
mounted() {
this._getSliderWidth();
this._initSlider();
this._windowresize();
},
methods: {
_initSlider(){
let me=this;
this.slider=new BTscroll(this.$refs.slider,{
scrollX: true,
scrollY: false,
slide: {
loop: me.loop,
threshold: 100//可滚动到下一个或上一个的阈值。
},
momentum: false,
bounce: false,
stopPropagation: true,
click:true
})
//当滚动结束的时候,派发一个scrollEnd事件,我们获取到当前的index给currentpageindex
this.slider.on('scrollEnd', ()=>{
this._scrollEnd()
})
this._autoGoNext()
},
_getSliderWidth(isresize){
let sliderWidth=this.$refs.slider.clientWidth;
let children=[...this.$refs.slidergroup.children];
if(!isresize){
this.dots=children.length;
}
let width=0;
children.forEach((item,index)=>{
width+=sliderWidth;
item.style.width=sliderWidth+'px'
//给每个children加上类
addClass(item,'slider-item')
})
if(!isresize && this.loop){
//当是无缝滚动的时候加上两个
width+=2*sliderWidth
}
this.$refs.slidergroup.style.width=width+'px'
},
//重刷新
refresh(){
this.slider && this.slider.refresh()
},
//滑动结束的时候
_scrollEnd(){
//当滚动结束的时候,派发一个scrollEnd事件,我们获取到当前的index给currentpageindex
let pageIndex = this.slider.getCurrentPage().pageX
this.currentPageIndex = pageIndex
this._autoGoNext()
},
//自动播放
_autoGoNext(){
if(!this.autoplay){
return
}
clearTimeout(this.playTimer)
this.playTimer = setTimeout(() => {
this.slider.next()
}, this.intervalTime)
},
//去到下一页
_gotopage(index){
this.slider.goToPage(index,0)
this._autoGoNext()
},
//当窗口发生变化的时候
_windowresize(){
window.addEventListener('resize',()=>{
this._getSliderWidth(true);
this.refresh()
})
}
}
}
</script>
<style scoped>
.slider{ position: relative}
.slider-content:after{content:""; display:block; clear: both;}
.slider-item{float:left}
.slider-item a{width: 100%;}
.slider-item img{ width: 100%;}
.dots{position: absolute;
right: 0;
left: 0;
bottom: 12px;
-webkit-transform: translateZ(1px);
transform: translateZ(1px);
text-align: center;
font-size: 0;}
.dots .dot{
display: inline-block;
margin: 0 4px;
width: 8px;
height: 8px;
background: hsla(0,0%,100%,.5);
border-radius:50%;
}
.dots .dot.active{ width: 20px;background: #fff; border-radius: 20px;}
</style>
4-6 轮播图组件实现(下)
在recommend.vue中使用slider组件
<!--轮播部分-->
<slider v-if="recommend.length" >
<div v-for="(item,index) in recommend" :key="index" class="slide-item">
<a :href="item.linkUrl" target="_blank">
<img :src="item.picUrl">
</a>
</div>
</slider>
优化
App.vue 中优化:缓存DOM到内存中,不用重新发送请求,这样slider就不会有闪动的现象
<keep-alive>
<router-view></router-view>
</keep-alive>
slider中优化:当组件中有定时器,一定要记得在组件销毁时清理掉这些定时器,使用生命周期destroyed()
beforeDestroy() {
clearTimeout(this.playTimer)//消除定时器
this.slide.destroy()
},
4-7 歌单数据接口分析
问题: QQ音乐歌单数据的请求头中有域名Host、来源Referer,所以请求的接口应该是有加上该域名和来源,直接请求就会报HTTP-500错误。
原因: 前端不能直接修改request header,所以要通过后端代理的方式解决。
解决: 采用 axios 在node.js中发送http请求
4-8 axios 的使用和后端接口代理
在config/index.js中配置反向代理
//配置返向代理
proxyTable: {
'/api/getRecomonList': {// '/api':匹配项
target: 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg',// 接口域名
secure: false,// 如果是https接口,需要配置这个参数
changeOrigin: true,// 是否跨域
//请求前处理
bypass: function(req, res, proxyOptions) {
//对请求头处理
req.headers.referer='https://y.qq.com'
req.headers.host='c.y.qq.com'
},
pathRewrite: {// 重写地址
'^/api/getRecomonList': ''
}
}
},
axios的使用
//引入axios
import axios from 'axios'
export function getRecommendList(){
return new Promise(function(resolve,reject){
let url='/api/getRecomonList'
let data=Object.assign({}, commonParams, {
platform: 'yqq',
hostUin: 0,
sin: 0,
ein: 29,
sortId: 5,
needNewCode: 0,
categoryId: 10000000,
rnd: Math.random(),
format: 'json'
})
axios.get(url,{params:data}).then(res=>{
resolve(res.data)
}).catch(err=>{
reject(err);
})
})
}
注意这里的data部分 {params:data}
4-9 歌单列表组件开发和数据的应用
recommend.vue中:定义和调用获取数据的方法
created(){
this._getRecommendList()
},
_getRecommendList(){
let that=this;
getRecommendList().then(res=>{
if(res.code==ERR_OK){
that.recommendList=res.data.list
}
})
},
4-10 scroll 组件的抽象和应用(上)
better-scroll滚动布局:只会滚动父元素下的第一个子元素 —— 想要slider和recommend-list同时可以滚动,需要在外层再嵌套一个
在recommend中
<div class="recommend">
<scroll class="recommend-content" :data="recommendList" ref="scroll">
<div>
//轮播部分
//...
//列表部分
</div>
</scroll>
</div>
recommend-content的高度要固定,所以我们需要加上样式
.recommend
position: fixed
width: 100%
top: 88px
bottom: 0
.recommend-content
height: 100%
在scroll.vue中的布局
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
4-11 scroll 组件的抽象和应用(下)
scroll 组件
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
props: {
//probeType: 1 滚动的时候会派发scroll事件,会截流。2 滚动的时候实时派发scroll事件,不会截流 。3 除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件
probeType: {
type: Number,
default: 1
},
// click: true 是否派发click事件,通常判断浏览器派发的click还是betterscroll派发的click,可以用event._constructed,若是bs派发的则为true
click: {
type: Boolean,
default: true
},
data: {
type: Array,
default: null
}
},
mounted(){
setTimeout(() => { //确保DOM已经渲染
this. _initScroll()
}, 20)
},
methods:{
_initScroll(){
if(!this.$refs.wrapper){
return
}
this.scroll = new BScroll(this.$refs.wrapper, {
probeType : this.probeType,
click: this.click
})
},
refresh() {
// 强制 scroll 重新计算,当 better-scroll 中的元素发生变化的时候调用此方法
this.scroll && this.scroll.refresh()
}
},
watch:{
data() {
setTimeout(() => {
this.refresh()
}, 20)
}
}
}
</script>
需要注意的是,因为页面的数据是动态变化的,我们无法判断是轮播图的数据先响应完,还是推荐列表的数据先响完,
所以当我们在轮播图的图片第一次将容器撑开的时候,我们就去调用一次scroll实例的refresh方法
给images加上onload事件
<img :src="item.picUrl" @load="loadImage">
loadImage(){
//因为有多张图片,但是我们只需要当只有一张图片撑开的时候才做一下操作
if(!this.checkloaded){ //添加一个标志位,如果load一次了,就不再执行onload事件了
this.checkloaded = true
this.$refs.scroll.refresh()
}
}
在scroll组件传入recommendList,,它和scroll组件接收的data相对应,我们在scroll组件里面监听了data的变化,一旦变化了就调用refresh的方法,
做好以上两点,才可以让滚动列表正常滚动哦~~
4-12 vue-lazyload 懒加载插件介绍和应用
推荐列表可是有好多好多图片的哦,为了有一个好的用户体检,处理图片懒加载问题,vue-lazyload 。让我们用起来吧。
cnpm install vue-lazyload --save
在main.js中
import vulazyload from 'vue-lazyload'
Vue.use(vulazyload,{
loading: require('./common/image/default.png') //loading时默认显示的图片
})
将src改成v-lazy
<img v-lazy="item.imgurl" width="60" height="60">
这样,只有用户滚动过的地方,图片才会加载,没有看的地方,就不会进行加载
问题:fastclick和better-scroll的click会有冲突.
解决:slider中的添加一个class=“needsclick”,这是fastclick中的一个属性
<img class="needsclick" :src="item.picUrl" @load="loadImage">
4-13 loading 基础组件的开发和应用
在列表没有加载完成的时候,我们可以在页面上展示一个loading
loading组件的开发
<template>
<div class="loading">
<img width="24" height="24" src="./loading.gif">
<p class="desc">{{title}}</p>
</div>
</template>
<script>
export default {
props:{
title:{
type:String,
default:'加载中'
}
}
}
</script>
<style scoped>
.loading img{
text-align: center;
margin: 10px auto;
display: block;
}
.desc{
font-size: 14px;
text-align: center;
}
</style>
在页面上使用loading
<!--loading -->
<loading v-if="!recommendList.length"></loading>