4個小時實現一個HTML5音樂播放器

来源:http://www.cnblogs.com/Scott-se7en/archive/2017/07/08/7137522.html
-Advertisement-
Play Games

技術點:ES6+Webpack+HTML5 Audio+Sass 這裡,我們將一步步的學到如何從零去實現一個H5音樂播放器。 首先來看一下最終的實現效果:Demo鏈接界面: 接下來就步入正題: 抽離公共方法,在播放器中有很多可能需要抽離的公共方法如:點擊播放進度條和音量進度條時需要計算滑鼠距離進度條 ...


技術點:ES6+Webpack+HTML5 Audio+Sass

這裡,我們將一步步的學到如何從零去實現一個H5音樂播放器。

首先來看一下最終的實現效果:Demo鏈接
界面:

 

 

接下來就步入正題:

  1. 要做一個音樂播放器就要非常瞭解在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等等;


  2. 確定整體結構:
    因為自己是做成插件的方式發佈在npm上供他人使用的,所以我們就採用面向對象的方式進行代碼編寫,又因為用戶的需求不一,所以在設計之初就暴露出大量的API和配置項以滿足大部分用戶的需求。
    這裡因為自己更習慣es6的語法,就全程以es6為基礎進行開發,同時為了開發效率,又使用了sass進行css的編寫,最後還使用了webpack和webpack-dev-server用以編譯es6和sass,項目打包,構建本地伺服器。


  3. 確定播放器UI和交互:
    可能關於界面每個人有自己的想法,這裡就不過多贅述了,以我做好的播放器UI為例進行分解

    從界面中可以看出一個播放器所需要的最基礎功能:
    播放/暫停、封面/歌名/歌手的顯示、播放進度條/載入進度條/進度操作功能、迴圈模式切換、進度文字更新/歌曲時長、靜音/音量大小控制、列表顯示狀態控制、點擊列表項切歌功能
    再結合我們想要滿足用戶需求,提供配置項和API的出發點可以得出我們想設計的配置項和暴露的API項:
    配置項:自動播放是否開啟、預設歌曲列表的顯示狀態、預設迴圈模式的設置
    API:播放/暫停/toggle、迴圈模式的切換、靜音/恢復、列表顯示狀態的切換、上一曲/下一曲/切歌、銷毀當前實例


  4. 確立項目結構,開始編碼:
    因為使用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);
        }
    };
    View Code

    由於設計之初,考慮到播放器的獨特性,設計為只能存在一個實例,設置了一個全局變數以判斷當前是否存在實例:

    let instance = false;

    在使用ES6的情況下,我們將主邏輯放在構造函數內部,將通用性強和API放在公共函數內部:

    class skPlayer {
        constructor(option){
        }
    
        template(){
        }
    
        init(){
        }
    
        bind(){
        }
    
        prev(){
        }
    
        next(){
        }
    
        switchMusic(index){
        }
    
        play(){
        }
    
        pause(){
        }
    
        toggle(){
        }
    
        toggleList(){
        }
    
        toggleMute(){
        }
    
        switchMode(){
        }
    
        destroy(){
        }
    }
    View Code

    實例判斷,如果存在返回無原型的空對象,因為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';
        }
    View Code

    事件綁定,主要綁定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();
                    }
                });
            }
        }
    View Code

    至此,核心代碼基本完成,接下來就是自己根據需要完成API部分,詳細部分移步至我的github查看源碼。
    最後我們暴露模塊:

    module.exports = skPlayer;

    一個HTML5音樂播放器就大功告成了 ~ !


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • numbers類型: 數字類型的數據有Byte,Short,Float,Int,Long,Double,這些類型在java中也都是基礎數據類型。 與java不同之處在於: Char類型已經不再是數字類型了。 在java之中基礎數據類型都對應有一個包裝類,如int,對應Integer,而在kotlin ...
  • JavaScript 是世界上最流行的,輕量級的編程語言。 這門語言可用於 HTML 和 web,更可廣泛用於伺服器、PC、筆記本電腦、平板電腦和智能手機等設備; JavaScript 被數百萬計的網頁用來改進設計、驗證表單、檢測瀏覽器、創建cookies,以及更多的應用。 系統的知識圖解: 在we ...
  • 他們可以存儲: 數組 json數據 圖片 腳本 樣式文件 ; 客戶端的存儲的兩個: 1.localStorage 沒時間限制的數據存儲() 方法有:.localStrage.getItem();localStrage.setItem();removeItem();localStrage.key()從 ...
  • JScookie 常用的3個預設函數(庫)無標題文檔記住用戶名學術或足球分析交流微信:chinamaths(進討論組) Don't hesitate to comment or add a like - Yours Bill Bill's技術博客 足球分析博客 足彩數據視頻 比爾極客日誌_博客園 比... ...
  • <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappe ...
  • 代碼和特性在chrome49下測試有效。 文本渲染的本質是對文本節點的渲染,通過瀏覽器內置的對象Range可以獲得選擇的起始點、與終止點 getRangeObjec代碼如下 function getRangeObject(){ if(window.getSelection) { var select ...
  • flex佈局可以幫我們快速佈局一些區塊,實現你想要的效果,不用再去float,position之類的.我們在佈局網頁的時候很多時候都是一些特殊佈局,flex就能幫我快速去佈局,不需要去定位. 任何一個盒子都可以指定為flex佈局,但是要註意,設為 Flex 佈局以後,子元素的float、clear和 ...
  • canvas是html5的新標簽,是個可以繪製圖形的畫布,畫布的預設大小為300x150。在自定義繪製畫布大小的時候有註意的問題,就是 用樣式來設置高度和寬度的時候 比如 您的瀏覽器不支持H5畫布屬性 var canvas = document.getElementById("canvas"); v ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...