【音乐App】—— Vue-music 项目学习笔记:推荐页面开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
上一篇总结了项目概述、项目准备、页面骨架搭建。这一篇重点梳理推荐页面开发。项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。
一、页面简介+轮播图数据分析 |
- 数据:从QQ音乐抓取的真实数据
轮播图 | 热门歌单推荐 |
二、JSONP原理介绍 |
- 一句话解释JSONP原理:动态生成一个JavaScript标签,其src由接口url、请求参数、callback函数名拼接而成;利用js标签没有跨域限制的特性实现跨域请求
- 有几点需要注意:
- callback函数要绑定在window对象上
- 服务端返回数据有特定格式要求:callback函数名+’(‘+JSON.stringify(返回数据) +’)’
- 不支持post,因为js标签本身就是一个get请求
- 什么是Promise:
- 简单说就是一个容器,里面保存着某个未来才会结束的事件 (通常是一个异步操作)的结果。
- 从语法上说,Promise是一个对象,从它可以获取异步操作的消息
- Promise基本用法:
- ES6规定,Promise对象是一个构造函数,用来生成Promise实例
var promise = new Promise(function(resolve,reject){
// ... some code
if(/* 异步操作成功 */){
resolve(value);
}else{
reject(error);
}
}); - Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不是自己部署。
- 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组件
- 插槽<slot></slot>:外部引用slider.vue时,<slider></slider>里面包裹的DOM,会被插入到插槽的部分
<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中的相关选项:
|
- 安装better-scroll依赖:
npm install better-scroll --save
- slider.vue 中引用:
import BScroll from 'better-scroll'
ref引用外层容器和内层元素:
<div class="slider" ref="slider">
<div class="slider-group" ref="sliderGroup">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)
}- slider.vue 中引用:
import {addClass, hasClass} from '@/common/js/dom'
在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
}
})
}
}- 初始化BScroll的时机:必须保证组件已经渲染好了,DOM高度已经被撑开
//在mouted生命钩子中通过setTimeout调用:
mouted() {
setTimeout(() => {
this._setSliderWidth()
this._initSlider()
}, 20)
} - 坑:recommend.vue中直接引用了<slider>,recommends的引用时机是在created()中调用了_getRecommend(),_getRecommend()的这个时间是一个异步过程,可能会有延迟,因为它取的是真实数据;因此,当recommends还没有get到时,即还没有填入任何数据时,slider.vue中的mouted()实际上已经执行了。
- 解决:recommend.vue中为slider-wrapper添加v-if="recommends.length",确保recommends数组中有内容时,才渲染<slider>
<div v-if="recommends.length" class="slide-wrapper">
- 添加dots区块,实现自动轮播
- data中维护一个数据dots,默认是一个空数组
dots: []
- methods中初始化Dots:
_initDots() {
this.dots = new Array(this.children.length)
} - 渲染dots:
<span class="dot" v-for="(item, index) in dots" :key="index"></span>
- 选中高亮:
/** 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
}) - 自动播放:
//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)
} - 坑:使用setTimeout,只会执行一次,从第一张自动滚动到第二张就停止了。
- 解决:scrollEnd事件中添加:
if(this.autoPlay) {
this._play()
} - 坑:自动滚动后不到400ms时,手动滑动后又执行了自动滚动,体验效果会很奇怪
- 解决:slider 添加 beforeScrollStart事件
this.slider.on('beforeScrollStart', () => {
if (this.autoPlay) {
clearTimeout(this.timer)
}
}) - 坑:在滚动中,改变视口大小,图片会同时显示两张,因为之前设置好的width都没变
- 解决:mounted中监听window的resize事件 —— 窗口改变事件,当窗口改变时,重新调用_setSlideWidth()
- 坑:如果窗口变和不变时都调用_setSlideWidth(),就会执行两次width += 2 * sliderWidth,这一定是不对的
- 解决:调用_setSlideWidth(),需要同时传入一个参数,用来判断窗口是否改变了
window,addEventListener('resize',(() => {
if(!this.slider) {
return
}
this._setSliderWidth(true)
this.slider.refresh()
})) _setSliderWidth(isResize) {
//其它代码
if(this.loop && !isResize){
width += 2 * sliderWidth
}
} App.vue 中优化:缓存DOM到内存中,不用重新发送请求,这样slider就不会有闪动的现象
<keep-alive>
<router-view></router-view>
</keep-alive>- 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
- 定义路由,通过axios发送一个Http请求,同时修改header中的和QQ相关的Host、Referer,
- 将浏览器传递过来的参数全部传给服务端,然后通json响应的内容输出到浏览器端。
- 在 const portfinder = require('portfinder') 后添加:
const express = require('express')
const axios = require('axios')
const app = express()
var apiRoutes = express.Router()
app.use('/api', apiRoutes) - 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对象
})
}
|
- recommend.vue中:定义和调用获取数据的方法
//created()中:
this._getDiscList(); //methods中:
_getDiscList() {
getDiscList().then((res) => {
if(res.code === ERR_OK) {
console.log(res.data)
}
})
}
八、歌单列表组件开发和数据的应用 |
- data中定义数据:
discList: []
- _getDiscList()中将返回的数据list赋给discList:
this.discList = res.data.list
- 使用 v-html="item.creator.name" 给html字符做转义
<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布局
- 左边固定宽高,右边根据手机视口宽度自适应
- 右侧:
.item
display: flex
align-items:center //水平方向居中 - 右侧文字内容:
.text
display: flex
flex-direction: column //纵向排列
justify-content: center //垂直居中 - 一个元素,既可以是flex布局的item,同时也可做flex布局
九、scroll组件的抽象和应用 |
- better-scroll滚动布局:只会滚动父元素下的第一个子元素 —— 想要slider和recommend-list同时可以滚动,需要在外层再嵌套一个<div>,将两个元素包裹起来
- 抽象出scorll组件 -- 基础组件
- base->scroll目录下: 创建 scroll.vue
- 布局DOM:一个wrapper加一个插槽
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template> 引入BScroll:
import BScroll from 'better-scroll'
- 需要传入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
}
} - 确保DOM已经渲染,再执行_initScroll:
mouted() {
setTimeout(() => { //确保DOM已经渲染
this. _initScroll()
}, 20)
} - 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()
}
} - watch监听data数据:
watch: {
data() { //监测data的变化
setTimeout(() => {
this.refresh()
}, 20)
}
} - 后面在项目的开发中,可以根据需要再随时添加props参数和methods代理方法
- recommend.vue 中使用:
- 引用scroll组件:
import Scroll from '@/base/scroll/scroll'
- 把class="recommend-content"的<div>改成<scroll>
- 坑:此时scroll已经初始化了,但还不能滚动
- 原因:scroll初始化的时机,是在scroll组件的mounted();但<scroll>包含的DOM是由获取到的data数据填充撑开高度才可以滚动,此时还没撑开,就滚动不了;当数据改变后,scroll应该改变
- 解决:<scroll>传入一个数据 :data="discList";当数据discList接收到时,scroll组件中的watch监听到这个变化,就会强制scroll重新计算
- 坑:因为整个页面会有两个部分都是请求数据,当_getRecommend()的请求时间大于this._getDiscList()的时候,页面的高度就不够
- 如果:如下 ↓ 滚动的高度就会差一个slider的高度,滚不到底部。
因为refresh()之前,slider的数据还没有渲染出来,scroll会认为,需要滚动的高度,只是列表的高度created() {
setTimeout(() => {
this._getRecommend();
}, 1000)
this._getDiscList();
} - 实际中,并不能知道两个部分,哪一个会先出现,需要注意还有一个坑:不能用计算属性计算两个部分的数据
- 原因:与图片的加载,视口的大小(实时图片的宽高)有关。
- 解决:给<img>添加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 --save
- 引用注册: main.js 中
import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload, {
loading: require('@/common/image/default.png') //loading时默认显示的图片
}) - 使用插件:recommend.vue 中把歌单列表<img>中原来的 :src替换为v-lazy
<img v-lazy="item.imgurl" width="60" height="60">
- 这样,只有用户滚动过的地方,图片才会加载,没有看的地方,就不会进行加载
- 问题:fastclick和better-scroll的click会有冲突.
- 解决:slider中的<img>添加一个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样式:
- recommend.vue 中引用注册,在<scroll>中使用:
<div class="loading-container" v-show="!disList.length">
<loading></loading>
</div>
注:项目来自慕课网
【音乐App】—— Vue-music 项目学习笔记:推荐页面开发的更多相关文章
- 【音乐App】—— Vue-music 项目学习笔记:歌单及排行榜开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 歌单及详情页 排行榜及详情 ...
- 【音乐App】—— Vue-music 项目学习笔记:项目准备
前言: 学习慕课网Vue高级实战课程后,在实践中总结一些这个项目带给自己的收获,希望可以再次巩固关于Vue开发的知识.这一篇主要梳理:项目概况.项目准备.页面骨架搭建.项目github地址:https ...
- 【音乐App】—— Vue-music 项目学习笔记:歌手详情页开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 歌曲列表 歌曲播放 一.子 ...
- 【音乐App】—— Vue-music 项目学习笔记:搜索页面开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 搜索歌手歌曲 搜索历史保存 ...
- 【音乐App】—— Vue-music 项目学习笔记:播放器内置组件开发(二)
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 播放模式切换 歌词滚动显示 ...
- 【音乐App】—— Vue-music 项目学习笔记:用户个人中心开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 歌曲列表 收藏歌曲 一.用 ...
- 【音乐App】—— Vue-music 项目学习笔记:歌曲列表组件开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 当前歌曲播放列表 添加歌曲 ...
- 【音乐App】—— Vue-music 项目学习笔记:歌手页面开发
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 一.歌手页面布局与设计 需 ...
- 最新 Vue 源码学习笔记
最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...
随机推荐
- Bit Operation妙解算法题
5道巧妙位操作的算法题. ***第一道*** 题目描述 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次.找出那个只出现了一次的元素. 说明: 你的算法应该具有线性时间复杂度. ...
- rsync 使用小记
工作中遇到了有关rsync使用的问题,在这里记录下供有同样需求的人参考一下 先说下环境 服务端配置 pid file = /rsyncdata/rsyncd.pid port = 873 addres ...
- BZOJ 1087:[SCOI2005]互不侵犯King(状压DP)
[SCOI2005]互不侵犯King [题目描述] 在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案.国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子 ...
- 【bzoj2789】[Poi2012]Letters 树状数组求逆序对
题目描述 给出两个长度相同且由大写英文字母组成的字符串A.B,保证A和B中每种字母出现的次数相同. 现在每次可以交换A中相邻两个字符,求最少需要交换多少次可以使得A变成B. 输入 第一行一个正整数n ...
- Quotes
A man's gotta do what a man's gotta do.
- BZOJ3531 [Sdoi2014]旅行 【树剖 + 线段树】
题目 S国有N个城市,编号从1到N.城市间用N-1条双向道路连接,满足 从一个城市出发可以到达其它所有城市.每个城市信仰不同的宗教,如飞天面条神教.隐形独角兽教.绝地教都是常见的信仰.为了方便,我们用 ...
- 【VBA】标准Sub/Function定义,带ScreenUpdating、On Error GoTo
[说明] 标准Sub/Function定义,带ScreenUpdating.On Error GoTo Sub AutoFillRole() '--------------- ERROR MSG--- ...
- shell的使用
删除 http://blog.csdn.net/u011579204/article/details/46709929 #! /bin/bash dir=/webserver/test/(等号之间不能 ...
- 1.docker学习
Docker —— 从入门到实践 http://udn.yyuap.com/doc/docker_practice/introduction/index.html 非常详细的Docker学习教程 ht ...
- KMP--君住长江头,我住长江尾,日日思君不见君,共饮长江水
POJ 3461: Oulipo 题意: 求出第一个串在第二个串中的出现次数... 分析: KMP板子题... 代码: #include<algorithm> #include<io ...