4个小时实现一个HTML5音乐播放器
技术点:ES6+Webpack+HTML5 Audio+Sass
这里,我们将一步步的学到如何从零去实现一个H5音乐播放器。
首先来看一下最终的实现效果:Demo链接界面:

接下来就步入正题:
- 要做一个音乐播放器就要非常了解在Web中音频播放的方式,通常都采用HTML5的audio标签
关于audio标签,它有大量的属性、方法和事件,在这里我就做一个大致的介绍。属性:
src:必需,音频来源;controls:常见,设置后显示浏览器默认的audio控制面板,不设置默认隐藏audio标签;
autoplay:常见,设置后自动播放音频(移动端不支持);
loop:常见,设置后音频将循环播放;
preload:常见,设置音频预加载(移动端不支持);
volume:少见,设置或返回音频大小,值为0-1之间的一个浮点数(移动端不支持);
muted:少见,设置或返回静音状态;
duration:少见,返回音频时长;
currentTime:少见,设置或返回当前播放时间;
paused:少见,返回当前播放状态,是否暂停;
buffered:少见,一个TimeRanges对象,包含已缓冲的时间段信息,即加载进度。该对象包含一个属性length,返回一个从0开始的数表示当前缓冲了多少段音频;还包含两个方法,start、end,分别需要传入一个参数,即传入音频已加载的第几段,从0开始。start返回该段的起始时间,end返回该段的终点时间。举例:即传入0,第一段的起始是0,终止时间是17,单位秒;
属性就介绍到这里,可能还有一些比较少用的属性如:playbackRate等,在视频播放中可能会用到,我就暂不讲解。方法:
play():开始播放音频;
pause():暂停播放音频;事件:
canplay:当前音频可以开始播放(只加载了部分buffered,并未全部加载完成);
canplaythrough:可以无停顿播放(即音频全部加载完成);
durationchange:音频时长发生变化;
ended:播放结束;
error:发生错误;
pause:播放暂停;
play:播放开始;
progress:音频下载过程中触发,事件触发过程中可以通过访问audio的buffered属性获取加载进度;
seeking:音频跳跃中触发,即为修改currentTime时;
seeked:音频跳跃完成时触发,即为修改完成currentTime时;
timeupdate:音频播放过程中触发,同时currentTime属性在同步更新;
事件就介绍到这里,可能还有一些不常用的事件暂不讲解。最后再讲解一下一个音频从开始加载到播放结束过程中,所触发的事件流以及我们在不同时间段可以操作的属性:
loadstart:开始加载;
durationchange:获取到音频时长(此时可以获取duration属性);
progress:音频下载中(将伴随下载过程一直触发,此时可以获取buffered属性);
canplay:所加载的音频足够开始播放(每次暂停后开始播放也会触发);
canplaythrough:音频全部加载完成;
timeupdate:播放过程中(currentTime属性伴随着同步更新);
seeking:修改当前播放进度中(即为修改currentTime属性);
seeked:修改当前播放进度完成;
ended:播放完成;
这就是整个音频的大致事件流,可能有一些少用的事件没有列举出。
在事件触发过程中,有一些属性在音频还没有开始加载的时候就可以设置,如:controls、loop、volume等等; - 确定整体结构:
因为自己是做成插件的方式发布在npm上供他人使用的,所以我们就采用面向对象的方式进行代码编写,又因为用户的需求不一,所以在设计之初就暴露出大量的API和配置项以满足大部分用户的需求。
这里因为自己更习惯es6的语法,就全程以es6为基础进行开发,同时为了开发效率,又使用了sass进行css的编写,最后还使用了webpack和webpack-dev-server用以编译es6和sass,项目打包,构建本地服务器。 - 确定播放器UI和交互:
可能关于界面每个人有自己的想法,这里就不过多赘述了,以我做好的播放器UI为例进行分解
从界面中可以看出一个播放器所需要的最基础功能:
播放/暂停、封面/歌名/歌手的显示、播放进度条/加载进度条/进度操作功能、循环模式切换、进度文字更新/歌曲时长、静音/音量大小控制、列表显示状态控制、点击列表项切歌功能
再结合我们想要满足用户需求,提供配置项和API的出发点可以得出我们想设计的配置项和暴露的API项:
配置项:自动播放是否开启、默认歌曲列表的显示状态、默认循环模式的设置
API:播放/暂停/toggle、循环模式的切换、静音/恢复、列表显示状态的切换、上一曲/下一曲/切歌、销毁当前实例 - 确立项目结构,开始编码:
因为使用webpack,所以我们直接将css打包至js内,以便作为插件供用户使用:require('./skPlayer.scss');抽离公共方法,在播放器中有很多可能需要抽离的公共方法如:点击播放进度条和音量进度条时需要计算鼠标距离进度条左端的距离以进行进度跳转,时间从duratin中获取到的以秒为单位的时间转换成标准时间格式等等:
const Util = {
leftDistance: (el) => {
let left = el.offsetLeft;
let scrollLeft;
while (el.offsetParent) {
el = el.offsetParent;
left += el.offsetLeft;
}
scrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft;
return left - scrollLeft;
},
timeFormat: (time) => {
let tempMin = parseInt(time / 60);
let tempSec = parseInt(time % 60);
let curMin = tempMin < 10 ? ('0' + tempMin) : tempMin;
let curSec = tempSec < 10 ? ('0' + tempSec) : tempSec;
return curMin + ':' + curSec;
},
percentFormat: (percent) => {
return (percent * 100).toFixed(2) + '%';
},
ajax: (option) => {
option.beforeSend && option.beforeSend();
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300){
option.success && option.success(xhr.responseText);
}else{
option.fail && option.fail(xhr.status);
}
}
};
xhr.open('GET',option.url);
xhr.send(null);
}
};由于设计之初,考虑到播放器的独特性,设计为只能存在一个实例,设置了一个全局变量以判断当前是否存在实例:
let instance = false;
在使用ES6的情况下,我们将主逻辑放在构造函数内部,将通用性强和API放在公共函数内部:
class skPlayer {
constructor(option){
} template(){
} init(){
} bind(){
} prev(){
} next(){
} switchMusic(index){
} play(){
} pause(){
} toggle(){
} toggleList(){
} toggleMute(){
} switchMode(){
} destroy(){
}
}实例判断,如果存在返回无原型的空对象,因为ES6构造函数内默认返回带原型的实例:
if(instance){
console.error('SKPlayer只能存在一个实例!');
return Object.create(null);
}else{
instance = true;
}初始化配置项,默认配置与用户配置合并:
const defaultOption = {
...
};
this.option = Object.assign({},defaultOption,option);将常用属性绑定在实例上:
this.root = this.option.element;
this.type = this.option.music.type;
this.music = this.option.music.source;
this.isMobile = /mobile/i.test(window.navigator.userAgent);一些公共的API内部this指向在默认情况下指向实例,但是为了减少代码量,将操作界面上的功能与API调用一套代码,在绑定事件的时候this指向会改变,所以通过bind的方式绑定this,当然也可以在绑定事件的时候使用箭头函数:
this.toggle = this.toggle.bind(this);
this.toggleList = this.toggleList.bind(this);
this.toggleMute = this.toggleMute.bind(this);
this.switchMode = this.switchMode.bind(this);接下来,我们就使用ES6字符串模板开始生成HTML,插入到页面中:
this.root.innerHTML = this.template();
接下来初始化,初始化过程中将常用DOM节点绑定,初始化配置项,初始化操作界面:
this.init();
init(){
this.dom = {
cover: this.root.querySelector('.skPlayer-cover'),
playbutton: this.root.querySelector('.skPlayer-play-btn'),
name: this.root.querySelector('.skPlayer-name'),
author: this.root.querySelector('.skPlayer-author'),
timeline_total: this.root.querySelector('.skPlayer-percent'),
timeline_loaded: this.root.querySelector('.skPlayer-line-loading'),
timeline_played: this.root.querySelector('.skPlayer-percent .skPlayer-line'),
timetext_total: this.root.querySelector('.skPlayer-total'),
timetext_played: this.root.querySelector('.skPlayer-cur'),
volumebutton: this.root.querySelector('.skPlayer-icon'),
volumeline_total: this.root.querySelector('.skPlayer-volume .skPlayer-percent'),
volumeline_value: this.root.querySelector('.skPlayer-volume .skPlayer-line'),
switchbutton: this.root.querySelector('.skPlayer-list-switch'),
modebutton: this.root.querySelector('.skPlayer-mode'),
musiclist: this.root.querySelector('.skPlayer-list'),
musicitem: this.root.querySelectorAll('.skPlayer-list li')
};
this.audio = this.root.querySelector('.skPlayer-source');
if(this.option.listshow){
this.root.className = 'skPlayer-list-on';
}
if(this.option.mode === 'singleloop'){
this.audio.loop = true;
}
this.dom.musicitem[0].className = 'skPlayer-curMusic';
}事件绑定,主要绑定audio的事件以及操作面板的事件:
this.bind();
bind(){
this.updateLine = () => {
let percent = this.audio.buffered.length ? (this.audio.buffered.end(this.audio.buffered.length - 1) / this.audio.duration) : 0;
this.dom.timeline_loaded.style.width = Util.percentFormat(percent);
}; // this.audio.addEventListener('load', (e) => {
// if(this.option.autoplay && this.isMobile){
// this.play();
// }
// });
this.audio.addEventListener('durationchange', (e) => {
this.dom.timetext_total.innerHTML = Util.timeFormat(this.audio.duration);
this.updateLine();
});
this.audio.addEventListener('progress', (e) => {
this.updateLine();
});
this.audio.addEventListener('canplay', (e) => {
if(this.option.autoplay && !this.isMobile){
this.play();
}
});
this.audio.addEventListener('timeupdate', (e) => {
let percent = this.audio.currentTime / this.audio.duration;
this.dom.timeline_played.style.width = Util.percentFormat(percent);
this.dom.timetext_played.innerHTML = Util.timeFormat(this.audio.currentTime);
});
//this.audio.addEventListener('seeked', (e) => {
// this.play();
//});
this.audio.addEventListener('ended', (e) => {
this.next();
}); this.dom.playbutton.addEventListener('click', this.toggle);
this.dom.switchbutton.addEventListener('click', this.toggleList);
if(!this.isMobile){
this.dom.volumebutton.addEventListener('click', this.toggleMute);
}
this.dom.modebutton.addEventListener('click', this.switchMode);
this.dom.musiclist.addEventListener('click', (e) => {
let target,index,curIndex;
if(e.target.tagName.toUpperCase() === 'LI'){
target = e.target;
}else{
target = e.target.parentElement;
}
index = parseInt(target.getAttribute('data-index'));
curIndex = parseInt(this.dom.musiclist.querySelector('.skPlayer-curMusic').getAttribute('data-index'));
if(index === curIndex){
this.play();
}else{
this.switchMusic(index + 1);
}
});
this.dom.timeline_total.addEventListener('click', (event) => {
let e = event || window.event;
let percent = (e.clientX - Util.leftDistance(this.dom.timeline_total)) / this.dom.timeline_total.clientWidth;
if(!isNaN(this.audio.duration)){
this.dom.timeline_played.style.width = Util.percentFormat(percent);
this.dom.timetext_played.innerHTML = Util.timeFormat(percent * this.audio.duration);
this.audio.currentTime = percent * this.audio.duration;
}
});
if(!this.isMobile){
this.dom.volumeline_total.addEventListener('click', (event) => {
let e = event || window.event;
let percent = (e.clientX - Util.leftDistance(this.dom.volumeline_total)) / this.dom.volumeline_total.clientWidth;
this.dom.volumeline_value.style.width = Util.percentFormat(percent);
this.audio.volume = percent;
if(this.audio.muted){
this.toggleMute();
}
});
}
}至此,核心代码基本完成,接下来就是自己根据需要完成API部分,详细部分移步至我的github查看源码。
最后我们暴露模块:module.exports = skPlayer;
一个HTML5音乐播放器就大功告成了 ~ !
4个小时实现一个HTML5音乐播放器的更多相关文章
- 用<audio>标签打造一个属于自己的HTML5音乐播放器
上一章节,我们刚刚讲了<video>标签,今晚,我们讲的是<audio>标签,这两个东东除了表示的内容不一样以外,其他的特性相似的地方真的太多了,属性和用法几乎一样,也就说,如 ...
- jqm视频播放器,html5视频播放器,html5音乐播放器,html5媒体播放器,video开展demo,html5视频播放演示示例,html5移动视频播放器
最近看到很多有用的论坛html5视频播放的发展,音乐播放功能,大多数都在寻找答案.所以,我在这里做一个demo.对于大家互相学习.html5开发越来越流行,至于这也是一个不可缺少的一部分的视频. 如何 ...
- HTML5项目笔记4:使用Audio API设计绚丽的HTML5音乐播放器
HTML5 有两个很炫的元素,就是Audio和 Video,可以用他们在页面上创建音频播放器和视频播放器,制作一些效果很不错的应用. 无论是视屏还是音频,都是一个容器文件,包含了一些音频轨道,视频轨道 ...
- 使用Audio API设计绚丽的HTML5音乐播放器
HTML5 有两个很炫的元素,就是Audio和 Video,可以用他们在页面上创建音频播放器和视频播放器,制作一些效果很不错的应用. 无论是视屏还是音频,都是一个容器文件,包含了一些音频轨道,视频轨道 ...
- 简约的HTML5音乐播放器插件
从我刚开始接触前端的时候就想写一个能播放音乐的小程序,刚开始写的时候虽然可以放,但是确实很慢,很卡,有很多可以优化的地方.最近在前一个版本的基础上重写了一个音乐播放器的插件,速度还可以吧 因为追求简约 ...
- [ 原创 ]学习笔记-做一个Android音乐播放器是遇到的一些困难
最近再做一个安卓的音乐播放器,是实验室里学长派的任务,我是在eclipse上进行开发的,由于没有android的基础,所以做起来困难重重. 首先是布局上的困难 1.layout里的控件属性不熟悉 2. ...
- 一个开源音乐播放器,低仿QQ音乐!
有暇,弄了个音乐播放器,页面效果整体上参考了QQ音乐,相关API使用了易源数据提供的相关接口(https://www.showapi.com/api/lookPoint/213),在此表示感谢.先来看 ...
- 一款好看+极简到不行的HTML5音乐播放器-skPlayer
Demo: github skPlayer在线预览 预览: 单曲循环模式预览: 使用方法: 方式1:NPM npm install skplayer 方式2:引入文件 引入css文件: <lin ...
- HTML5音乐播放器(最新升级改造加强版)
最近么,单位里面么老不顺心的,公司一直催要程序员要PHP,然后本宅好不容易推荐了一个,我日嘞,最后待遇变成1.3,吾师最后也同意1.3W,然后还说要考虑... 尼玛,4年多5年不到一点的工作经验,前端 ...
随机推荐
- JavaScript性能优化 DOM编程
最近在研读<高性能JavaScript>,在此做些简单记录.示例代码可在此处查看到. 一.DOM 1)DOM和JavaScript 文档对象模型(DOM)是一个独立于语言的,用于操作XML ...
- iOS 证书详情介绍
1.Certification(证书)安装证书后使这台电脑就有开发APP和真机调试的功能. 证书分为开发证书(Developer Certification)和发布证书(Distribution Ce ...
- Bash Scripting Learn Notes
References: [1] http://www.tldp.org/LDP/Bash-Beginners-Guide/html/ 1. Executing programs from a scri ...
- HTML5 进阶系列:canvas 动态图表
前言 canvas 强大的功能让它成为了 HTML5 中非常重要的部分,至于它是什么,这里就不需要我多作介绍了.而可视化图表,则是 canvas 强大功能的表现之一. 现在已经有了很多成熟的图表插件都 ...
- javaWeb学习总结(8)- jsp指令(3)
一.JSP指令简介 一.JSP指令简介 JSP指令(directive)是为JSP引擎而设计的,它们并不直接产生任何可见输出,而只是告诉引擎如何处理JSP页面中的其余部分. 在JSP 2.0规范中共定 ...
- Dubbo微容器(Cooma)详解
ExtensionLoader ExtensionLoader是Dubbo中的SPI的实现方法,它是Dubbo框架的微容器,也为框架提供各种组件的扩展点 三种注解 SPI Adaptive Activ ...
- RFM模型——构建数据库营销的商业战役!(转)
RFM模型:R(Recency)表示客户最近一次购买的时间有多远,F(Frequency)表示客户在最近一段时间内购买的次数,M (Monetary)表示客户在最近一段时间内购买的金额.一般原始数据为 ...
- 03 编译安装apache的简易配置
1.监听端口,默认为80,在主配置文件 /etc/httpd24/httpd.conf中可以更改 2.持久连接 Include /etc/httpd24/extra/httpd-default.con ...
- 【JAVAEE学习笔记】hibernate03:多表操作详解、级联、关系维护和练习:添加联系人
一.一对多|多对一 1.关系表达 表中的表达 实体中的表达 orm元数据中表达 一对多 <!-- 集合,一对多关系,在配置文件中配置 --> <!-- name属性:集合属性名 co ...
- selenium+python定位元素方法
定位元素方法 官网地址:http://selenium-python.readthedocs.org/locating-elements.html 这里有各种策略用于定位网页中的元素(l ...