原文链接:https://blog.csdn.net/Forever201295/article/details/80266600

一、项目说明
该播放器的是基于学习vue的实战练习,不用于其他途径。应用中的全部数据来自于 QQ音乐 移动端(https://m.y.qq.com/),利用 jsonp 以及 axios 代理后端请求抓取。

二、目录结构
目录/文件         说明
api                    与后台数据交互文件
base                 一些与业务逻辑无关的基础组件,例如轮播图:slider组件
common           存放图片,字体,样式,以及js插件等公共资源
components     业务逻辑代码
router                项目路由
store                 vuex状态管理配置
三、base组件
1、轮播图slider
引入组件 better-scroll

1.1、参数设置
loop:是否循环播放
autoPlay:是否自动播放
interval:自动播放的间隔时间
2.1、实现
(1).需要通过获取slider的宽度来设置每一个轮播图和轮播图的包裹层的宽度
(2).初始化better-scroll实例
若设置 loop为true 会自动 clone 两个轮播插在前后位置,如果轮播循环播放,是前后各加一个轮播图保证无缝切换,所以需要再加两个宽度

if (this.loop) {
width += 2 * sliderWidth
}

(3). 给slider绑定’scrollEnd‘事件,来获取当前滚动值currentPageIndex
(4).dots小圆点的active状态。通过currentPageIndex === index 来判断
(5).为了保证改变窗口大小依然正常轮播,监听窗口 resize 事件,通过better-scroll提供的refresh()重新渲染轮播图

window.addEventListener('resize', () => {
if (!this.slider) {
return
}
this._setSliderWidth(true)
this.slider.refresh()
})
}

(6)在组件销毁之前 beforeDestroy 销毁定时器

2 播放进度条组件
2.1 全屏下 条状滚动条progeress-bar
参数设置
percent:显示当前播放进度
实现
a. 拖拽按钮时候:监听touchstart,touchmove,touchend事件
touchstart: 获取第一次点击的横坐标clinetX:startX,整个progress的clientWidth:left。
touchmove:获取移动后的横坐标,计算对应的delta,此时进度条的位置 = clinetWidth+delta
touchend:派发出percent。从而改变progress的width
progressTouchStart(e) {
this.touch.startX = e.touches[0].clientX
this.touch.left = this.$refs.progress.clientWidth
},
progressTouchMove(e) {
let delta = e.touches[0].clientX - this.touch.startX
let offsetWidth = this.touch.left + delta
this._offset(offsetWidth)
},
progressTouchEnd() {
this._triggerPercent()
},

b. 点击时候:也是通过点击的位置计算出progress的宽度。

progressClick(e) {
const rect = this.$refs.progressBar.getBoundingClientRect()
const offsetWidth = e.pageX - rect.left
this._offset(offsetWidth)
this._triggerPercent()
},

注:getBoundingClientRect用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。
getBoundingClientRect是DOM元素到浏览器可视范围的距离(不包含文档卷起的部分),
该函数返回一个Object对象,该对象有6个属性:top,lef,right,bottom,width,height;

2.1 mini 圆形滚动条progeress-circle
参数
radius:设置圆形的直径
percent:当前进度
实现
圆形采用svg,中有两个圆,一个是背景圆形,另一个为已播放的圆形进度,圆形进度主要用了stroke-dasharray(描边距离) 和stroke-dashoffset(描边偏移距离) 两个属性的设置来显示对应进度变化
<svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
<circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" :stroke-dasharray="dashArray"
:stroke-dashoffset="dashOffset"/>
</svg>

viewBox = 0 0 100 100 100 是相对于svg里面的设置
传入radius为32: 100 =》直径 32
r = 50 :50 =》半径 16
stroke-dasharray : 描边距离 (这里对应为 math.PI * 100)
stroke-dashoffset: 描边偏移距离 (设置为 314,则偏移了314,这个圆就没有了。设置为0 ,这个圆完全显示)

computed: {
dashOffset() {
return (1 - this.percent) * this.dashArray //只需根据percent来stroke-dashoffset即可显示进度
}
}

四、api拿后端数据
组件中的数据全都是拿了qq音乐网页版的数据,拿数据的方式有两种,一种可以直接通过jsonp跨域来获取的,另一种接口通过referer伪造请求,
1.jsonp方式
在common中,封装一个公用jsonp方法

import originJsonp from 'jsonp'

export default function jsonp(url, data, option) {
url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)

return new Promise((resolve, reject) => {
originJsonp(url, option, (err, data) => {
if (!err) {
resolve(data)
} else {
reject(err)
}
})
})
}

2.伪造请求
一些接口在后台简单设置一下 Referer, Host,可以限制前台直接通过浏览器抓到你的接口,但是这种方式防不了后端代理的方式,前端 XHR 会有跨域限制,后端发送 http 请求则没有限制,因此可以伪造请求
vue提供的axios可以在浏览器端发送 XMLHttpRequest 请求,在服务器端发送 http 请求获取;
在webpack.dev.config中配置如下

var express = require('express')
var axios = require('axios')
var app = express()
var apiRoutes = express.Router()
before(apiRoutes){
apiRoutes.get('/api/getDiscList',(req,res)=>{
const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg';
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query //这是请求的query
}).then((response) => {
//response是url地址返回的,数据在data里。
res.json(response.data)
}).catch((e) => {
console.log(e);
})
});
app.use('/api', apiRoutes);
},
}

定义一个路由,拿到一个 /api/getDiscList 接口,通过 axios 伪造 headers,发送给QQ音乐服务器一个 http 请求,得到服务端正确的响应,通过 res.json(response.data) 返回到浏览器端;
注意:此时的这个接口返回的格式已经是json,应该设置format:json
那么问题来了,大公司怎么防止被恶意代理呢?当你的访问量大的时候,出口ip就会可能被查到获取封禁,还有一种方式就是参数验签,也就是请求人家的数据必须带一个签名参数,然后这个签名参数是很难拿到的这个正确的签名,从而达到保护数据的目的;

五、components业务逻辑代码
1、推荐页面
Scroll 初始化但却没有滚动,是因为初始化时机不对,必须保证数据到来,DOM 成功渲染之后 再去进行初始化
可以使用父组件 给 Scrol组件传 :data 数据,Scroll 组件自己 watch 这个 data,有变化就立刻 refesh 滚动
对应图片可以通过监听onload事件,来进行滚动刷新

<img @load="loadImage" class="needsclick" :src="item.picUrl">
loadImage() {
if (!this.checkloaded) {
this.checkloaded = true
this.$refs.scroll.refresh()
}
}

新版本 BScroll 已经自己实现检测 DOM 变化,自动刷新,大部分场景下无需传 data 了

2、歌手页面
2.1、数据重构
歌手页面的结构是 热门、 A-Z 的顺序排列,我们这里只抓取100条数据,观察其数据是乱序的,但我们可以利用数据的 Findex 进行数据的重构
1.首先可以定义一个 map 结构

let map = {
hot: {
title: HOT_NAME,
item: []
}
}

接着遍历得到的数据,将前10条添加到热门 hot 里
然后查看每条的 Findex ,如果 map[Findex] 没有,创建 map[Findex] push 进新条目,如果 map[Findex] 有,则向其 push 进新条目

list.forEach((item, index) => {
if (index < HOT_SINGER_LEN) {
map.hot.item.push(new SingerFormat({
id: item.Fsinger_mid,
name: item.Fsinger_name,
}))
}
const key = item.Findex
if (!map[key]) {
map[key] = {
title: key,
items: []
}
}
map[key].items.push(new SingerFormat({
id: item.Fsinger_mid,
name: item.Fsinger_name
}))
})

这样就得到了一个 符合我们基本预期的 map 结构,但是因为 map 是一个对象,数据是乱序的,Chrome 控制台在展示的时候会对 key 做排序,但实际上我们代码并没有做。
所以还要将其进行排序,这里会用到 数组的 sort 方法,所以我们要先把 map对象 转为 数组

let hot = []
let ret = []
let un = []
for (let key in map) {
let val = map[key]
if (val.title.match(/[a-zA-z]/)) {
ret.push(val)
} else if (val.title === HOT_NAME) {
hot.push(val)
} else {
un.push(val)
}
}
ret.sort((a, b) => {
return a.title.charCodeAt(0) - b.title.charCodeAt(0)
})
return hot.concat(ret, un)

这样就拿到一个类似[hot,a,b,c…….]这样符合需求的按规律排的数组。

2.2、锚点操作控制主区块列表
实现效果:点击或滑动 shortcut 不同的锚点 ,自动滚动至相应的标题列表
实现思维:获得每一次操作shortcut上对应的index,想办法通过index来设置左边区块的滚动值
1.如何获得index值
a.点击时候:循环的时候将给dom绑上data-index属性,写上当前index。点击时候通过DOM操作获取(e.target对象的getAttribute)
b.滑动时候:第一次点击触碰 shortcut ,记录触碰位置的 index,y坐标值,在touchmove事件中拿到第二次触碰shortcut的y坐标值y2,将两次触碰的位置的差值处理成索引上的 delta 差值,从而可以拿到第二次触碰的index值。
2.怎么通过index设置滚动值
利用BScroll的scrollToElement可以设置content滚动到某个DOM位置

scrollToElement() {
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
}

通过index值可以知道当前content应该滚动到第几个标题列表里面

_scrollTo(index) {
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) //listgroup代表左边标题区块
}

2.3、滑动主列表控制锚点
实现效果:滑动主列表,侧边 shortcut 自动高亮不同锚点
实现思维:实时监听主列表的滑动事件,得到每次滑动的值scrollY,设计一个listHeight数组存放每一个主列表到浏览器顶端的距离,比对scrollY存放在哪个listHeight的区间中得到currentIndex数值,这个currentIndex就对应着shortcut需高亮的锚点
1.scrollY的获取
a.获取滚动值:添加参数“listenScroll”来设置是否实时监听content的滚动事件,并向父组件派发事件scroll传出当前的滚动值:pos.y

if (this.listenScroll) {
let me = this
this.scroll.on('scroll', (pos) => { // 实时监测滚动事件,派发事件:Y轴距离
me.$emit('scroll', pos)
})
}

b.父组件监听到滚动派发的事件,并将值存入scrollY

@scroll="scroll" //template调用组件时绑定
scroll(pos) {
this.scrollY = pos.y // 实时获取 BScroll 滚动的 Y轴距离
}

2.listHeight的设计实现

_calculateHeight(){
const lists = this.$refs.listGroup //listGroup为主列表区块
let height = 0;
this.listHeight.push(height)
for(let i=0; i<lists.length; i++){
height += lists[i].clientHeight
this.listHeight.push(height)
}
},

3、通过实时watch scrollY的值,比对listHeight,拿到当前content滚动值落在哪个区间,也就拿到了currentIndex

scrollY(newY){ //获取的浏览器滚动的值均为负数
const listHeight = this.listHeight
if(newY > 0){ //当滚动到屏幕顶部时
newY = 0
return
}
for (let i = 0; i < listHeight.length - 1; i++){
let hei_1 = listHeight[i]
let hei_2 = listHeight[i+1]
if(-newY >= hei_1 && -newY < hei_2){
this.currentIndex = i; //currentIndex值是定义锚点高亮的值
this.diff = hei_2 + newY
return
}
}
},

ps:vue用法小记:watch 的 scrollY(newY){}
1.当我们在 Vue 里修改了在 data 里定义的变量,就会触发这个变量的 setter,经过vue的封装处理,会触发 watch 的回调函数,也就是 scrollY(newY) {} 这里的函数会执行,同时,newY 就是我们修改后的值。
2.scrollY 是定义在 data 里的,列表滚动的时候,scroll 事件的回调函数里有修改 this.scrollY,所以能 watch 到它的变化。

2.4 滚动固定标题
实现效果:主列表顶端固定一个标题,显示当前滚动的列表标题,标题改变时有transform上移效果
实现思维:固定标题fixedTitle拿到当前滚动数组中的title数据即可,在主列表的滚动事件中。设计一个diff值:存入每个区块的高度上限(也就是底部)减去 Y轴偏移的值,实时监听diff,当diff小于title块的高度时候,开始上移效果
1.fixedTitle获取

fixedTitle() { //computed中设置
if (this.scrollY > 0) {
return ''
}
return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ''
}

2.diff设置和监听

this.diff = hei_2 + newY //hei_2为即将滚动到下一个listGroup的高度
diff(newVal) {
let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0 ; //TITLE_HEIGHT 为title块的高度:30
if (this.fixedTop === fixedTop) { //设定this.fixedTop的值存入fixedTop值。若滚动过程中fixedTop没有发生变化就不进行transform设置。减少DOM操作,
return
}
this.fixedTop = fixedTop
this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)`
}

3、歌手详情页
歌手详情页是在歌手页singer跳转至二级路由页 singer-detail
index.js 路由里配置

{
path: '/singer',
component: Singer,
children: [
{
path: ':id', // 表示 id 为变量
component: SingerDetail
}
]
}

在singer页面里面跳转路由设定

selectSinger(singer){
this.$router.push({
path: `/singer/${singer.id}`
})
}

3.1、vuex
由于歌手详情页这个组件在app中有多次调用,这里设计到‘多个组件共享状态’的问题。故采用vuex状态管理,本项目简要介绍如下,具体移步 vuex:
通常的流程为:
- 定义 state,考虑项目需要的原始数据(最好为底层数据)
- getters,就是对原始数据的一层映射,可以只为底层数据做一个访问代理,也可以根据底层数据映射为新的计算数据(相当于 vuex 的计算属性)
- 修改数据:mutations,定义如何修改数据的逻辑(本质是函数),在定义 mutations 之前 要先定义 mutation-types
actions.js 通常是两种操作
- 异步操作
- 是对mutation的封装,比如一个动作需要触发多个mutation的时候,就可以把多个mutation封装到一个action中,达到调用一个action去修改多个mutation的目的。

3.1.1、歌手详情页关于vuex的设置
a.state.js:创建singer对象

const state = {
singer: {},
}
export default state

b.getter.js:对singer对象设置映射

export const singer = state => state.singer

c.mutation-types.js:设置singer可以修改的type值

export const SET_SINGER = 'SET_SINGER'

D.mutation.js:写入修改singer的函数

import * as types from './mutation-types'

const mutations = {
[types.SET_SINGER](state, singer){
state.singer = singer
}
}

export default mutations

e:index.js:实现vuex配置

import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'
import createLogger from 'vuex/dist/logger'
Vue.use(Vuex)
//非生产模式下开启debug
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
actions,
getters,
state,
mutations,
strict: debug,
plugins: debug ? [createLogger()] : []
})

3.2、利用vuex进行数据传递
实现效果:在singer页面点击进入singer-detai页面。传入歌手信息。显示歌手详情
实现思维:在singer组件跳转路由的时候,将当前点击的歌手信息写入singer的state状态中

首先 listview.vue 检测点击事件,将具体点击的歌手派发出去,以供父组件 singer 监听
selectItem(item) {
this.$emit('select', item) //item即为歌手数据
},

父组件监听事件执行 selectSinger(singer)
selectSinger(singer) {
this.$router.push({
path: `/singer/${singer.id}`
})
this.setSinger(singer)
},

...mapMutations({ /语法糖,'...'将多个对象注入当前对象
setSinger: 'SET_SINGER' // 将 this.setSinger() 映射为 this.$store.commit('SET_SINGER')
})

mapMutation为vuex提供的语法糖,获取所有的mutation
3. singer-detail 取 vuex 中存好的数据

computed: {
...mapGetters([ // 将 this.singer映射为 this.$store.getter.singer
'singer'
])
}

mapGetters为vuex提供的语法糖,获取所有的getters

3.3、music-list
实现效果:歌手详情页的歌单主要实现了歌单向上滚动时,这个歌单也跟着滚动上去,且背景图逐渐变得灰暗,歌单向下滚动时,背景图逐渐清晰,放大,
实现思维:主要是监听歌单的滚动事件,拿到各个变化的点
1、通过实时获取的scrollY与背景图片的比值。从而得到图片放大的比例,以及图片模糊的opacity值

scrollY(newY){
let translateY = Math.max(this.minTranslateY, newY) //设置minTranslateY值,用来限制歌单只能滚动到离顶部一段距离 this.minTranslateY = -this.imgHeight + RESERVE_HEIGHT(40)
let zIndex = 0 //滚动过程中需要有层级的切换
let scale = 1
let blur = 1
const percent = Math.abs(newY / this.imgHeight)
if (newY > 0) {
scale = 1 + percent
zIndex = 10
} else {
blur = Math.max(0.2, 1-percent)
}
this.$refs.bgLayer.style['transform'] = `translate3d(0, ${translateY}px,0)`
this.$refs.bgImage.style['opacity'] = `${blur}`
if(newY < translateY){
this.$refs.bgImage.style.paddingTop = 0
this.$refs.bgImage.style.height = `${RESERVE_HEIGHT}px`
zIndex = 10
} else {
this.$refs.bgImage.style.paddingTop = '70%'
this.$refs.bgImage.style.height = 0
}
this.$refs.bgImage.style['transform'] = `scale(${scale})`
this.$refs.bgImage.style.zIndex = zIndex
}

此操作中涉及到css的transform的设置。考虑到浏览器对其兼容性不一,封装一个prefixStyle自动加上浏览器对应的前缀

let elementStyle = document.createElement('div').style

let vendor = (() => {
let transformNames = {
webkit: 'webkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
ms: 'msTransform',
standard: 'transform'
}

for (let key in transformNames) {
if (elementStyle[transformNames[key]] !== undefined) return key
}
return false
})()

export function prefixStyle(style) {
if (vendor === false) return false
if (vendor === 'standard') return style
return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}

4、播放器 player组件
播放器是本次实战的难点及重点,把播放器组件放在 App.vue 下,因为它是一个跟任何路由都不相关的东西。在任何路由下,它都可以去播放。切换路由并不会影响播放器的播放。

4.1 vuex设计
由于点击 详情页,以及搜索等 都可以进行播放歌曲,且播放组件在哪一个路由下都存在,故对其相关状态进行vuex管理

playing: false, //当前是否正在播放
fullScreen: false, //全屏属性
playlist: [], //为实现下一首,上一首功能,保存当前播放列表
sequenceList: [], //当前按正常顺序的列表,列表还有一种随机列表
mode: playMode.sequence, //播放模式。其三种放到配置文件config中
currentIndex: -1 //当前歌曲的index

4.2 展开收起动画
全屏和底部之间的切换加上一些动画切换效果,引入插件‘create-keyframe-animation’
实现效果:该动画是点到点之间位移且慢慢放大的缩放效果
实现思维:获取两个点的位置,利用css3的translate3d属性进行位移和scale的设置,结合transition提供动画的四个钩子函数(@enter,@after-enter,@leave,@after-leave)以及插件create-keyframe-animation来做缓动的动画效果
1. 获取mini-player对应位置(x,y)以及scale

_getPosAndScale() {
const targetWidth = 40
const paddingLeft = 40
const paddingBottom = 30
const paddingTop = 80
const width = window.innerWidth * 0.8
const scale = targetWidth / width
const x = -(window.innerWidth / 2 - paddingLeft)
const y = window.innerHeight - paddingTop - width / 2 - paddingBottom
return {
x,y,scale
}
},

4.2 调用插件做动画
import animations from 'create-keyframe-animation' //引入

const {x, y, scale} = this._getPosAndScale() //在method里的enter()钩子中调用
let animation = {
0: {
transform: `translate3d(${x}px,${y}px,0) scale(${scale})`
},
60: {
transform: `translate3d(0,0,0) scale(1.1)`
},
100: {
transform: `translate3d(0,0,0) scale(1)`
}
}
animations.registerAnimation({
name: 'move',
animation,
presets: {
duration: 400,
easing: 'linear'
}
})
animations.runAnimation(this.$refs.cdWrapper, 'move', done)

4.3 切换播放模式
实现思维:播放模式三种:sequence(顺序播放)、loop(循环播放)、random(随机播放),默认是sequence,主要难点在与random播放,需要打乱播放列表。
打乱洗牌算法:遍历数组,且每次在0-数组长度 内获取一个随机数。将随机数的值与遍历值互换,

function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}

export function shuffle(arr) {
let _arr = arr.slice()
for (let i = 0; i < _arr.length; i++) {
let j = getRandomInt(0, i)
let t = _arr[i]
_arr[i] = _arr[j]
_arr[j] = t
}
return _arr
}

当打乱了播放数组后。为了要保持当前播放的歌曲不变。那么currentIndex也要相应改变

_resetCurrentIndex(list) { //这里传入打乱的list
let index = list.findIndex((item) => {
return item.id == this.currentSong.id
})
this.setCurrentIndex(index)
},

4.4 歌词
获取歌词
获取歌词的接口也需要绕过refer,用axios服务器去拿qq的服务器,其设置接口如下:
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query
}).then((response) => { //ps:这里返回的response依然为jsonp格式,故需要对数据处理成json数据
let ret = response.data
if (typeof ret === 'string') {
const reg = /^\w+\(({.+})\)$/
const matches = ret.match(reg)
if (matches) {
ret = JSON.parse(matches[1])
}
}
res.json(ret)
})

获取到的歌词是base64格式,引入js-base64库对其进行解码
2. 歌词滚动
当前歌曲的歌词高亮是利用 js-lyric 会派发的 handle 事件

this.currentLyric = new Lyric(lyric, this.handleLyric)

js-lyric 会在每次改变当前歌词时触发这个函数,函数t提供的参数为:当前歌词的 lineNum 和 txt
为了当前高亮歌词保持最中间 是利用了 BScroll 滚动至高亮的歌词

handleLyric({lineNum, txt}) {
this.currentLineNum = lineNum
if(lineNum > 5){
this.$refs.lyricList.scrollToElement(this.$refs.lyricLine[lineNum - 5],0,1000) //lyricLine代表每一句歌词 ,lyricList 为包裹歌词的content
}else{
this.$refs.lyricList.scrollTo(0, 0, 1000)
}
},

4.5 运用mixins
由于底部可能是会有播放min-player,导致页面的滚动区域少了60px,这是一个公共的问题。在其他很多页面都会出现。故选择使用mixins处理机制
1.实现思路: handlePlaylist方法在调用页面处理程序实现滚动区域的bottom值,设置content的bottom。然后进行刷新scroll。在mixin文件中也设置一个handlePlaylist方法。当调用mixin的页面没有handlePlaylist方法时候。就会这行mixin里面得handlePlaylist进行报错。

mounted() {
this.handlePlaylist(this.playlist)
},
activated() {
this.handlePlaylist(this.playlist)
},
watch: {
playlist(newVal) {
this.handlePlaylist(newVal)
}
},
methods: {
handlePlaylist() {
throw new Error('component must implement handlePlaylist method')
}
}

调用mixins页面的handlePlaylist方法

handlePlaylist(playList) {
const bottom = playList.length > 0 ? '60px' : '0'
this.$refs.singer.style.bottom = bottom
this.$refs.list.refresh()
},

5、search页面
5.1 搜索结果上拉加载
上拉加载数据需要根据接口的参数设置来一页页拿数据,该接口的参数设计如下

w: query //搜索内容
p: page //拿搜索的第几页数据了
perpage:perpage //每一页返回的数据条数
catZhida:zhida ? 1 : 0 //是否进行搜索歌手

实现关键代码
1. 拓展scroll组件,加上pullup属性来监听是否滑到了底部,滑到了底部就派发scrollToEnd事件

this.scroll.on('scrollEnd', () => {
if(this.scroll.y <= (this.scroll.maxScrollY + 50)){ //增加50的buffer
this.$emit('scrollToEnd')
}
})

监听scrollToEnd事件,执行加载更多searchMore事件,并在每次调用search接口的时候通过传回来的数据判断是否还有下一页数据
//调用seacrh接口
search(query) {
search(query, this.page, this.showSinger, perpage).then((res) => {
if(res.code === ERR_OK){
this.result = this.result.concat(this._getResult(res.data))
this._checkMore(res.data)
}
})
},
//并判断是否有下一页数据
_checkMore(data) {
const song = data.song
if (!song.list.length || (song.curnum + song.curpage * perpage) >= song.totalnum) {
this.hasMore = false
}
},
//每次搜索时候。初次调用search
searchFirst(){
this.page = 1
this.result = []
this.hasMore = true
this.search(this.query)
},
//下拉加载更多
searchMore(){
if(!this.hasMore) {
return
}
this.page++
this.search(this.query)
},

5.2 搜索历史
将每次搜索的关键词存入搜索历史,且页面刷新了,搜索历史还在,搜索历史列表数量固定在15个以内,且排在第一个的必须是最新搜索的关键词,还有对搜索历史的单个删除和批量删除作用。
实现关键代码
1. 引入good-storage库,该库对localstorage进行简单的封装。可以直接存入数组。提供了set,get方法
2. 存入关键词,由于要多次调用这个方法,故封装一个insertArray方法

function insertArray(arr, val, compare, maxLen) {
const index = arr.findIndex(compare)
if(index === 0){ //传入搜索词在第一个位置。arr不做任何变化
return
}
if(index > 0){ //传入搜索词存在且不是第一个位置,先删掉旧的位置上的数据,再把搜索词插入数组头部
arr.splice(index, 1)
}
arr.unshift(val)
if (maxLen && arr.length > maxLen) { //最大值限定
arr.pop()
}
}
//将插入后的新数组写入localstorage,并返回新数组供外部调用给vuex状态管理
export function saveSearch(query){
let searches = storage.get(SEARCH_KEY, [])
insertArray(searches, query, (item) => {
return item === query
},SEARCH_MAX_LEN)
storage.set(SEARCH_KEY, searches)
return searches
}

批量删除,作为一个比较严重的“大操作”,这里应需要提示用户是否确认真的要删除全部,故加上一个“确认框”。
methods: {
hide() { //往外提供show,hide方法供调用是否显示该确认框
this.showFlag = false
},
show() {
this.showFlag = true
},
confirm() { //派发出确认(confirm)以及取消(cancel)事件
this.hide()
this.$emit('confirm')
},
cancel() {
this.hide()
this.$emit('cancel')
}
}
}

六、编译打包上线
将项目通过npm run build 编译成一个dist文件目录,可以搭一个简单的node服务器跑起来。可以采用express框架结合axios。
配置prod.server.js

对项目的优化:对组件进行异步加载处理

const UserCenter = (resolve) => {
import('components/user-center/user-center').then((module) => {
resolve(module)
})
}

移动端调试利器:vconsole
项目github:https://github.com/caoyanyuan/vue-player

七 、疑难总结 & 小技巧
6.1.关于 Vue
v-html可以转义字符。处理某些带有html的数据
watch 对象可以得到某个属性每次变化的新值
created()里面定义的数据只是初始化。而不会像data()那样给数据添加getter()和setter()方法从而监测数据变化。
mounted 是先触发子组件的 mounted,再会触发父组件的 mounted,但是对于 created 钩子,又会先触发父组件,再触发子组件。
如果组件有计数器,在组件销毁时期要记得清理,
对于 Vue 组件,this.refs.xxx拿到的是Vue实例,所以需要再通过refs.xxx拿到的是Vue实例,所以需要再通过el 拿到真实的 dom

6.2关于 JS
setTimeout(fn, 20)
一般来说 JS 线程执行完毕后一个 Tick 的时间约17ms内 DOM 就可以渲染完毕所以课程中 setTimeout(fn, 20) 是非常稳妥的写法
audio 提供的API
<audio ref="audio" src=“” @play="ready" @error="error"@timeupdate="updateTime" @ended="end"></audio>

@play :当src资源拿到了之后执行的事件
使用场景:songReady作为一首歌可以播放的标志位,可以解决”如果未拿到歌曲资源,就进行播放造成的DOM报错“

@play=‘ready’
ready() {
this.songReady = true
}

@timeupdate:拿到当前歌曲的播放时间
使用场景:获取当前时间来做对应的滚动条进度显示

@timeupdate="updateTime"
updateTime(e) {
this.currentTime = e.target.currentTime
},

currentTime:属性,拿到或者设置当前播放到的时间点
使用场景:currentTime为进度条拖拽到一个点,对应这个点的时间刻。将currentTime设置给audio,即可把歌曲对应播放到这个时间刻

this.$refs.audio.currentTime = currentTime //设置

@ended:歌曲播放结束后发生的事件
使用场景:播放完了、进行播放到下一首,或是如果在循环单曲模式下,继续循环这一首

end() {
if (this.mode === playMode.loop) {
this.loop()
} else {
this.next()
}
},

6.3 关于 webpack
” ~ ” 使 SCSS 可以使用 webpack 的相对路径
@import “~common/scss/mixin”;
@import “~common/scss/variable”;
babel-runtime 会在编译阶段把 es6 语法编译的代码打包到业务代码中,所以要放在dependencies里。
Fast Click 是一个简单、易用的库,专为消除移动端浏览器从物理触摸到触发点击事件之间的300ms延时
为什么会存在延迟呢?
从触摸按钮到触发点击事件,移动端浏览器会等待接近300ms,原因是浏览器会等待以确定你是否执行双击事件
何时不需要使用FastClick
FastClick 不会伴随监听任何桌面浏览器
Android 系统中,在头部 meta 中设置 width=device-width 的Chrome32+ 浏览器不存在300ms 延时,所以,也不需要
<meta name="viewport" content="width=device-width, initial-scale=1">

同样的情况也适用于 Android设备(任何版本),在viewport 中设置 user-scalable=no,但这样就禁止缩放网页了
IE11+ 浏览器中,你可以使用 touch-action: manipulation; 禁止通过双击来放大一些元素(比如:链接和按钮)。IE10可以使用 -ms-touch-action: manipulation

————————————————
版权声明:本文为CSDN博主「cyyeel」的原创文章。
原文链接:https://blog.csdn.net/Forever201295/article/details/80266600

vue实现音乐播放器实战笔记的更多相关文章

  1. Qt+MPlayer音乐播放器开发笔记(一):ubuntu上编译MPlayer以及Demo演示

    前言   在ubuntu上实现MPlayer播放器播放音乐.   Demo                Mplayer   MPlayer是一款开源多媒体播放器,以GNU通用公共许可证发布.此款软件 ...

  2. Vue中音乐播放器

    一.安装依赖 yarn add vue-aplayer ​ or` npm i vue-aplayer 二.使用 <template> <div class="vue_ap ...

  3. Vue实现音乐播放器(七):轮播图组件(二)

    轮播图组件 <template> <div class="slider" ref="slider"> <div class=&qu ...

  4. Vue实现音乐播放器(六):jsonp的应用+抓取轮播图数据

    用jsonp来获取数据   通过封装方法来获取 在src文件夹下的api文件夹里面去封装一些获取相关部分组件的数据的方法 在api文件夹下的recommend.js中 配置一下公共参数 请求的真实的u ...

  5. Vue实现音乐播放器(四):页面入口+header组件的编写

    首先下载三个包 babel-runtime对es语法进行转义 fastclick解决移动端点击300毫秒延迟的问题 babel-polyfill对es6 api进行转义 下载了包之后要在main.js ...

  6. Vue实现音乐播放器(二)-Vue-cli脚手架安装

  7. 一个基于H5audio标签的vue音乐播放器

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. 02 Vue介绍与安装,指令系统 v-*、音乐播放器

    VUE文档 https://cn.vuejs.org/v2/guide/ 1.vue的介绍 尤雨溪 1.vue的优点 2.vue的介绍 3.vue的安装 4.声明式渲染 <body> &l ...

  9. vue音乐播放器

    利用vue写一个简单的音乐播放器,包括功能有歌曲搜索.歌曲播放.歌曲封面.歌曲评论.播放动画.mv播放六个功能. <template> <div class="wrap&q ...

随机推荐

  1. 4. css事件

    可通过使用css伪类实现点击元素变色的效果,两个伪类是:active, :focus :active :active选择器用于选择活动链接.当在一个链接上点击时,它就会成为活动的(激活的),:acti ...

  2. eclipse添加方法注释

    打开注释模板编辑窗口:Window ->Preferences->java -> Code Style -> Code Template->Comments type 设 ...

  3. 【启蒙】C笔记之初学阶段(下篇)

    下篇继续点赞,谢谢老铁,不存在下次一定的哈! c语言简单判断质数的方法 int isprime(int a){ ) ; ==||a==||a==) ; else { ;i<=sqrt(a);i+ ...

  4. Java 多线程 --死锁及解决方案

    在java 多线程中 过多的同步造成相互不释放资源 从而相互等待,造成死锁线现象,一般发生于同步中持有多个对象锁 如以下代码: public class DeadLock { public stati ...

  5. jeecg ant design vue一级菜单跳到外部页面——例如跳到百度

    需求:点击首页跳到百度新打开的页面 找到SideMenu.vue   对应的inde.js找到renderMenuItem 函数.加一个判断 if(menu.meta.url=='https://ww ...

  6. 什么是LVM

    LVM是逻辑盘卷管理(Logical Volume Manager)的简称,它是Linux环境下对磁盘分区进行管理的一种机制,LVM是建立在硬盘和分区之上的一个逻辑层,来提高磁盘分区管理的灵活性.前面 ...

  7. mysql-管理命令【创建用户、授权、修改密码、删除用户和授权、忘记root密码】

    一.创建用户 命令: CREATE USER 'username'@'host' IDENTIFIED BY 'password'; 关键参数说明: username - 创建登录用户名, host ...

  8. 【集群实战】NFS网络文件共享服务

    1. NFS介绍 1.1 什么是NFS? NFS是Network File System的缩写,中文意思是网络文件系统. 它的主要功能是通过网络(一般是局域网)让不同的主机系统之间可以共享文件或目录. ...

  9. ASP.NET Core 找不到 npm指令异常

    1.错误再现 利用VS2019预览版创建ASP.NET Core 的单页面Web程序 创建后直接运行,出现如下错误 Ensure that 'npm' is installed and can be ...

  10. 数学--数论-- HDU6298 Maximum Multiple 打表找规律

    Given an integer nn, Chiaki would like to find three positive integers xx, yy and zzsuch that: n=x+y ...