用 TS + Vue 重写 APlayer HTML5 音乐播放器

简介

@DIYgod/APlayer 是一款简洁漂亮的 HTML5 音乐播放器 (〃ノωノ)
在我第一次看到这款播放器颜值的时候让我眼前一亮,我很是崇拜那些能设计出好看界面的设计师 (* >ω<)css

可是在用过以后发现仍是有不足的地方 这是我曾经提过的 Issuesvue

用了一段时间,很喜欢 APlayer 简洁的 UI,提一些其余可改进的建议:react

1.我认为有必要提供动态管理播放列表的 API
(若是没有,在须要动态添加歌曲到列表时只能从新初始化)
2.应该提供一个销毁播放器的 API
3.歌词容许异步添加,一般获取歌词接口是单独的
(如今必须等待歌词接口返回再初始化播放器,若歌词获取失败或时间过长会同时影响到播放音乐功能)webpack

关于第三条,APlayer 实际上是支持异步歌词的但仅支持传入 .lrc 文件的地址
若是是像网易云/QQ音乐那样返回的是 JSON 格式的那就不知足需求了ios

为何不提 PR 要重写呢?
这个我想了一会,最终仍是以为组件化的方式开发更好一些(原 APlayer 用的是原生 JS 没有依赖别的库)
并且由于我之前还在作后端的时候就本身写过音乐播放器(仿微博播放器,当时不会用 Git 源码已丢)
因此挺有经验的,重写一个难度也不大,并且比较为所欲为,还能够随意加一些本身想要的东西 qwqgit

截图

说明:该播放器是基于 @DIYgod/APlayer 的布局和 样式 采用 TS + Vue 组件化重构的github

Demo

演示:http://aplayer.quq.cat
文档:http://aplayer.quq.cat/docs
源码:https://github.com/MoeFE/vue-...
NPM:https://www.npmjs.com/package...
播放列表来自网易云歌单:http://music.163.com/#/playli... web

若是喜欢的话别忘了点一个 star 哟 (*ゝω・)
欢迎提 IssuesPR (´・_・`)正则表达式

框架选型

为了你们使用方便,我选择 Vue ,能够响应式控制播放器各个属性 并以插件的形式发布(详情请看 demo
我这里为了方便你们能更好的调试,在生产环境下开启了 SourceMapdevtools
若是您安装了 vue-devtools 能够打开调试器查看组件划分和各个组件的信息typescript

至于为何选择用 TypeScript 本文就不作过多的解释了
你们能够自行在网上找一下 TypeScriptJavaScript 的区别

我只能告诉你:对于一个曾经使用 C# 的开发者来讲这简直不能太爽啦 微软爸爸赛高 (* >ω<)

最后推荐一款 TS + Vue 的脚手架模版:https://github.com/Toilal/vue...
之后或将加入到官方模版中:https://github.com/vuejs-temp...

TS + React 的脚手架能够用这个:https://github.com/wmonk/crea...

拆分组件

拿到布局样式后要作的第一件事情就是拆分组件
@DIYgod/APlayer 的布局和 样式 复制过来
确保样式没有问题后再将各个组件的布局和样式单独复制出来
不懂设计的只好复制了 请容许我作一个悲伤的表情 (ಗ ‸ ಗ )

拆分组件

我将播放器拆分红了如下组件:

组件名称 组件说明
APlayer.ts 播放器容器组件
Button.ts 按钮组件
Picture.ts 歌曲图片组件
Container.ts 右侧容器组件
Info.ts 歌曲信息组件
Lyric.ts 歌词面板组件
Progress.ts 进度条组件
Time.ts 播放时间组件
Volume.ts 音量控制组件
List.ts 播放列表组件
Item.ts 播放列表项组件

再来一张更清晰的图片吧:

vue-devtools

点击查看高清原图

功能开发

功能开发其实没有多少难度,HTML5 已经封装好了 HTMLAudioElement 元素
咱们就是用一下它的 API 和视图进行数据绑定和交互而已 看一下文档就行了

不过这里会遇到一个小问题,那就是 Vue 不能监听到 Audio 对象的属性变化
由于 Audio 对象其实就是 HTMLAudioElement 元素,Vue 是不能监听到元素属性变化的,因此我想了一个小办法

定义了一个 Media 接口,里面定义了和 Audio 对象相同的属性,在 Audio 的事件中对 Media 的属性进行同步
这样的话,就可使用 Media 对象响应式获取 Audio 的属性值
能够查看这一段代码:APlayer.ts#L326-L334

我这里简单介绍一些比较经常使用的属性和方法吧

名称 说明
autoplay 是否自动播放 (在 Safari 中无效,能够自行在初始化音频后手动调用 play 方法)
bufferd 获取已缓冲的进度(必须在 readeyState >= 3 以后获取,不然会抛异常)
loop 是否循环播放音频(推荐根据当前播放模式自行实现该功能)
preload 预加载选项,推荐使用 metadata,在未播放时仅获取音频的长度,而不要加载整个音频
src 获取或设置音频的播放地址
volume 获取或设置音频的音量(0~1)
paused 获取当前音频是否已暂停
currentTime 获取或设置当前音频的播放进度(单位:秒)
duration 获取当前音频的长度(单位:秒)
playbackRate 获取或设置当前音频的播放速度
play () 播放音频
pause () 暂停音频

点击查看全部 Media 事件

事实上 AudioVideo 对象差很少 都属于 Media
因此若是你会开发音乐播放器那么也会开发视频播放器了

这里重点说一下 timeupdate 事件,这个事件在音频播放时不断触发,这个能够说是最有用的事件了
由于在播放过程当中须要不断的重绘播放器的播放进度和已播放时间
若是有歌词的话,还须要根据当前的播放时间去同步歌词

若是没有或者不知道这个事件的话,你可能会使用 setInterval 代替
使用 setInterval 的话,会有两个问题:
1.重绘时间到底设置多少合适?太快了影响性能,太慢了页面不一样步
2.若是用户暂停播放了,须要清除定时器,开始播放又要初始化定时器,太麻烦
(或者偷懒的话能够判断 pausedreturn ,那么须要不断的跑一个空定时器)

LRC 歌词解析和同步

可能作这个功能的时候是最好玩的吧 qwq
由于在好久之前 千千静听那个年代 我无聊的时候就作一下 LRC 歌词
因此对这个功能很敏感 尽可能作到最好吧 (´・_・`)

这里主要功能是歌词解析,歌词同步的话只要计算出与当前播放时间最匹配的项元素
而后设置歌词面板的滚动条位置到当前元素的位置便可

常见的时间标签有如下几种

[mm:ss] 只有分和秒的时间标签
[mm:ss:ms] 有分、秒、毫秒的时间标签
[mm:ss.ms] 有分、秒、毫秒的时间标签的另外一种格式
[mm:ss:ms][mm:ss.ms] 多个时间标签共享这一句相同的歌词

个人思路是:
首先按照行将歌词文本分割成数组,再按行进行解析
使用正则表达式匹配出该行的 分、秒、毫秒 和显示的歌词文本
将 分、秒、毫秒 都转换成毫秒单位而后加起来与歌词文本关联后保存到数组中,最后须要按照时间正序排列
那么 当前要显示的歌词 = 过滤数组中 时间 < 当前播放时间 后的最后一项

private async parseLRC (): Promise<void> {
  if (!this.lrc || this.lrc === 'loading') return
  if (this.isURL(this.lrc)) { // 若是歌词是一个URL地址则请求该地址得到歌词文本
    const { data } = await Axios.get(this.lrc.toString())
    this.currentLRC = data
  } else this.currentLRC = this.lrc

  const reg = /\[(\d+):(\d+)[.|:](\d+)\](.+)/
  const regTime = /\[(\d+):(\d+)[.|:](\d+)\]/g
  const regCompatible = /\[(\d+):(\d+)]()(.+)/
  const regTimeCompatible = /\[(\d+):(\d+)]/g
  const regOffset = /\[offset:\s*(-{0,1}\d+)\]/
  const offsetMatch = this.lrc.match(regOffset)
  const offset = offsetMatch ? Number.parseInt(offsetMatch[1]) : 0
  this.LRC = []

  const matchAll = (line: string) => {
    let match = line.match(reg) || line.match(regCompatible)
    if (!match) return
    if (match.length !== 5) return
    const minutes = Number.parseInt(match[1]) || 0
    const seconds = Number.parseInt(match[2]) || 0
    const milliseconds = Number.parseInt(match[3]) || 0
    const time = (minutes * 60 * 1000 + seconds * 1000 + milliseconds) + offset
    const text = (match[4] as string).replace(regTime, '').replace(regTimeCompatible, '')
    if (!text) return // 优化:不要显示空行
    this.LRC.push({ time, text })
    matchAll(match[4]) // 递归匹配多个时间标签
  }

  this.currentLRC.replace(/\\n/g, '\n').split('\n').forEach(line => matchAll(line))

  // 歌词格式不支持
  if (this.LRC.length <= 0) this.LRC = [{ time: -1, text: '(・∀・*) 抱歉,该歌词格式不支持' }]
  else this.LRC.sort((a, b) => a.time - b.time)
}

点击查看完整代码

总结

完善了原 APlayer 不足的地方:
1.能够响应式的随意控制播放器属性
2.歌词同步支持多种时间标签格式(fix #39
3.歌词同步兼容 [offset:0] 标签
4.异步歌词的支持
5.容许控制播放速度(相同的歌曲用不一样的速度听感受会不同哦 quq)
6.音量容许拖动控制
7.支持注册全部 Media 事件
8.保存播放器配置到 localStorage 中,刷新后能够恢复播放进度等信息

而且体验了一把用 TSVue 的快感w

最后 弱弱的:@MoeFE 欢迎各位大佬加入 (๑•̀ㅂ•́)و✧
额..没啥要求 头像要萌要可爱的!!

好想有个大佬能带我装逼带我飞 (ง •_•)ง

相关文章
相关标签/搜索