VUE移动端音乐APP学习【十六】:播放器歌词显示开发

小风车吱呀转 2021-05-03 22:48:03
学习 vue app 移动 音乐


播放器歌词数据抓取

在api文件夹下创建song.js 设置获取歌曲api

import axios from 'axios';
export function getLyric(id) {
return axios.get(`/api/lyric?id=${id}`);
}

把这个方法封装到common->js->下的song类,歌词可以理解为song的一个属性。不能直接拿歌词需要调用这个接口,所以给song扩展一个方法getLyric

import { getLyric } from '../../api/song';
import { ERR_OK } from '../../api/config';
export default class Song {
// song的id,歌手,歌曲名name,专辑名album,歌曲长度duration,歌曲图片img,歌曲的真实路径url
 constructor({
id, singer, name, album, duration, image, url,
}) {
this.id = id;
this.singer = singer;
this.name = name;
this.album = album;
this.duration = duration;
this.image = image;
this.url = url;
}
getLyric() {
getLyric(this.id).then((res) => {
if (res.data.code === ERR_OK) {
this.lyric = res.data.lrc.lyric;
console.log(this.lyric);
}
});
}
}

在player组件的watch里调用看看能不能正确获取到歌词

 

 watch: {
currentSong(newSong, oldSong) {
if (newSong.id === oldSong.id) {
return;
}
this.$nextTick(() => {
this.$refs.audio.play().catch((error) => {
this.togglePlaying();
// eslint-disable-next-line no-alert
alert('播放出错,暂无该歌曲资源');
});
this.currentSong.getLyric();
});
},

 

播放器歌词数据解析

可以看到歌词是非常长的字符串。接下来就是解析字符串。利用第三方库 lyric-parser ,它支持传入lyricStrhandler,歌词在不断播放的时候,每执行到一个时间点都会执行handler函数

 

npm install lyric-parser@1.0.1

 

在数据解析之前需要优化一个地方:currentSong每次变化的时候都会调用song的getLyric方法,则会有很多次请求,这样是不合理的。

所以要加个逻辑判断并利用Promise进行改造

getLyric() {
if (this.lyric) {
// getLyric本身返回的就是Promise
return Promise.resolve(this.lyric);
}
// 封装Promise,只用于获取歌词
return new Promise((resolve, reject) => {
getLyric(this.id).then((res) => {
if (res.data.code === ERR_OK) {
this.lyric = res.data.lrc.lyric;
resolve(this.lyric);
} else {
// 获取不到歌词
// eslint-disable-next-line prefer-promise-reject-errors
reject('no lyric');
}
});
});
}

player组件里引入插件lyric-parser,并且修改watch里的currentSong(),不直接调用currentSong.getLyric(),在methods里封装getLyric()

 

import Lyric from 'lyric-parser';
currentSong(newSong, oldSong) {
...
// 这里不直接调用currentSong.getLyric()
this.getLyric();
});
},

 

 getLyric() {
this.currentSong.getLyric().then((lyric) => {
this.currentLyric = new Lyric(lyric);
console.log(this.currentLyric);
});
},
//在data里添加currentLyric
 data() {
return {
songReady: false,
currentTime: 0,
radius: 32,
currentLyric: null,
};
},

打印可以看到lyric对象的数据,数据里有一个lines,每个lines对象都有time和txt

播放器歌词滚动列表实现

在player组件添加以下dom结构:使用获取到currentLyric,并且遍历lines对象

 <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">{{line.text}}</p>
</div>
</div>
</div>

在浏览器先将middle-l的dom结构删掉,可以看到歌词列表

 

 

目前的歌词列表是无法滚动的,也不能实时根据歌曲的播放显示对应的歌词,需要去处理一下

当执行到getLyric()时,获取到歌词的时候,调用this.currentLyric.play(),这样的话歌词就会播放了。还要在初始化的时候传一个回调函数handleLyric()。

getLyric() {
this.currentSong.getLyric().then((lyric) => {
this.currentLyric = new Lyric(lyric, this.handleLyric);
if (this.playing) {
this.currentLyric.play();
}
console.log(this.currentLyric);
});
},

定义这个方法handleLyric():当歌词每一行发生改变的时候,它就回调一下,让当前的歌词变高亮。

  • 首先需要先去data里定义一个currentLineNum,表示当前所在的行。

 

 data() {
return {
songReady: false,
currentTime: 0,
radius: 32,
currentLyric: null,
currentLineNum: 0,
};
},
  • 在刚才的dom结构绑定一个class:当currentLineNum等于index的时候就显示current样式,实现高亮效果
<p ref="lyricLine" class="text" :class="{'current':currentLineNum === index}" v-for="(line,index) in currentLyric.lines" :key="index">{{line.txt}}</p>
  • handleLyric设置当前currentLine等于index,这样就可以看到当前播放的歌词
handleLyric({ lineNum, txt }) {
this.currentLineNum = lineNum;
},

歌词列表如果想要实现滚动,就需要用到scroll组件。向scroll传入data是为了currentLyric发生变化的时候,它可以自动调用它的refresh方法

<scroll class="middle-r" ref="lyricList" :data="currentLyric&&currentLyric.lines">
<div class="lyric-wrapper">
<div v-if="currentLyric">
<p ref="lyricLine" class="text" :class="{'current':currentLineNum === index}" v-for="(line,index) in currentLyric.lines" :key="index">{{line.txt}}</p>
</div>
</div>
</scroll>
import Scroll from '../../base/scroll/scroll';
components: {
ProgressBar,
ProgressCircle,
Scroll,
},

当我们歌词播放到中间的时候(歌词的第5-6行开始,可以保证在屏幕的中间),会有上下滚动的效果。这个时候如果滚动到一个位置它也会再滚回去。

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);
}
},

播放器歌词左右滑动实现

在播放器页面有个dot的dom结构

<div class="bottom">
<div class="dot-wrapper">
<span class="dot"></span>
<span class="dot"></span>
</div>
...
</div>

当前哪个点应该是active,用一个变量维护这个状态。currentShow默认为cd,当切换右边页面的时候,currentShow就改为lyric

 data() {
return {
songReady: false,
currentTime: 0,
radius: 32,
currentLyric: null,
currentLineNum: 0,
currentShow: 'cd',
};
},
<span class="dot" :class="{'active':currentShow==='cd'}"></span>
<span class="dot" :class="{'active':currentShow==='lyric'}"></span>

接下来就是实现左右滑动。

  • created()下定义touch变量
  • middle绑定touch事件,touchStart,touchMove,touchEnd,并定义这三个方法(其中还添加了一些动画效果使得滑动画面不生硬)
middleTouchStart(e) {
this.touch.initiated = true;
const touch = e.touches[0];
// 记录X坐标和Y坐标
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;
// 为什么要维护纵坐标呢,因为歌词滚动是用scroll是一个上下滚动的过程,当纵轴偏移大于横轴偏移时,就不应该左右移动
const deltaY = touch.pageY - this.touch.startY;
if (Math.abs(deltaY) > Math.abs(deltaX)) {
return;
}
// 在滚动时,需要知道歌词列表滚动的宽度是多少。首先要记录在滚动过程中,middle-r距离右侧的宽度
const left = this.currentShow === 'cd' ? 0 : -window.innerWidth;
// 最大不超过0
const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX));
// lyricList实际上是个scroll组件,即vue组件,是没法直接操作dom,需要访问它的element才能访问dom
// 滑动的比例=列表向左宽度的宽度/整个屏幕的宽度
this.touch.percent = Math.abs(offsetWidth / window.innerWidth);
this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`;
this.$refs.lyricList.$el.style[transitionDuration] = 0;
// percent越大,透明度就越小
this.$refs.middleL.style.opacity = 1 - this.touch.percent;
this.$refs.middleL.style[transitionDuration] = 0;
},
middleTouchEnd() {
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 { // 从左向右滑
// 如果滑超过10%就要偏移回去
// eslint-disable-next-line no-lonely-if
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`;
},
View Code

播放器歌词剩余功能实现

有以下问题需要去完善

  • 多次切换歌曲时,歌词会不停来回跳动

原因:歌词是用currentLyric对象内部的一些功能实现跳跃,每个currentLyric内部是用了一个计时器实现歌曲的播放跳到相应的位置。每次currentSong改变的时候,都会重新new一个新的lyric-parser出来,但是之前的对象并没有做清理操作,也就是之前的currentLyric还是有计时器在里面,所以造成了歌词来回闪动的bug.

解决方法:在切currentSong即重新getLyric之前,把当前的getLyricstop

 currentSong(newSong, oldSong) {
if (newSong.id === oldSong.id) {
return;
}
if (this.currentLyric) {
this.currentLyric.stop();
}
...
},
  • 点击暂停时,歌词并没有停止滚动

原因:在播放状态改变的时候,歌词的播放状态没有改变

解决方法:在togglePlaying()中添加判断逻辑:播放状态改变时,歌词的播放状态也随之改变

 togglePlaying() {
// 如果没有ready好的话就直接返回
if (!this.songReady) {
return;
}
this.setPlayingState(!this.playing);
if (this.currentLyric) {
this.currentLyric.togglePlay();
}
},
  • 在循环播放模式下,将进度条切到末尾让它重新回到初始位置时,歌词并没有回到最初的位置

解决方法:在loop()中实现逻辑,使用歌词的seek方法将它偏移到初始位置。

loop() {
this.$refs.audio.currentTime = 0;
this.$refs.audio.play();
if (this.currentLyric) {
this.currentLyric.seek(0);
}
},
  • 在拖动进度条的时候,歌词并没有随着进度条的改变而改变

解决方法:在onProgressBarChange()中实现逻辑,也是调用歌词的seek方法

onProgressBarChange(percent) {
const currentTime = this.currentSong.duration * percent;
this.$refs.audio.currentTime = currentTime;
if (!this.playing) {
this.togglePlaying();
}
if (this.currentLyric) {
this.currentLyric.seek(currentTime * 1000);
}
},
  • 在cd下方显示歌词,这样用户就不用每次切换到歌词列表看歌词

解决方法:在cd-wrapper下面加一个div,传入数据playingLyric。playingLyric在handleLyric执行的时候改变。

<div class="middle-l" ref="middleL">
<div class="cd-wrapper" ref="cdWrapper">
<div class="cd" :class="cdCls">
<img class="image" :src="currentSong.image">
</div>
</div>
<div class="playing-lyric-wrapper">
<div class="playing-lyric">{{playingLyric}}</div>
</div>
</div>
data() {
return {
...
playingLyric: '',
};
},
handleLyric({ lineNum, txt }) {
...
this.playingLyric = txt;
},
  • 考虑getLyric异常情况:获取不到歌词的时候,要做清理操作
getLyric() {
this.currentSong.getLyric().then((lyric) => {
this.currentLyric = new Lyric(lyric, this.handleLyric);
if (this.playing) {
this.currentLyric.play();
}
}).catch(() => {
this.currentLyric = null;
this.playingLyric = '';
this.currentLineNum = 0;
});
},
  • 考虑边界条件:当歌曲列表只有一首歌,点击下一首或上一首的时候会有什么问题?源代码中index = currentIndex + 1,currentIndex = 0;此时index等于playlist的长度,重置为0,然后又执行下面的逻辑将currentIndex置为0,则playlist不会发生变化,currentSong的id也不会发生变化,之后的逻辑都不会执行。

解决方法:在next()和prev()加个判断,如果playlist的长度为1时,就让它使用loop()进行单曲循环。

prev() {
// 如果没有ready好的话就直接返回 不能使用下面的逻辑实现功能
if (!this.songReady) {
return;
}
if (this.playlist.length === 1) {
this.loop();
} else {
let index = this.currentIndex - 1;
if (index === -1) {
index = this.playlist.length - 1;
}
this.setCurrentIndex(index);
if (!this.playing) {
this.togglePlaying();
}
}
this.songReady = false;
},
  • 当我们在微信播放的时候,实际上js是不会执行的但是audio可以将当前歌曲播放完。一旦歌曲播放完就会触发end事件,但是end事件是js不会执行。如果end不执行,那么再次播放的时候,songReady就一直不会设置为true,我们就切换不了歌曲。

解决方法:让audio的play方法延迟时间更长一点,保证在手机浏览器从后台切换到前台js执行的时候,播放器可以正常播放。

currentSong(newSong, oldSong) {
if (newSong.id === oldSong.id) {
return;
}
if (this.currentLyric) {
this.currentLyric.stop();
}
setTimeout(() => {
this.$refs.audio.play().catch((error) => {
this.togglePlaying();
// eslint-disable-next-line no-alert
alert('播放出错,暂无该歌曲资源');
}, 1000);
// 这里不直接调用currentSong.getLyric()
this.getLyric();
});
},

播放器底部播放器适配

以前页面的滚动高度都是计算到底的,但是现在有了迷你播放器占了底部一定高度,scroll滚动的高度就出错了。

监听playerlist,如果当有playerlist的时候,scoll组件的bottom值重新设置成mini-player的高度,让它重新计算scorll滚动的高度。

因为这些组件都需要处理这个问题,处理这个问题的逻辑又非常类似,可以使用mixin

创建mixin.js

export const playlistMixin = {
computed: {
// 通过getters拿到playlist
 ...mapGetters([
'playlist',
]),
},
mounted() {
this.handlePlaylist(this.playlist);
},
activated() {
this.handlePlaylist(this.playlist);
},
watch: {
playlist(newVal) {
this.handlePlaylist(newVal);
},
},
methods: {
handlePlaylist() {
// 具体方法要到具体组件实现
// 抛个异常,组件必须实现这个函数,一旦组件定义这个函数,它就会覆盖mixin里的这个函数。如果没有则调用mixin里的这个函数
throw new Error('component must implement handlePlaylist method');
},
},
};

一个组件可以插入多个mixin,所以有个mixins属性使用。在music-list组件应用mixin,一旦组件使用了mixin,就必须定义handlePlaylist方法不然会报错.

定义handlePlaylist方法,判断如果有playlist,改变改变list的bottom并强制scroll重新计算

 

import { playlistMixin } from '../../common/js/mixin';
mixins: [playlistMixin],
handlePlaylist(playlist) {
const bottom = playlist.length > 0 ? '60px' : '';
this.$refs.list.$el.style.bottom = bottom;
// 调用refresh()让scroll重新计算高度
this.$refs.list.refresh();
},

 

singer组件同理,但是需要调用listview让它重新计算,在listview.vue中暴露一个refresh方法后,再在singer.vue中调用

 

refresh() {
this.$refs.listview.refresh();
},
<div class="singer" ref="singer">
<list-view @select="selectSinger" :data="singers" ref="list"></list-view>
<router-view></router-view>
</div>
handlePlaylist(playlist) {
const bottom = playlist.length > 0 ? '60px' : '';
this.$refs.list.$el.style.bottom = bottom;
// 调用refresh()让scroll重新计算高度
this.$refs.list.refresh();
},

最后修改推荐页面

handlePlaylist(playlist) {
const bottom = playlist.length > 0 ? '60px' : '';
this.$refs.recommend.style.bottom = bottom;
// 调用refresh()让scroll重新计算高度
this.$refs.scroll.refresh();
},

 

版权声明
本文为[小风车吱呀转]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/Small-Windmill/p/14725002.html

  1. 21. Object oriented foundation "problems and solutions of object traversal"
  2. Discussion on hot micro front end: Google AdWords is a real micro front end
  3. Usecallback and usememo for real performance optimization
  4. 【前端圭臬】十一:从规范看 JavaScript 执行上下文(下)
  5. [front end standard] 11: Javascript execution context from the perspective of specification (2)
  6. Hexagonal六角形架构ReactJS的实现方式 - Janos Pasztor
  7. Transaction of spring's reactive / imperative relational database
  8. The implementation of hexagonal hexagonal reactjs Janos pasztor
  9. HTTP状态码:402 Payment Required需要付款 - mozilla
  10. HTTP status code: 402 payment required - Mozilla
  11. Factory mode, constructor mode and prototype mode
  12. Build the scaffold of react project from scratch (Series 1: encapsulating a request method with cache function based on Axios)
  13. Cocos Quick Start Guide
  14. Comparison of three default configurations of webpack5 modes
  15. A case study of the combination of flutter WebView and Vue
  16. CSS: BFC and IFC
  17. A common error report and solution in Vue combat
  18. JS: this point
  19. JS: prototype chain
  20. JavaScript series -- promise, generator, async and await
  21. JS: event flow
  22. Front end performance optimization: rearrangement and redrawing
  23. JS - deep and shallow copy
  24. JavaScript异步编程3——Promise的链式使用
  25. JavaScript asynchronous programming 3 -- chain use of promise
  26. Vue.js组件的使用
  27. The use of vue.js component
  28. How to judge whether a linked list has links
  29. Element UI custom theme configuration
  30. Text image parallax effect HTML + CSS + JS
  31. Spring的nohttp宣言:消灭http://
  32. Vue3 intermediate guide - composition API
  33. Analysis of URL
  34. These 10 widgets that every developer must know
  35. Spring's nohttp Manifesto: eliminate http://
  36. Learn more about JS prototypes
  37. Refer to await to JS to write an await error handling
  38. A short article will directly let you understand what the event loop mechanism is
  39. Vue3 uses mitt for component communication
  40. Characteristics and thinking of ES6 symbol
  41. Two way linked list: I'm no longer one-way driving
  42. Vue event and form processing
  43. Reactive TraderCloud实时外汇开源交易平台
  44. Reactive tradercloud real time foreign exchange open source trading platform
  45. Node.js REST API的10个最佳实践
  46. Ten best practices of node.js rest API
  47. Fiddler advanced usage
  48. Process from Vue template to render
  49. Promise up (asynchronous or synchronous)
  50. Principle and implementation of promise
  51. Vs code plug in sharing - run code
  52. Vue practical notes (1) introduction of Ant Design
  53. Vue actual combat notes (2) introduction of element plus
  54. Introduction to webpack
  55. Webpack construction process
  56. Vue notes
  57. The experience and lessons of moving from ruby megalith architecture to go microservice
  58. Using leancloud to add artitalk module to hexo blog
  59. Implementation of chrome request filtering extension
  60. Detailed introduction of beer import declaration elements and label quarantine [import knowledge]