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年不到一点的工作经验,前端 ...
随机推荐
- AFNetworking 动态修改acceptableContentTypes 设置ContentType
AFJSONResponseSerializer+Serializer.h #import <AFNetworking/AFNetworking.h> @interface AFJSONR ...
- 关于SESSION失效和关闭浏览器问题
关闭浏览器和session失效没有任何关系, session本身有一个存活时间,在tomcat中默认的是30分钟, 这也就是楼上说的不是马上失效 但和浏览器不要划等号 因为即使你浏览器一直开着,如 ...
- Cohort Analysis and LifeCycle Grids mixed segmentation with R(转)
This is the third post about LifeCycle Grids. You can find the first post about the sense of LifeCyc ...
- VR全景智慧城市:开启VR全景逛街新时代~
VR全景,又被称为3D实景,是一种新兴的富媒体技术,其与视频,声音,图片等传统的流媒体大的区别是"可操作,可交互". 对于顾客体验来说,VR确实是对于实体店是一种颠覆性的创新,它既 ...
- Scrapy的debug方式
Scrapy不方便调试,但是为了深入学习框架内部的一些原理,有时候仅仅依靠日志是不够的.下面提供一种scrapy的debug方式 demo直接用来自官方例子来演示:https://github.com ...
- (计蒜客)UCloud 的安全秘钥
UCloud 的安全秘钥 题意 给出一个数组 s 串,和数组 t 串,那么如果两者长度相同且两者所含的数字全部相同,则说这两个串相似. 给定原始串 S ,以及 m 个询问 T 串,问 S 串有多少个连 ...
- UVALive-5731
UVALive-5731 题意 一颗 n - 1 条边的有向树,要求整棵树成为强连通图,一次操作即构建一条路(一笔画), 限制: 新建的路上的所有边必须与原有的边逆向,即构建的路必须在原有的边和点上, ...
- 用Python玩微信(非常详细)
代码放在这里:wzyonggege/python-wechat-itchat 词云那里可以换成小黄人图片 ----------------------------------------------- ...
- re 学习随便
. 任意一个字符 \转义字符 * 字符重复0--多次 + 字符重复1-多次 ? 字符重复0-1次 ^行首匹配 或者在一个字符集中表示取反 \$ 匹配字符串末尾 \b 匹配\w 与\w 之间的 \B ...
- EJS模板引擎
前面的话 nodejs的模板引擎有很多, EJS是比较简单和容易上手的.本文将详细介绍EJS 概述 EJS是一个简单高效的模板语言,通过数据和模板,可以生成HTML标记文本.可以说EJS是一个Java ...