【音乐App】—— Vue-music 项目学习笔记:播放器内置组件开发(二)
前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。
播放模式切换 | 歌词滚动显示 |
一、播放器模式切换功能实现 |
按钮样式随模式改变而改变
- 动态绑定iconMode图标class:
<i :class="iconMode"></i>
import {playMode} from '@/common/js/config' iconMode(){
return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random'
} - 给按钮添加点击事件,通过mapGetters获取mode,通过mapMutaions修改:
<div class="icon i-left" @click="changeMode">
changeMode(){
const mode = (this.mode + 1) % 3
this.setPlayMode(mode)
} setPlayMode: 'SET_PLAY_MODE'
播放列表顺序随模式改变而改变
- common->js目录下:创建util.js,提供工具函数
function getRandomInt(min, max){
return Math.floor(Math.random() * (max - min + 1) + min)
} //洗牌: 遍历arr, 从0-i 之间随机取一个数j,使arr[i]与arr[j]互换
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
} - 通过mapGetters获取sequenceList,在changeMode()中判断mode,通过mapMutations修改playlist
changeMode(){
const mode = (this.mode + 1) % 3
this.setPlayMode(mode)
let list = null
if(mode === playMode.random){
list = shuffle(this.sequenceList)
}else{
list = this.sequenceList
} this.resetCurrentIndex(list)
this.setPlayList(list)
}
播放列表顺序改变后当前播放歌曲状态不变
- findIndex找到当前歌曲id值index,通过mapMutations改变currentIndex,保证当前歌曲的id不变
resetCurrentIndex(list){
let index = list.findIndex((item) => { //es6语法 findIndex
return item.id === this.currentSong.id
})
this.setCurrentIndex(index)
} - 坑:CurrentSong发生了改变,会触发watch中监听的操作,如果当前播放暂停,改变模式会自动播放
- 解决:添加判断,如果当前歌曲的id不变,认为CurrentSong没变,不执行任何操作
currentSong(newSong, oldSong) {
if(newSong.id === oldSong.id) {
return
}
this.$nextTick(() => { //确保DOM已存在
this.$refs.audio.play()
})
}
当前歌曲播放完毕时自动切换到下一首或重新播放
- 监听audio派发的ended事件:@ended="end"
end(){
if(this.mode === playMode.loop){
this.loop()
}else{
this.next()
}
},
loop(){
this.$refs.audio.currentTime = 0
this.$refs.audio.play()
}
“随机播放全部”按钮功能实现
- music-list.vue中给按钮监听点击事件
@click="random"
- actions.js中添加randomPlay action
import {playMode} from '@/common/js/config'
import {shuffle} from '@/common/js/util' export const randomPlay = function ({commit},{list}){
commit(types.SET_PLAY_MODE, playMode.random)
commit(types.SET_SEQUENCE_LIST, list)
let randomList = shuffle(list)
commit(types.SET_PLAYLIST, randomList)
commit(types.SET_CURRENT_INDEX, 0)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
} - music-list.vue中定义random方法应用randomPlay
random(){
this.randomPlay({
list: this.songs
})
} ...mapActions([
'selectPlay',
'randomPlay'
]) - 坑:当点击了“随机播放全部”之后,再选择歌曲列表中指定的一首歌,播放的不是所选择的歌曲
- 原因:切换了随机播放之后,当前播放列表的顺序就不是歌曲列表的顺序了,但选择歌曲时传给currentIndex的index还是歌曲列表的index
- 解决:在actions.js中的selectPlay action中添加判断,如果是随机播放模式,将歌曲洗牌后存入播放列表,找到当前选择歌曲在播放列表中的index再传给currentIndex
function findIndex(list, song){
return list.findIndex((item) => {
return item.id === song.id
})
} export const selectPlay = function ({commit, state}, {list, index}) {
//commit方法提交mutation
commit(types.SET_SEQUENCE_LIST, list)
if(state.mode === playMode.random) {
let randomList = shuffle(list)
commit(types.SET_PLAYLIST, randomList)
index = findIndex(randomList, list[index])
}else{
commit(types.SET_PLAYLIST, list)
}
commit(types.SET_CURRENT_INDEX, index)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
}
二、播放器歌词数据抓取 |
- src->api目录下:创建song.js
import {commonParams} from './config'
import axios from 'axios' export function getLyric(mid){
const url = '/api/lyric' const data = Object.assign({}, commonParams, {
songmid: mid,
pcachetime: +new Date(),
platform: 'yqq',
hostUin: 0,
needNewCode: 0,
g_tk: 5381, //会变化,以实时数据为准
format: 'json' //规定为json请求
}) return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
} - webpack.dev.config.js中通过node强制改变请求头
app.get('/api/lyric', function(req, res){
var url="https://szc.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.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)
})
}) - common->js->song.js中将获取数据的方法封装到class类
getLyric() {
getLyric(this.mid).then((res) => {
if(res.retcode === ERR_OK){
this.lyric = res.lyric
//console.log(this.lyric)
}
})
} - player.vue中调用getLyric()测试
currentSong(newSong, oldSong) {
if(newSong.id === oldSong.id) {
return
}
this.$nextTick(() => { //确保DOM已存在
this.$refs.audio.play()
this.currentSong.getLyric()//测试
})
}因为请求后QQ返回的仍然是一个jsonp, 需要在后端中做一点处理
- webpack.dev.config.js中通过正则表达式,将接收到的jsonp文件转换为json格式
var ret = response.data
if (typeof ret === 'string') {
var reg = /^\w+\(({[^()]+})\)$/
// 以单词a-z,A-Z开头,一个或多个
// \(\)转义括号以()开头结尾
// ()是用来分组
// 【^()】不以左括号/右括号的字符+多个
// {}大括号也要匹配到
var matches = ret.match(reg)
if (matches) {
ret = JSON.parse(matches[1])
// 对匹配到的分组的内容进行转换
}
}
res.json(ret)注意:后端配置后都需要重新启动!!!
三、播放器歌词数据解析 |
- js-base64 code解码
- 安装js-base64依赖:
npm install js-base64 --save
- common->js->song.js中:
import {Base64} from 'js-base64'
this.lyric = Base64.decode(res.lyric)//解码 得到字符串
- 解析字符串
- 安装 第三方库 lyric-parser:
npm install lyric-parser --save
- 优化getLyric:如果已经有歌词,不再请求
getLyric() {
if(this.lyric){
return Promise.resolve()
} return new Promise((resolve, reject) => {
getLyric(this.mid).then((res) => {
if(res.retcode === ERR_OK){
this.lyric = Base64.decode(res.lyric)//解码 得到字符串
// console.log(this.lyric)
resolve(this.lyric)
}else{
reject('no lyric')
}
})
})
}
- player.vue中使用lyric-parser,并在data中维护一个数据currentLyric
import Lyric from 'lyric-parser' //获取解析后的歌词
getLyric() {
this.currentSong.getLyric().then((lyric) => {
this.currentLyric = new Lyric(lyric)//实例化lyric对象
console.log(this.currentLyric)
})
}在watch的currentSong()中调用:this.getLyric()
四、播放器歌词滚动列表实现 |
显示歌词
- player.vue中添加DOM结构
<div class="middle-r" ref="lyricList">
<div class="lyric-wrapper">
<div v-if="currentLyric">
<p ref="lyricLine"
class="text"
v-for="(line, index) in currentLyric.lines" :key="index"
:class="{'current': currentLineNum === index}">
{{line.txt}}
</p>
</div>
</div>
</div>
歌词随歌曲播放高亮显示
- 在data中维护数据
currentLineNum: 0
- 初始化lyric对象时传入handleLyric方法,得到当前currentLingNum值,判断如果歌曲播放,调用Lyric的play()
//获取解析后的歌词
getLyric() {
this.currentSong.getLyric().then((lyric) => {
//实例化lyric对象
this.currentLyric = new Lyric(lyric, this.handleLyric)
// console.log(this.currentLyric)
if(this.playing){
this.currentLyric.play()
}
})
},
handleLyric({lineNum, txt}){
this.currentLineNum = lineNum
}
- 动态绑定current样式,高亮显示index为currentLineNum值的歌词
:class="{'current': currentLineNum === index}"
歌词实现滚动,歌曲播放时当前歌词滚动到中间显示
- 引用并注册scroll组件
import Scroll from '@/base/scroll/scroll'
- 使用<scroll>替换<div>,同时传入currentLyric和currentLyric.lines作为data
<scroll class="middle-r" ref="lyricList" :data="currentLyric && currentLyric.lines">
- 在handleLyric()中添加判断
- 当歌词lineNum大于5时,触发滚动,滚动到当前元素往前偏移第5个的位置;否则滚动到顶部
handleLyric({lineNum, txt}){
this.currentLineNum = lineNum
if(lineNum > 5){
let lineEl = this.$refs.lyricLine[lineNum - 5] //保证歌词在中间位置滚动
this.$refs.lyricList.scrollToElement(lineEl, 1000)
}else{
this.$refs.lyricList.scrollTo(0, 0, 1000)//滚动到顶部
}
} - 此时,如果手动将歌词滚动到其它位置,歌曲播放的当前歌词还是会滚动到中间
五、播放器歌词左右滑动 |
需求:两个点按钮对应CD页面和歌词页面,可切换
- 实现:data中维护数据currentShow,动态绑定active class:
currentShow: 'cd'
<div class="dot-wrapper">
<span class="dot" :class="{'active': currentShow === 'cd'}"></span>
<span class="dot" :class="{'active': currentShow === 'lyric'}"></span>
</div>
需求:切换歌词页面时,歌词向左滑,CD有一个渐隐效果;反之右滑,CD渐现
- 实现:【移动端滑动套路】—— touchstart、touchmove、touchend事件 touch空对象
- created()中创建touch空对象:因为touch只存取数据,不需要添加gettter和setter监听
created(){
this.touch = {}
} - <div class="middle">绑定touch事件:一定记得阻止浏览器默认事件
<div class="middle" @touchstart.prevent="middleTouchStart"
@touchmove.prevent="middleTouchMove"
@touchend="middleTouchEnd"> - 实现touch事件的回调函数:touchstart和touchmove的回调函数中要传入event,touchstart中定义初始化标志位initiated
//歌词滑动
middleTouchStart(e){
this.touch.initiated = true //初始化标志位
const touch = e.touches[0]
this.touch.startX = touch.pageX
this.touch.startY = touch.pageY
},
middleTouchMove(e){
if(!this.touch.initiated){
return
}
const touch = e.touches[0]
const deltaX = touch.pageX - this.touch.startX
const deltaY = touch.pageY - this.touch.startY
//维护deltaY原因:歌词本身Y轴滚动,当|deltaY| > |deltaX|时,不滑动歌词
if(Math.abs(deltaY) > Math.abs(deltaX)){
return
}
const left = this.currentShow === 'cd' ? 0 : -window.innerWidth
const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX))
this.touch.percent = Math.abs(offsetWidth / window.innerWidth) //滑入歌词offsetWidth = 0 + deltaX(负值) 歌词滑出offsetWidth = -innerWidth + delta(正值)
this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px, 0, 0)`
this.$refs.lyricList.$el.style[transitionDuration] = 0
this.$refs.middleL.style.opacity = 1 - this.touch.percent //透明度随percent改变
this.$refs.middleL.style[transitionDuration] = 0
},
middleTouchEnd(){
//优化:手动滑入滑出10%时,歌词自动滑过
let offsetWidth
let opacity
if(this.currentShow === 'cd'){
if(this.touch.percent > 0.1){
offsetWidth = -window.innerWidth
opacity = 0
this.currentShow = 'lyric'
}else{
offsetWidth = 0
opacity = 1
}
}else{
if(this.touch.percent < 0.9){
offsetWidth = 0
opacity = 1
this.currentShow = 'cd'
}else{
offsetWidth = -window.innerWidth
opacity = 0
}
}
const time = 300
this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px, 0, 0)`
this.$refs.lyricList.$el.style[transitionDuration] = `${time}ms`
this.$refs.middleL.style.opacity = opacity
this.$refs.middleL.style[transitionDuration] = `${time}ms`
}
- 坑:
- 使用 <scroll class="middle-r" ref="lyricList">的引用改变其style是:this.$refs.lyricList.$el.style
- 使用 <div class="middle-l" ref="middleL">的引用改变其style是:this.$refs.middleL.style
六、播放器歌词剩余功能 |
- 坑:切换歌曲后,歌词会闪动
- 原因:每次都会重新实例化Layric,但前一首的Layric中的定时器还在,造成干扰
- 解决:在Watch的currentSong()中添加判断,切换歌曲后,如果实例化新的Layric之前有currentLyric,清空其中的定时器
if(this.currentLyric){
this.currentLyric.stop() //切换歌曲后,清空前一首歌歌词Layric实例中的定时器
} - 坑:歌曲暂停播放后,歌词还会继续跳动,并没有被暂停
- 解决:在togglePlaying()中判断如果存在currentLyric,就调用currentLyric的togglePlay()切换歌词的播放暂停
if(this.currentLyric){
this.currentLyric.togglePlay()//歌词切换播放暂停
}
- 坑:单曲循环播放模式下,歌曲播放完毕后,歌词并没有返回到一开始
- 解决:在loop()中判断如果存在currentLyric,就调用currentLyric的seek()将歌词偏移到最开始
if(this.currentLyric){
this.currentLyric.seek(0) //歌词偏移到一开始
}
- 坑:拖动进度条改变歌曲播放进度后,歌词没有随之改变到对应位置
- 解决:在onProgressBarChange()中判断如果存在currentLyric,就调用seek()将歌词偏移到currentTime*1000位置处
const currentTime = this.currentSong.duration * percent if(this.currentLyric){
this.currentLyric.seek(currentTime * 1000)//偏移歌词到拖动时间的对应位置
} - 需求:CD页展示当前播放的歌词
- 添加DOM结构:
<div class="playing-lyric-wrapper">
<div class="playing-lyric">{{playingLyric}}</div>
</div> data中维护数据
playingLyric: ''
- 在回调函数handleLyric()中改变当前歌词:
this.playingLyric = txt
- 考虑异常情况:如果getLyric()请求失败,做一些清理的操作
getLyric() {
this.currentSong.getLyric().then((lyric) => {
//实例化lyric对象
this.currentLyric = new Lyric(lyric, this.handleLyric)
// console.log(this.currentLyric)
if(this.playing){
this.currentLyric.play()
}
}).catch(() => {
//请求失败,清理数据
this.currentLyric = null
this.playingLyric = ''
this.currentLineNum = 0
})
} - 考虑特殊情况:如果播放列表只有一首歌,next()中添加判断,使歌曲单曲循环播放;prev()同理
next() {
if(!this.songReady){
return
}
if(this.playlist.length === 1){ //只有一首歌,单曲循环
this.loop()
}else{
let index = this.currentIndex + 1
if(index === this.playlist.length){
index = 0
}
this.setCurrentIndex(index)
if(!this.playing){
this.togglePlaying()
}
this.songReady = false
}
} - 优化:因为手机微信运行时从后台切换到前台时不执行js,要保证歌曲重新播放,使用setTimeout替换nextTick
setTimeout(() => { //确保DOM已存在
this.$refs.audio.play()
// this.currentSong.getLyric()//测试歌词
this.getLyric()
}, 1000)
七、播放器底部播放器适配+mixin的应用 |
- 问题:播放器收缩为mini-player之后,播放器占据列表后的一定空间,导致BScroll计算的高度不对,滚动区域受到影响
- mixin的适用情况:当多种组件都需要一种相同的逻辑时,引用mixin处可以将其中的代码添加到组件中
mixin详解
——转载自【木子墨博客】 |
- common->js目录下:创建mixin.js
import {mapGetters} from 'vuex' export const playlistMixin = {
computed:{
...mapGetters([
'playlist'
])
},
mounted() {
this.handlePlaylist(this.playlist)
},
activated() { //<keep-alive>组件切换过来时会触发activated
this.handlePlaylist(this.playlist)
},
watch:{
playlist(newVal){
this.handlePlaylist(newVal)
}
},
methods: { //组件中定义handlePlaylist,就会覆盖这个,否则就会抛出异常
handlePlaylist(){
throw new Error('component must implement handlePlaylist method')
}
}
} - music-list.vue中应用mixin
import {playlistMixin} from '@/common/js/mixin'
mixins: [playlistMixin]定义handlePlaylist方法,判断如果有playlist,改变改变list的bottom并强制scroll重新计算
handlePlaylist(playlist){
const bottom = playlist.length > 0 ? '60px' : ''
this.$refs.list.$el.style.bottom = bottom //底部播放器适配
this.$refs.list.refresh() //强制scroll重新计算
}
- singer.vue中同上:需要在listview.vue中暴露一个refresh方法后,再在singer.vue中调用
refresh() {
this.$refs.listview.refresh()
} handlePlaylist(playlist) {
const bottom = playlist.length > 0 ? '60px' : ''
this.$refs.singer.style.bottom = bottom //底部播放器适配
this.$refs.list.refresh() //强制scroll重新计算
} - recommend.vue中同上:
handlePlaylist(playlist){
const bottom = playlist.length > 0 ? '60px' : ''
this.$refs.recommend.style.bottom = bottom //底部播放器适配
this.$refs.scroll.refresh() //强制scroll重新计算
}
注:项目来自慕课网
【音乐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/66We ...
- 【音乐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. 歌曲列表 收藏歌曲 一.用 ...
- H5播放器内置播放视频(兼容绝大多数安卓和ios)
关于H5播放器内置播放视频,这个问题一直困扰我很长一段时间,qq以前提供白名单已经关闭,后来提供了同层属性的控制,或多或少也有点差强人意. 后来一次偶然发现一个非常简单的方法可以实现. 只需要给vid ...
随机推荐
- camelot工具进行pdf表格解析重建
camelot内置生成html文件的方法,但表格数据转化成pandas.dataframe的过程中,丢失了跨行跨列的结构信息,故生成html的表格无跨行跨列结构. 于是我在输出部分选择直接手写html ...
- hadoop配置文件: hdfs-site.xml, mapred-site.xml
dfs.name.dir Determines where on the local filesystem the DFS name node should store the name table( ...
- [ZJOI2011][bzoj2229] 最小割 [最小割树]
题面 传送门 思路 首先我们明确一点:这道题不是让你把$n^2$个最小割跑一遍[废话] 但是最小割过程是必要的,因为最小割并没有别的效率更高的算法(Stoer-Wagner之类的?) 那我们就要尽量找 ...
- [bzoj4259][bzoj4503] 残缺的字符串 [FFT]
题面 传送门 bzoj上的这两题是一样的...... 正文 我看到这道题,第一想法是跑魔改过的KMP,然后很快发现不可行 于是想换个角度思考 其实,本题最大的问题就在于通配符的存在:它可以匹配任意一个 ...
- 安装淘宝内核LVS
具体安装方法按照淘宝twiki来:http://kernel.taobao.org/index.php?title=Documents/Kernel_build. 但是有些问题是要注意的: 1. 修改 ...
- 深入比较选择 Angular 还是 React
我应该选择 Angular 还是 React?现在JS框架两强的格局让许多开发者选择起来很纠结.无论你是一个正在思考如何入门的新手,还是一个为下个项目挑选框架的设计者,或是一个架构师为公司做长远的规划 ...
- 大陆争霸(bzoj 1922)
Description 在一个遥远的世界里有两个国家:位于大陆西端的杰森国和位于大陆东端的 克里斯国.两个国家的人民分别信仰两个对立的神:杰森国信仰象征黑暗和毁灭 的神曾·布拉泽,而克里斯国信仰象征光 ...
- 洛谷 [P2577] 午餐
DP + 贪心 我们发现,如果只有一个窗口,贪心即可解决,吃饭时间长的人一定要先打饭 有两个窗口的时候,这条性质依然满足,但是两个窗口如何分配,需要 01 背包 #include <iostre ...
- wap提交表单
原文发布时间为:2010-08-31 -- 来源于本人的百度文章 [由搬家工具导入] <%@ page contentType="text/vnd.wap.wml" page ...
- 用IHTMLDocument2接口获取页面上想要的数据,代替正则表达式
原文发布时间为:2010-07-01 -- 来源于本人的百度文章 [由搬家工具导入] 1. 用 IHTMLDocument2::all 获得所有元素; 2. 用 IHTMLElementCollect ...