推荐界面(轮播图+歌单列表)+歌单详情页,重点是数据的获取,在线抓取
获取数据Jsonp2,解决跨域问题,在任何站点都可以通过Jsonp去请求这个接口来获取数据
原理:不是ajax请求,是动态创建script标签进行跨域的,将script的src指向我们请求的服务端地址,npm安装jsonp,axios,编写jsonp.js引入jsonp,并对原来的jsonp做一个简单的封装
1. src->common->js中,编写jsonp.js,引入jsonp插件,并对其做一个简单的封装
import originJsonp from 'jsonp'
// url是一个地址,请求通过data拼接到url上,并返回一个promise
export default function jsonp(url, data, option) {
// url没有?时要先添加一个
url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)
return new Promise((resolve, reject) => {
originJsonp(url, option, (err, data) => {
if (!err) {
resolve(data)
} else {
reject(err)
}
})
})
}
// 对要与url拼接的data进行处理
export 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) : ''
}
用promise对原生的jsonp进行封装之后,即可用jsonp真实的抓取我们的线上数据,获取数据通常会获取一些方法,在src-api目录下给每一个部分封装获取它相关数据的方法,我们这里做的是推荐相关,所以在这里建一个recommend.js,实现getRecommend方法,这个方法的原理就是利用jsonp获取数据,所以要先import jsonp,首先要定义url,data指的就是Query String Parameters,我们将相同的参数进行封装,所以在configure.js中进行公共参数的封装
2. 在api->config.js中统一参数格式
export const commonParams = {
g_tk: 1251607169,
inCharset: 'utf-8',
outCharset: 'utf-8',
notice: 0,
format: 'jsonp'
}
export const options = {
param: 'jsonpCallback'
}
// "code":0,正确的值为0
export const ERR_OK = 0
3. 引入jsonp.js和conifg.js,编写recommend.js获取jsonp数据
import jsonp from 'common/js/jsonp'
import {commonParams, options} from './config.js'
import axios from 'axios'
export function getRecommend() { // 利用jsonp抓取推荐数据
const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
const data = Object.assign({}, commonParams, { // assign合并对象
platform: 'h5',
uni: 0,
needNewCode: 1
})
return jsonp(url, data, options)
}
export function getDiscList() {
const url = '/api/getDiscList'
const data = Object.assign({}, commonParams, {
platform: 'yqq',
hostUin: 0,
sin: 0,
ein: 29,
sortId: 5,
needNewCode: 0,
categoryId: 10000000,
rnd: Math.random(),
format: 'json' // 将format从jsonp修改为json
})
return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
}
3.在recommend.vue中接收并处理recommend.js获取的数据,getDiscList是后来获取的歌单列表
import {getRecommend, getDiscList} from 'api/recommend' // 引入js中定义的方法
import {ERR_OK} from 'api/config'
在data中自定义变量结束recommend和discList,discList的获取在后边介绍
data() {
return {
recommends: [],
discList: []
}
}
在created()调用方法,获取数据
created() {
// setTimeout(() => {
// this._getRecommend()
// }, 2000);
this._getRecommend()
// 模拟loading效果
// setTimeout(() => {
// this._getDiscList()
// }, 1000);
this._getDiscList()
},
methods: {
_getRecommend() {
getRecommend().then((res) => { // res对应的就是json对象,之前已经import该方法了
if (res.code === ERR_OK) {
// console.log(res.data.slider)
this.recommends = res.data.slider
}
})
},
_getDiscList() {
getDiscList().then((res) => {
if (res.code === ERR_OK) {
// console.log(res.data.list)
this.discList = res.data.list
}
})
}
}
4接下来将获取的数据应用到轮播图组件上.
写一个轮播图组件src-base-slider.vue,将数据填充到DOM中,在src目录下新建base文件夹,用来存放基础组件,base->slider->slider.vue
1)在recommend.vue中注册并引入slider,将3中recommends数据中的图片和图片的超链接传给slider.vue
首先,created中获得的slider数据在data中保存:
data() {
return {
recommends: [],
discList: []
}
}
编写DOM:
<div v-if="recommends.length" class="silder-wrapper">
<slider>
<div class="needsclick" @load="loadImage" v-for="item in recommends" :key="item.id">
<a :href="item.linkUrl">
<img :src="item.picUrl" >
</a>
</div>
</slider>
</div>
2)编写slider组件,slider可以实现手动滑动,自动轮播,首先实现手动播放,然后是自动轮播,slot的作用:外部引入slider
的时候,slider包裹的DOM会被插入到插槽的部分;现在props中定义一些外部组件控制silder组件的一些变量
props: {
loop: { // 循环轮播,手动滑动
type: Boolean,
default: true
},
autoPlay: { // 自动轮播
type: Boolean,
default: true
},
interval: { // 轮播间隔
type: Number,
default: 4000
}
}
slider可以使用betterScroll来实现,主要是用来监听一些touch事件。我们在 mounted钩子函数中对betterScroll进行初始化,因为是横向滚动,所以在初始化之前我们要设置slider的宽度:编写this._setSliderWidth()和_initSlider()方法,并在mounted中调用他们,首先为DOM元素添加引用
<div class="slider" ref="slider"> <!-- 外层容器 -->
<div class="slider-group" ref="sliderGroup"> <!-- 内部元素,要设置sliderGroup的宽度,因为横向不能被自动撑宽-->
计算宽度:
// 横向滚动初始化bscroll之前要下计算宽度,因为横向不能被自动撑宽
_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]
// 为child添加样式,因为不可能在父组件加载样式的时候手动添加样式
addClass(child, 'slider-item')
// 为每张图片设置宽度
child.style.width = sliderWidth + 'px'
// 计算所有图片的总宽度
width += sliderWidth
}
// loop轮播实现的时候会左右克隆两个DOM,所以长度要增加
if (this.loop && !isResize) {
width += 2 * sliderWidth
}
// 设置slider-group(内容区的宽度)
this.$refs.sliderGroup.style.width = width + 'px'
}
其中,methods中,计算宽度,其中将添加样式的方法addClass有dom.js引入,src-common-js-dom.js
import {addClass} from 'common/js/dom'
export function hasClass(el, className) { // 如果DOM已经有class了就不用再添加了
let reg = new RegExp('(^|\\s)' + className + '(\\s|$)')
return reg.test(el.className) // 满足class的定义方式就返回true
}
// DOM对象,DOM对象的className(样式的class)
export function addClass(el, className) {
if (hasClass(el, className)) {
return
}
// 添加class,添加之前可能已经有别的class了,新旧class拼接
let newClass = el.className.split(' ')
newClass.push(className)
el.className = newClass.join(' ')
}
我们在mounted中初始化高度,因为在recommend组件中,created获取数据是异步执行的,所以在slider组件中mounted执行的时候,slot中可能没有数据。我们要确保mounted执行的时候slot中是有值的,我们可以在recommend中执行slider组件之前添加v-if进行条件约束,判断推荐数据有值了之后在进行组件的引入;
<div v-if="recommends.length" class="silder-wrapper">
<slider>
mounted() {
// 保证DOM成功渲染的话可以加一个延时,也可以用nextTick()
setTimeout(() => {
// 将初始化代码封装到methods中
this._setSliderWidth()
this._initDots()
this._initSlider()
if (this.autoPlay) { // 是否要自动播放
this._play()
}
}, 20) // 浏览器刷新通常是17毫秒一次
window.addEventListener('resize', () => {
if (!this.slider) { // bScroll没有初始化的时候
return
}
// 窗口大小改变时重新计算宽度
this._setSliderWidth(true)
this.slider.refresh()
})
},
之后,编写BS初始化函数,初始化函数也写在mountend中
_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 // 超链接不能点击的问题,fastClick的问题
})
}
这样我们就实现了鼠标拖动的轮播滚动,接下来我们要去实现dots和自动的轮播滚动;首先在slider组件中添加dots,因为在无缝滚动的时候,我们复制了两幅图片,所以我们要在init_slider之前初始化dots,保证dots的个数与原图片个数相同;
// 将初始化代码封装到methods中
this._setSliderWidth()
this._initDots()
this._initSlider()
同时,在data中定义这个dots,dots默认是空数组
data() {
return {
dots: [],
currentPageIndex: 0
}
}
——initDots函数,只需要初始化一个数组即可
_initDots() {
this.dots = new Array(this.children.length)
}
之后,便可以在DOM中渲染dots了,滚动到当前点是,dot会放大
<template>
<div class="slider" ref="slider"> <!-- 外层容器 -->
<div class="slider-group" ref="sliderGroup"> <!-- 内部元素,要设置sliderGroup的宽度-->
<slot>
</slot>
</div>
<div class="dots">
<span class="dot" v-for="(item,index) in dots" :key="item, index" :class="{active: currentPageIndex === index}"></span>
</div>
</div>
</template>
之后,我们去维护currenPageIndex的值,在data中定义,并初始化为0;在bs滚动的时候是会派发一个事件的,所以我们在初始化slider的时候维护index的值
_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 // 超链接不能点击的问题,fastClick的问题
})
// 每一次滚动完一张图片时更新currentPageIndex的值,若是在自动轮播模式下,要添加play方法
this.slider.on('scrollEnd', () => {
let pageIndex = this.slider.getCurrentPage().pageX
if (this.loop) { // 循环模式下添加拷贝,所以index要减一
pageIndex -= 1
}
this.currentPageIndex = pageIndex
接下来还要添加一个自动播放的功能,就是props中的autoplay属性,在moutend中的setTime中,我们要判断是否为自动播放,如果是自动播放的话,我们要添加一个播放函数_play();
setTimeout(() => {
// 将初始化代码封装到methods中
this._setSliderWidth()
this._initDots()
this._initSlider()
if (this.autoPlay) { // 是否要自动播放
this._play()
}
}, 20) // 浏览器刷新通常是17毫秒一次
play方法的核心就是修改pageIndex,让其跳转到下一张图片
_play() { // 自动轮播
let pageIndex = this.currentPageIndex + 1 // 播放下一张
if (this.loop) { // 如果是一个循环加1,因为this.currentPageIndex是从0开始的
pageIndex += 1 // 所以pageIndex所对应的元素要比他多一个
// 设置定时器自动播放
this.timer = setTimeout(() => {
// goToPage是BScroll的方法,下标,方向,0代表x方向,间隔400
this.slider.goToPage(pageIndex, 0, 400)
}, this.interval) // 在props中定义的轮播的间隔
}
}
上述自动播放中,我们用的是setTimeout,只会重播一次,所以在_initSlider中,判断是否是自播放,若是的话调用自动播放函数this._play()
// 自动轮播一次就会停止问题,添加自动播放函数
if (this.autoPlay) {
clearTimeout(this.timer)
this._play()
}
视口宽度改变时。轮播图错乱,解决办法,在mounted中监听视口大小的变化,这是因为视口的宽度改变了,但是我们之前为图片设置的宽度还是视口宽度变化前的宽度,所以在mounted中我们只需要监听window.resize事件
mounted() {
// 保证DOM成功渲染的话可以加一个延时,也可以用nextTick()
setTimeout(() => {
// 将初始化代码封装到methods中
this._setSliderWidth()
this._initDots()
this._initSlider()
if (this.autoPlay) { // 是否要自动播放
this._play()
}
}, 20) // 浏览器刷新通常是17毫秒一次
window.addEventListener('resize', () => {
if (!this.slider) { // bScroll没有初始化的时候
return
}
// 窗口大小改变时重新计算宽度
this._setSliderWidth(true)
this.slider.refresh()
})
}
但是我们不能每次在初始化的时候都将图片个数加2,所以我们设置一个标志位,isResize,表示这个函数是不是resize过来的,如果是resize过来的,我们计算总宽度的时候就不加那两张图片了
// 横向滚动初始化bscroll之前要下计算宽度,因为横向不能被自动撑宽
_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]
// 为child添加样式
addClass(child, 'slider-item')
// 为每张图片设置宽度
child.style.width = sliderWidth + 'px'
// 计算所有图片的总宽度
width += sliderWidth
}
// loop轮播实现的时候会左右克隆两个DOM,所以长度要增加
if (this.loop && !isResize) {
width += 2 * sliderWidth
}
// 设置slider-group(内容区的宽度)
this.$refs.sliderGroup.style.width = width + 'px'
},
还有一个问题,在手机端点击的时候不能跳转,所以在初始化init_slider的时候将click:true去掉,bs中的click:true会阻止浏览器默认的click,自己派发一个click,这个click有恰好被fastclick监听到,然后被阻止,导致了click不能被执行,所以我们将bs中click:true去掉,因为a连接跳转就是一个默认行为,不需要监听click
切换tab时闪现,每次切换界面时都会重新发送数据请求,recommend的created生命周期等都会重新走一遍,重新获取数据,使用keeplive实现数据的缓存,
<template>
<div id="app">
<m-header></m-header>
<tab></tab>
<keep-alive>
<router-view></router-view>
</keep-alive>
<player></player>
</div>
</template>
slider被切走的时候会调用destroyed这个钩子函数,手动清除计时器等资源timer,有利于资源的释放
destroyed() {
clearTimeout(this.timer)
}
timer是在自动轮播方法_play()中定义的
this.timer = setTimeout(() => {
// goToPage是BScroll的方法,下标,方向,0代表x方向,间隔400
this.slider.goToPage(pageIndex, 0, 400)
}, this.interval)
轮播图完成,接下来是热门歌单部分的编写