[toc]
前言#
花了一个下午带晚上的事件做了一个简单的音乐播放器,大部分时间都花在了 audio 的自动播放问题上,原来浏览器只是不允许在移动设备上自动播放,从 chrome66 开始 PC 端也不允许自动播放了,audio.play() 必须写在点击事件的回调函数中才能生效,不然就会报错 DOMException: play() failed because the user didn't interact with the document first.,也就是说现在想要让页面上的音频播放必须经过用户点击以后才行,不能不经过和用户的互动直接播放了。这方面的资料比较少,chrome 有一篇文档说明autoplay policy changes ↗。除开这个问题,其他功能并没有遇到太大的问题,样式的事件花的多一点,播放器的逻辑比较简单,JS基本都是操作样式和 Audio 对象,下面来分享以下构思和实现的过程。
HTMLMediaElement的属性、方法和事件很多,建议大家到MDN ↗仔细阅读以下文档。
项目预览#
项目完成后我放在了服务器上,访问地址是音乐播放器 ↗,GitHub 地址 音乐播放器-github ↗。其实本来是想在 GitHub Pages上也能够预览的,配置了 apache 的跨域访问,但是在请求阿里云的静态资源(专辑封面和歌曲)的时候一直403,明明以及把 GitHub Pages 的地址加入到白名单了,试了半天只能放弃了。下面是整个页面的预览图片:

项目需求#
- 页面设计:歌曲封面,歌曲名,进度条,音量控制,歌曲播放控制(上一曲,下一曲,播放暂停),歌曲列表。
- 初始化:打开页面时需要根据后台的数据生成播放列表,以及初始化播放控件(进度条归零,初始化音量,待播放歌曲设置为第一首,显示第一首歌的名称和时间)
- 播放逻辑,点击播放暂停,上一首下一首和直接点击歌曲列表的逻辑
- 给播放列表做一个打开和关闭的动画
- 进度条控制:让用户可以自己选择歌曲的进度
- 音量控制
- 断网控制
页面结构和样式#
背景#
背景比较简单,我选了一张自己比较喜欢的龙猫的图片,设置了一个 filter: blur(5px),效果还不错。
播放器主体#
播放器主体就参照一般的音乐播放器,左边是歌曲封面,右边是播放控件,用display: inline-block,播放器的宽高都是固定的,inline-block 的空格空隙问题我是用 font-size: 0来处理的。需要注意的是要设置以下图片或者右边元素的 vertical-align,因为图片下边缘默认与当前 inline box 中的基线对齐的,如果不设置 vertical-align 图片默认和右边元素中的最后一行文本的基线对齐。结构如下:
<img src="" alt="cover" class="left" />
<div class="right"></div>html右侧的播放器控件部分就分为上中下三块,最上面显示歌曲信息,中间一层是进度条和音量按钮,最下面是几个播放按钮。结构如下:
<h4>music info</h4>
<div class="mid"></div>
<div class="controls"></div>html最上面的歌曲信息部分没啥好说的,设置合适的字体就可以了,white-space: nowrap,text-overflow: ellipsis,处理歌曲信息过长。中间一层我放了进度条和音量控制,进度条就是总长度一个元素,当前长度一个元素,两个元素选择不同的颜色重叠就可以了,时间我用的是总长度的进度条元素的伪元素来实现的,伪元素的 content 用进度条元素的属性来设置 content = attr(data-time)。 本来我是想当前进度也用伪元素来实现的,但是伪元素并不算是 DOM 元素,无法用 JS 来操作,并且没法添加事件(虽然有各种黑科技)。
理论上来说伪元素不算
DOM元素,所以无法用JS操作以及绑定事件,不过我看到segmentfault上有两个答案对这两种行为都提出了解答,通过JS改变伪元素样式 ↗, pointer-event为伪元素绑定事件 ↗
中间这一层我直接给包裹层一个高度,内部的几个块都用绝对定位,保持两个进度条的位置一致,并且和音量键对齐。同时音量的控制模块也用绝对定位。用绝对定位是为了让这几个元素对齐比较方便,特别是音量和进度条都是两个元素重叠在一起,并且两个元素都是需要点击计算距离的,所以 1px 的误差都不能有,用绝对定位比较好处理。结构如下:
<div class="mid">
<div class="progress" data-time="00:00/00:00"></div>
<div class="current"></div>
<div class="volume">
<div class="vol-control"></div>
<div class="vol-current"></div>
</div>
<div class="vol">
<span><i class="fa fa-volume-up"></i></span>
</div>
</div>html最下面的一层控制按钮,直接用列表 inline-block 就可以了,计算好宽度间隔。对于列表的横向排列,我们经常希望第一个元素和边界没有间隔,后面的元素留有相同的空间,处理方法有很多种。 1. 用 flex 布局 2. 对每个 li 使用相同的 margin-left, 然后 ul 使用一个负 margin,这样做 ul 的宽度需要增加一个间隙的值 3. 使用 li + li 选择器,选择所有 li的相邻兄弟元素,第一个元素不会被选到 4. 使用 li:first-child 给第一个元素单独设置样式 5. 使用li:first-child ~ li 选择第一个元素的所有兄弟元素
最后每个元素添加一个 hover 样式,然后加上一个 transition: all .3s ease-in-out; 给 hover 效果添加一个过度状态。
歌曲列表#
歌曲列表的样式并没有什么难的地方,一个列表就可以解决。不过我想给歌曲列表的打开和关闭制作一个动画,当打开的时候,歌曲列表从播放器主体下方滑出;当关闭的时候,列表缓慢上移,上移的部分在主体下方消失,效果如下面的动图:
上移的效果非常容易,transform: translateY(-300px) 就可以了,但是上移的部分还是会显示在画面中,并且挡住整个主体了。我的解决方案是在 ul 外面再套一层 div,div 设置一个 overflow: hidden;,在 ul 向上移动的过程中 div 的高度也同步减小,保持两者的 transition 的时间相同,这样两者的过度效果能保持相同。因为 overflow: hidden 的存在,ul 滑动到上方的部分将会被隐藏。具体代码看上面的 GitHub 链接。
功能实现#
所有功能基本都围绕 HTMLMediaElement 的属性、方法和事件来实现,基本都是对 Audio 的操作,说以下主要思路。
播放逻辑#
上一曲,下一曲和点击播放列表中的歌曲逻辑基本是相同的。在请求到歌曲信息后保存到一个对象中,除了将对象中的信息添加到歌曲列表中之外,也是我们操作按钮的时候取数据的地方。我们需要一个用来标记当前播放歌曲 index 的变量,初始化为 0,也就是打开播放器的时候默认从第一首开始播放。然后点击上一首就对变量 +1(需要判断是不是第一首,如果是第一首就跳到最后一首),点击下一曲就对变量 -1(需要判断是不是最后一首,如果是最后一首就跳到第一首),点击歌曲列表直接通过 li 的 data-num 属性来设置变量的值。剩下的初始化进度条,名称,时间等功能都是一样的,可以写到一个函数里,分别调用就可以了。
重复点击同一首歌曲默认不操作,也就是歌曲的
data-num和 当前播放的歌曲的index相同的时候不处理,但是当页面初始化完成直接点击第一首歌的时候data-num和index都是0,这个逻辑需要处理一下。
播放和暂停时候按钮的样式不同,点击的时候需要处理一下,包括上面的第三个按钮点击的时候一样要看看播放按钮是否处于暂停的样式。
播放逻辑中要用到的 Audio 方法基本就是 audio.play() 和 audio.pause(),属性就是一个 audio.src,需要注意的是 audio 对象在一首歌播放结束后就会暂停,触发 ended 事件,此时我们要触发一下下一首的按钮就可以了。
audio.onended = function () {
var event = new Event('click')
nextBtn.dispatchEvent(event)
}javascript点击按钮获取歌曲信息的方式有两种,一种按照
index到歌曲信息数组中去取;第二种是在生成歌曲列表的时候用data-*属性将信息保存到每个歌曲对应的li中。从操作上来说第一种方法是比较简单的,但可能播放器逻辑更复杂的时候第二种方法也有用武之地,比如用户有多个播放列表,后台给我们的是一个数组,数组中每个对象有一个type属性来标记歌单,可能第二种方法会更好一点。
进度条#
进度条要处理的主要是三个方面:播放新的歌曲需要初始化进度条和时间,跟随歌曲播放的进度前进,可以让用户手动设置进度。
每次用户点击上一曲下一曲或者播放列表的时候就需要初始化进度条,我们可以用 audio.oncanplay 事件来监听,这个事件是当 audio 已经加载可以开始播放的时候触发,当这个事件触发的时候我们将时间归零,进度条的长度重置为 0 就可以了。
播放进度我们可以用 audio.ontimeupdate 事件来监听,在 Chrome 上这个事件一秒触发四次,每次触发我们就获取 audio.currentTime 属性,然后根据 currenTime 和 duration 之间的比值来设置我们的进度条长度即可,同时也可以设置时间,currentTime 和 duration 都是以秒为单位的浮点数,我们进行一些处理就行了。
点击进度条我们利用 MouseEvent 对象的 offsetX 属性来获取鼠标事件触发时,鼠标的坐标和触发事件的对象的左边缘在 X 轴上的偏移量,获得这个偏移量之后,我们将进度条的宽度移动至此,并且根据进度条长度和总长度的比例计算出 currenTime,然后设置 currentTime。currentTime 是一个可以赋值的属性,并且它的值的改变也会触发 timeupdate 事件。
音量的控制本来是想使用滑动的方式来处理的,奈何水平不行,对滑动的几个事件处理怎么都做到平滑,要改进这个功能要把滑动相关的几个事件深入了解一下。
音量控制#
音量控制的原理本质上跟进度条一样的,只不过因为音量是竖直方向上的,所以用 offsetY 来获取偏移量,通过比例的计算来设置 audio.volume 属性即可,这个控件在手机上不生效,手机还是要通过自身的音量按钮来控制声音大小。
断网控制#
当断网时,如果继续执行页面逻辑或出现请求不到资源,图片加载不出,页面报错。所以在断网发生时我们需要立即处理。通过 window 对象的 offine 和 online 事件我们可以很好地处理这个逻辑。当断网发生时我们应该禁止用户点击控件和歌曲列表,如果用解绑事件的方式太麻烦了,我直接采取用一个遮罩层盖住控件,收起播放列表,暂停当前播放,在遮罩层上显示一个断网信息。当网络恢复的时候我们再打开播放列表,如果断网前音乐是在播放的,那我们就恢复播放。效果如下:
思维拓展#
要制作一个比较完善的播放器还有几个功能需要加上: 1. 显示歌词 2. 歌曲搜索
总结#
虽然只是一个小小的音乐播放器,不过还是补充了不少知识的,当程序员永远都是 纸上得来终觉浅,绝知此事要躬行 啊,不过纸上得来的的也是多多益善,没有这个纸做铺垫,写再多代码也只是原地踏步。