【音乐App】—— Vue-music 项目学习笔记:推荐页面开发
作者:
秒速五厘米
一、页面简介+轮播图数据分析
数据:从QQ音乐抓取的真实数据
轮播图 | 热门歌单推荐 |
二、JSONP原理介绍
一句话解释JSONP原理:动态生成一个JavaScript标签,其src由接口url、请求参数、callback函数名拼接而成;利用js标签没有跨域限制的特性实现跨域请求
有几点需要注意:
callback函数要绑定在window对象上
服务端返回数据有特定格式要求:callback函数名+’(‘+JSON.stringify(返回数据) +’)’
不支持post,因为js标签本身就是一个get请求
什么是Promise:
简单说就是一个容器,里面保存着某个未来才会结束的事件 (通常是一个异步操作)的结果。
从语法上说,Promise是一个对象,从它可以获取异步操作的消息
Promise基本用法:
1. ES6规定,Promise对象是一个构造函数,用来生成Promise实例
var promise = new Promise(function(resolve,reject){ // ... some code if(/* 异步操作成功 */){ resolve(value); }else{ reject(error); } });
2. Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不是自己部署。
3. Promise实例生成以后,可以用then方法分别制定Resolved状态和Rejected状态的回调函数:
promise.then(function(value){ // sucess },function(error){ // failure });
三、JSONP
github地址:https://github.com/webmodules/jsonp
安装JSONP依赖:
npm install jsonp --save
四、封装JSONP、Primise
common->js目录下: 创建 jsonp.js
import originJSONP from 'jsonp' export default function jsonp(url, data, option) { url += (url.indecOf('?') < 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) : '' }
五、JSONP的应用+轮播图数据抓取
api目录下创建 config.js:配置与接口统一的参数
/** * 为了和QQ音乐接口一致,配置一些公用的参数、options和err_num码 */ export const commonParams = { g_tk: 5381, //会变,以实时数据为准 inCharset: 'utf-8', outCharset: 'utf-8', notice: 0, format: 'jsonp' } export const options = { param: 'jsonpCallback' } export const ERR_OK = 0
api目录下创建 recommend.js:
import jsonp from '@/common/js/jsonp' import {commonParames, options} from './config' export function getRecommend() { const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg' const data = Object.assign({}, commonParames, { platfrom: 'h5', uin: 0, needNewCode: 1 }) return jsonp(url, data, options) }
recommend.vue中调用并获取数据
import {getRecommend} from '@/api/recommend' import {ERR_OK} from '@/api/config' export default { created() { this._getRecommend(); }, methods: { _getRecommend() { getRecommend().then((res) => { if(res.code === ERR_OK) { console.log(res.data.slider) } }) } } }
六、 轮播图组件实现
base目录下: 创建slider.vue组件
插槽
<div class="slider-group"> <slot></slot> </div
recommend.vue 中编写插槽中的DOM:
<slider> <div v-for="(item, index) in recommends" :key="index"> <a :href="item.linkUrl"> <img :src="item.picUrl"> </a> </div> </slider
slider.vue 中指定需要从父组件接收的属性:loop是否循环、autoPlay是否自动播放、interval间隔时间
props: { loop: { type: Boolean, default: true }, autoPlay: { type: Boolean, default: true }, interval: { type: Number, default: 4000 } }
横向滚动:使用better-scroll
better-scroll中文文档:https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options-advanced.html#snap
better-scroll中的相关选项:
snap:专门为slide组件使用的
SnapSpeed : 轮播图切换的动画时间
1.安装better-scroll依赖:
npm install better-scroll --save
2.slider.vue 中引用:
import BScroll from 'better-scroll'
3.ref引用外层容器和内层元素:
<div class="slider" ref="slider"> <div class="slider-group" ref="sliderGroup">
4.common->js目录下创建 dom.js:封装一些DOM操作相关的代码
//为元素添加Class、判断元素是否有指定class export function addClass(el, className){ if(hasClass(el, className)){ return } let newClass = el.className.split(' ') newClass.push(className) el.className = newClass.join(' ') } export function hasClass(el, className){ let reg = new RegExp('(^|\\s)' + className + '(\\s|$)') return reg.test(el.className) }
5.slider.vue 中引用:
import {addClass, hasClass} from '@/common/js/dom'
6.在methods中定义两个方法:设置slider宽度、初始化slider
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] addClass(child, 'slider-item')//为循环生成的slider子元素,动态添加slider-item class child.style.width = sliderWidth + 'px'//不要忘记加单位! width += sliderWidth } if(this.loop){ //如果loop为true,BScroll的snap属性会左右克隆两个DOM,保证循环切换 width += 2 * sliderWidth } this.$refs.sliderGroup.style.width = width + 'px'//不要忘记加单位! }, _initSilder() { this.slider = new BScroll(this.$refs.slider,{ scrollX: true, //横向滚动 scrollY: false, //禁止纵向滚动 momentum: false,//禁止惯性运动 snap: { loop: this.loop, threshold: 0.3, speed: 400 } }) } }
7.初始化BScroll的时机:必须保证组件已经渲染好了,DOM高度已经被撑开
//在mouted生命钩子中通过setTimeout调用: mouted() { setTimeout(() => { this._setSliderWidth() this._initSlider() }, 20) }
8.坑:recommend.vue中直接引用了
9.解决:recommend.vue中为slider-wrapper添加v-if="recommends.length",确保recommends数组中有内容时,才渲染
<div v-if="recommends.length" class="slide-wrapper">
添加dots区块,实现自动轮播
1.data中维护一个数据dots,默认是一个空数组
dots: []
2.methods中初始化Dots:
_initDots() { this.dots = new Array(this.children.length) }
3.渲染dots:
<span class="dot" v-for="(item, index) in dots" :key="index"></span>
4.选中高亮:
/** data中维护一个数据currentPageIndex:0,表示当前默认是第一页 * v-bind动态绑定 :class="{active: currentPageIndex === index}"> * 在_initSlider()方法中给slider添加事件: */ this.slider.on('scrollEnd', () => { //当一个页面滚动完毕后,会派发一个scrollEnd事件 let pageIndex = this.slider.getCurrentPage().pageX //获得slider的pageIndex if(this.loop) { //如果是循环,snap会默认给子元素前面增加一个拷贝 pageIndex -= 1 //要得到实际的pageIndex,pageInde需要-1 } this.currentPageIndex = pageIndex })
5.自动播放:
//mounted()->setTimeout中判断autoplay属性,调用_play(): if(this.autoplay) { this._play() } //methods中定义_play(): _play() { let pageIndex = this.currentPageIndex + 1;//this.currentPageIndex从0开始的 if(this.loop) { pageIndex += 1//loop为true时,最开始有一个复制的副本,实际的pageIndex需要+1 } this.timer = setTimeout(() => { //页面的切换,利用BScroll的接口goToPage this.slider.goToPage(pageIndex, 0, 400) //参数:X方向、Y方向、时间间隔 },this.interval) }
6.坑:使用setTimeout,只会执行一次,从第一张自动滚动到第二张就停止了。
7.解决:scrollEnd事件中添加:
if(this.autoPlay) { this._play() }
8.坑:自动滚动后不到400ms时,手动滑动后又执行了自动滚动,体验效果会很奇怪
9.解决:slider 添加 beforeScrollStart事件
this.slider.on('beforeScrollStart', () => { if (this.autoPlay) { clearTimeout(this.timer) } })
10.坑:在滚动中,改变视口大小,图片会同时显示两张,因为之前设置好的width都没变
11.解决:mounted中监听window的resize事件 —— 窗口改变事件,当窗口改变时,重新调用_setSlideWidth()
12.坑:如果窗口变和不变时都调用_setSlideWidth(),就会执行两次width += 2 * sliderWidth,这一定是不对的
13解决:调用_setSlideWidth(),需要同时传入一个参数,用来判断窗口是否改变了
window,addEventListener('resize',(() => { if(!this.slider) { return } this._setSliderWidth(true) this.slider.refresh() })) _setSliderWidth(isResize) { //其它代码 if(this.loop && !isResize){ width += 2 * sliderWidth } }
14.App.vue 中优化:缓存DOM到内存中,不用重新发送请求,这样slider就不会有闪动的现象
<keep-alive> <router-view></router-view> </keep-alive>
15.slider中优化:当组件中有定时器,一定要记得在组件销毁时清理掉这些定时器,使用生命周期destroyed()
destroyed() { clearTimeout(this.timer) }
七、歌单数据接口分析
问题: QQ音乐歌单数据的请求头中有域名Host、来源Referer,所以请求的接口应该是有加上该域名和来源,直接请求就会报HTTP-500错误。
原因: 前端不能直接修改request header,所以要通过后端代理的方式解决。
解决: 采用 axios 在node.js中发送http请求
安装axios:
npm install axios --save
build->webpack.dev.conf.js
1.定义路由,通过axios发送一个Http请求,同时修改header中的和QQ相关的Host、Referer,
2.将浏览器传递过来的参数全部传给服务端,然后通json响应的内容输出到浏览器端。
3.在 const portfinder = require('portfinder') 后添加:
const express = require('express') const axios = require('axios') const app = express() var apiRoutes = express.Router() app.use('/api', apiRoutes)
3.在 devServer 中添加:
before(app) { //定义getDiscList接口,回调传入两个参数,前端请求这个接口 app.get('/api/getDiscList', function(req, res){ var url = "https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg" axios.get(url, { headers: { //通过node请求QQ接口,发送http请求时,修改referer和host referer: 'https://y.qq.com/', host: 'c.y.qq.com' }, params: req.query //把前端传过来的params,全部给QQ的url }).then((response) => { //成功与失败的回调 res.json(response.data) }).catch((e) => { console.log(e) }) })
recommend.js中:
import axios from 'axios'; export function getDiscList() { // const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg' 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' //使用的时axios,所以format使用的是json,不是jsonp }) // return jsonp(url, data, options) return axios.get(url, { params: data }).then((res) => { return Promise.resolve(res.data) //es6新语法,返回一个以给定值解析后的Promise对象 }) }
Promise.resolve(value)方法返回一个以给定值解析后的Promise对象;但如果这个值是个thenable(即带有then方法),返回的promise会“跟随”
这个thenable的对象,采用它的最终状态(指resolved/rejected/pending/settled);
如果传入的value本身就是promise对象,则该对象作为Promise.resolve方法的返回值返回;否则以该值为成功状态返回promise对象。
recommend.vue中:定义和调用获取数据的方法
//created()中: this._getDiscList(); //methods中: _getDiscList() { getDiscList().then((res) => { if(res.code === ERR_OK) { console.log(res.data) } }) }
八、歌单列表组件开发和数据的应用
data中定义数据:
discList: []
_getDiscList()中将返回的数据list赋给discList:
<div class="recommend-list"> <h1 class="list-title">热门歌单推荐</h1> <ul> <li v-for="(item, index) in discList" :key="index" class="item"> <div class="icon"> <img :src="item.imgurl" width="60" height="60"> </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>
CSS样式:经典flex布局
1.左边固定宽高,右边根据手机视口宽度自适应
2.右侧:
.item display: flex align-items:center //水平方向居中
3.右侧文字内容:
.text display: flex flex-direction: column //纵向排列 justify-content: center //垂直居中
4.一个元素,既可以是flex布局的item,同时也可做flex布局
九、scroll组件的抽象和应用
better-scroll滚动布局:只会滚动父元素下的第一个子元素 —— 想要slider和recommend-list同时可以滚动,需要在外层再嵌套一个
,将两个元素包裹起来
抽象出scorll组件 -- 基础组件
1.base->scroll目录下: 创建 scroll.vue
2.布局DOM:一个wrapper加一个插槽
<template> <div ref="wrapper"> <slot></slot> </div> </template>
3.布局DOM:一个wrapper加一个插槽
import BScroll from 'better-scroll'
4.需要传入props参数:
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 } }
5.确保DOM已经渲染,再执行_initScroll:
mouted() { setTimeout(() => { //确保DOM已经渲染 this. _initScroll() }, 20) }
6.methods中定义初始化scroll的方法,并代理几个必需的方法:
methods: { _initScroll() { if(!this.$refs.wrapper){ return } this.scroll = new BScroll(this.$refs.wrapper, { probeType : this.probeType, click: this.click }) }, enable() { // 启用 better-scroll,默认开启 this.scroll && this.scroll.enable() }, disable() { // 禁用better-scroll, 如果不加,scroll的高度会高于内容的高度 this.scroll && this.scroll.disable() }, refresh() { // 强制 scroll 重新计算,当 better-scroll 中的元素发生变化的时候调用此方法 this.scroll && this.scroll.refresh() } }
7.watch监听data数据:
watch: { data() { //监测data的变化 setTimeout(() => { this.refresh() }, 20) } }
8.后面在项目的开发中,可以根据需要再随时添加props参数和methods代理方法
recommend.vue 中使用:
1.引用scroll组件:
import Scroll from '@/base/scroll/scroll'
2.把class="recommend-content"的
改成
3.坑:此时scroll已经初始化了,但还不能滚动
4.原因:scroll初始化的时机,是在scroll组件的mounted();但
5.解决:
6.坑:因为整个页面会有两个部分都是请求数据,当_getRecommend()的请求时间大于this._getDiscList()的时候,页面的高度就不够
7.如果:如下 ↓ 滚动的高度就会差一个slider的高度,滚不到底部。因为refresh()之前,slider的数据还没有渲染出来,scroll会认为,需要滚动的高度,只是列表的高度 [1] => 10.log
created() { setTimeout(() => { this._getRecommend(); }, 1000) this._getDiscList(); }
8.实际中,并不能知道两个部分,哪一个会先出现,需要注意还有一个坑:不能用计算属性计算两个部分的数据
9.原因:与图片的加载,视口的大小(实时图片的宽高)有关。
10.解决:给添加onload事件
<img :src="item.picUrl" @load="loadImage"
loadImage() { if(!this.checkloaded){ //添加一个标志位,如果load一次了,就不再执行onload事件了 this.checkloaded = true this.$refs.scroll.refresh() } }
十、 lazyload懒加载插件介绍和应用
歌单优化:歌单是由很多张图片组成的,使用vue-lazyload插件 解决图片懒加载 的问题
vue-lazyload github地址: https://github.com/hilongjw/vue-lazyload
安装插件:
npm install vue-lazyload --sav
引用注册: main.js 中
import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload, { loading: require('@/common/image/default.png') //loading时默认显示的图片 })
使用插件:recommend.vue 中把歌单列表中原来的 :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"
十一、 loading基础组件的开发和应用
优化体验:在歌单列表没有渲染好之前,展示一个转圈loading
布局DOM:
<div class="loading"> <img width="24" height="24" src="./loading.gif"> <p class="desc">{{title}}</p> </div>
props参数:
props: { title: { type: String, default: '正在载入...' } }
CSS样式:
<style scoped rel="stylesheet/stylus"> @import "~common/stylus/variable" .loading width: 100% text-align: center .desc line-height: 20px font-size: $font-size-small color: $color-text-l </style>
recommend.vue 中引用注册,在
<div class="loading-container" v-show="!disList.length"> <loading></loading> </div>
注:项目来自慕课网