技術點:ES6+Webpack+HTML5 Audio+Sass
這裡,我們将一步步的學到如何從零去實作一個H5音樂播放器。
首先來看一下最終的實作效果:Demo連結 =>
界面:

skPlayer
接下來就步入正題:
要做一個音樂播放器就要非常了解在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為例進行分解

skPlayer
從界面中可以看出一個播放器所需要的最基礎功能:
播放/暫停、封面/歌名/歌手的顯示、播放進度條/加載進度條/進度操作功能、循環模式切換、進度文字更新/歌曲時長、靜音/音量大小控制、清單顯示狀态控制、點選清單項切歌功能
再結合我們想要滿足使用者需求,提供配置項和API的出發點可以得出我們想設計的配置項和暴露的API項:
配置項:自動播放是否開啟、預設歌曲清單的顯示狀态、預設循環模式的設定
API:播放/暫停/toggle、循環模式的切換、靜音/恢複、清單顯示狀态的切換、上一曲/下一曲/切歌、銷毀目前執行個體
确立項目結構,開始編碼:
因為使用webpack,是以我們直接将css打包至js内,以便作為插件供使用者使用:
require('./skPlayer.scss');
抽離公共方法,在播放器中有很多可能需要抽離的公共方法如:點選播放進度條和音量進度條時需要計算滑鼠距離進度條左端的距離以進行進度跳轉,時間從duration中擷取到的以秒為機關的時間轉換成标準時間格式等等:
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);
}
};
由于設計之初,考慮到播放器的獨特性,設計為隻能存在一個執行個體,設定了一個全局變量以判斷目前是否存在執行個體:
(執行個體判斷,如果存在傳回無原型的空對象,因為ES6構造函數内預設傳回帶原型的執行個體)
let instance = false;
if(instance){
console.error('SKPlayer隻能存在一個執行個體!');
return Object.create(null);
}else{
instance = true;
}
在使用ES6的情況下,我們将主邏輯放在構造函數内部,将通用性強和API放在公共函數内部:
class skPlayer {
constructor(option){}
template(){}
init(){}
bind(){}
prev(){}
next(){}
switchMusic(index){}
play(){}
pause(){}
toggle(){}
toggleList(){}
toggleMute(){}
switchMode(){}
destroy(){}
}
初始化配置項,預設配置與使用者配置合并:
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();
}
});
}
}
最後我們暴露子產品:
module.exports = skPlayer;
至此,核心代碼基本完成,接下來就是自己根據需要完成API部分,詳細部分移步至我的github檢視源碼。
一個HTML5音樂播放器就大功告成了 ~ !