ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用JavaScript实现HTML5音乐播放器:播放、暂停、重听与停止

用JavaScript实现HTML5音乐播放器:播放、暂停、重听与停止 前两篇我们把播放器界面搭起来了HTML5 负责结构、CSS 负责长相结果你发现按钮全是“装饰品”点过去点过来一点反应都没有。别急这太正常了因为交互压根还没写。今天这篇就解决这个问题用 JavaScript 给这些按钮接上“电路”让 mp3 音乐播放器真正能播、能停、能重听、能复位。如果你是从中间才看到这个系列也没关系HTML 和 CSS 可以直接拿来用重点在后面 JS 的核心思路。我默认你已经知道 HTML5 里audio标签是什么也写过几行 JavaScript 函数。如果你完全没碰过 JS这篇文章也能跟下来我不会堆术语但该讲清楚的原理一个都不少。1. 交互设计拆解播放器的四种状态与按钮分工1.1 先想清楚交互的本质是状态切换写代码之前我习惯先在脑子里跑一遍流程。音乐播放器的交互说白了就是“状态切换”。我家里的灯不可能同时处于“开”和“关”两个状态播放器也一样。一个最简单的播放器全生命周期只有四种状态初始状态页面打开音频还没播过按钮显示“播放”。播放中音乐正在响按钮应该变成“暂停”。暂停状态音乐在中间某个位置停住了再点播放应该从停住的地方继续。停止状态播放位置回到开头音频不再响按钮恢复成“播放”。你可能会问暂停和停止听起来没什么区别区别很大后面我专门讲。这里先建立一个核心概念每次点击按钮本质上是“读取当前状态再决定把它变成什么状态”。比如你点“播放”程序要先判断现在是不是暂停或停止状态如果是才执行播放如果你在播放中点了它那就不是播放了而是暂停。这种思考方式叫状态机思维不是什么高深理论就是让程序有“记忆”。很多新手写播放器喜欢在每个点击事件里写一大坨逻辑结果按钮越加越多逻辑绕成一团麻。我的习惯是先画出状态和转移关系再动手写代码后面加进度条、音量条、歌词同步都不慌。1.2 四个按钮各自的职责我们的控制栏一共就四个核心操作播放、暂停、重听、停止。但注意播放和暂停通常共用一个按钮因为这两个动作是互斥的要么在播放要么没在播放。所以按钮文字要在“播放”和“暂停”之间来回切。具体分工我整理成了表按钮当前状态点击后的行为按钮文案/图标变化播放/暂停暂停或停止开始播放变为“暂停”播放/暂停播放中暂停保留当前位置变为“播放”重听任意状态回到 0 秒并从开头播放变为“暂停”停止任意状态回到 0 秒且不再播放变为“播放”这里有一个细节重听和停止在不同状态下点击结果的“目标状态”是不同的比如在暂停时点重听结果是播放状态在播放中点停止结果是停止初始状态。而播放/暂停按钮点击后的结果取决于当前状态。这就是为什么一定要先判断状态再执行动作而不是写死一个行为。1.3 重听和停止最容易搞混的一对先说说“重听”。重听的意思是从头开始播放它和“先暂停再点播放”不是一回事。你在播放中间点了重听音频会瞬间回到 0 秒位置再从头响起你在暂停状态点了重听也是立刻从头响起。实现上就两句话把currentTime设成 0然后调用play()。再说“停止”。停止是让整个播放器“复位”音乐不响了播放位置回到最开头按钮文案恢复成“播放”。它和暂停最大的区别在于暂停之后位置还停在原地所以你再点播放声音从原地继续停止之后位置已经归零你再点播放就是从开头重新来。你可以想象一台老式磁带机按“暂停”是齿轮卡住、磁带还在原地按“停止”是磁头抬起、机器归位按“重听”是倒带后重新放。这个类比放到代码里非常精确。1.4 为什么要把每个功能拆成独立函数我不会把所有逻辑塞进一个点击事件里而是每个动作对应一个函数playAudio()、pauseAudio()、restartAudio()、stopAudio()。这样做的直接好处是以后在别的地方也能复用这些函数比如键盘空格键控制播放暂停、歌词点击跳转、播放列表双击切歌都能直接调用不需要重新写一遍逻辑。JavaScript 里函数是组织代码的基本单位这个习惯越早养越好。2. audio 元素与事件模型播放器的心脏2.1 为什么选 HTML5 的 audio 而不是自己玩音频你可能觉得播放 mp3 是件很复杂的事要处理解码、缓冲、声道输出什么的。但 HTML5 的audio标签把这些全部封装好了你只需要告诉浏览器“我要放这个文件”然后调用几个方法就行。它和video是同一批标准里的东西将来你想把播放器扩展成视频播放器核心逻辑几乎不用改。音频解码这块浏览器会自己去处理。你用audio播放 mp3、wav、ogg 都没问题不需要写解码器也不需要管音频流的网络请求。你的工作只有一件事情调用 API。2.2 与播放相关的核心属性和方法列一下我们今天用得上的 API后面写代码都靠它们API类型作用注意事项play()方法开始或恢复播放返回一个 Promise可能被浏览器拒绝pause()方法暂停播放保留当前位置不会重置播放位置load()方法重新加载音频资源会中断当前播放一般用于复位或换源currentTime属性获取/设置当前播放位置单位秒必须在元数据加载完成后设置paused属性布尔值当前是否暂停这是系统状态比你自己定义的变量可靠ended属性布尔值是否播放完毕监听ended事件更常用duration属性音频总时长单位秒元数据加载完之前是 NaNplaybackRate属性播放倍速默认 1想倍速听歌/听课就改这个其中play()是唯一一个会“出声”的方法pause()只是让声音停住而load()会重新走一遍加载流程所以它有重置效果但代价是重新发请求比较重。日常复位我更推荐pause()加currentTime 0组合而不是每次都load()。2.3 自动播放限制为什么不能一进来就响很多新手一上来就在window.onload里写audio.play()结果控制台报一个类似NotAllowedError的错误一脸懵。这是浏览器故意的不允许页面在用户没有交互的情况下自动出声防止你一打开某个网页就被迫听广告或噪音。解决办法很简单所有play()都必须在用户手势的上下文中触发比如按钮的点击回调、键盘事件。我们的播放器每个操作都是点击按钮触发的天然满足条件所以不会有问题。但如果你在加载完音频后异步回调里调用play()就可能会被拦。后面“常见问题”里我会专门提到排查方法。另外play()返回的 Promise 一定要接个catch因为一旦被拒绝却没人处理控制台会一直报红色错误。这在排查问题的时候特别有用能第一时间看出是不是自动播放策略的问题。3. 从零实现HTML 结构 JS 逻辑完整代码3.1 控制栏的 HTML 结构我假设你已经有一个漂亮的播放器外壳了这次我们聚焦控制栏。新建一个index.html核心结构大概长这样!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title我的 MP3 播放器/title style /* 样式见 3.2这里省略 */ /style /head body div classplayer div classcover idcover封面区域/div div classinfo div classsong-name idsongName未知歌曲/div div classsong-status idsongStatus未播放/div /div audio idaudio srcmusic.mp3 preloadmetadata/audio div classcontrols button idrestartBtn classctrl-btn重听/button button idplayBtn classctrl-btn primary播放/button button idstopBtn classctrl-btn停止/button /div /div script // JS 见 3.3 /script /body /html注意我给audio加了preloadmetadata意思是页面加载时先拉取音频的元数据时长、封面等但不会下载整个文件更不会自动播放。这是个很实用的属性能让你在用户点击播放之前就拿到duration。3.2 状态反馈的 CSS 小技巧涟漪光圈扩散光有逻辑还不够用户点击按钮后界面得给他反馈。最直观的反馈就是按钮文字切换比如“播放”变“暂停”。我还会加一个点击涟漪效果网上很火的那个“css涟漪光圈扩散”其实就是用:active配合box-shadow扩散做出来的不需要额外引入库。.ctrl-btn { padding: 10px 24px; border: none; border-radius: 24px; background: #2c3e50; color: #fff; font-size: 16px; cursor: pointer; transition: all 0.2s ease; outline: none; position: relative; } .ctrl-btn:active { transform: scale(0.96); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.5); animation: ripple 0.6s ease-out; } keyframes ripple { 0% { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.5); } 100% { box-shadow: 0 0 0 20px rgba(255, 255, 255, 0); } } .ctrl-btn.primary { background: #e74c3c; min-width: 90px; } .ctrl-btn.is-play { background: #3498db; }视觉效果上你按下按钮时有一圈光圈从边缘扩散开然后淡出像是在水面上丢了一颗石子。代码不复杂但能给用户很好的“我点到了”的反馈。按钮文字切换我会放到 JS 里同步处理。3.3 JS 交互逻辑逐段拆解接下来是重头戏。我先把完整 JS 写出来然后一段一段讲清楚为什么这么写。const audio document.getElementById(audio); const playBtn document.getElementById(playBtn); const restartBtn document.getElementById(restartBtn); const stopBtn document.getElementById(stopBtn); const songStatus document.getElementById(songStatus); // 1. 播放 / 暂停切换 function togglePlay() { if (audio.paused) { audio.play().catch(err { songStatus.textContent 播放被拦截 err.message; }); } else { audio.pause(); } } // 2. 重听回到开头并播放 function restartAudio() { audio.currentTime 0; audio.play().catch(err { songStatus.textContent 播放失败 err.message; }); } // 3. 停止回到开头并暂停复位 UI function stopAudio() { audio.pause(); audio.currentTime 0; } // 4. 统一更新按钮和状态文本 function syncUI() { if (audio.paused) { playBtn.textContent 播放; playBtn.classList.remove(is-play); songStatus.textContent 已暂停; if (audio.currentTime 0) { songStatus.textContent 未播放; } } else { playBtn.textContent 暂停; playBtn.classList.add(is-play); songStatus.textContent 播放中 formatTime(audio.currentTime); } } // 5. 格式化时间秒 - 分:秒 function formatTime(seconds) { if (isNaN(seconds)) return 0:00; const min Math.floor(seconds / 60); const sec Math.floor(seconds % 60); return min : (sec 10 ? 0 sec : sec); } // 6. 事件绑定 playBtn.addEventListener(click, togglePlay); restartBtn.addEventListener(click, restartAudio); stopBtn.addEventListener(click, stopAudio); // 7. 监听 audio 的系统事件统一刷新 UI audio.addEventListener(play, syncUI); audio.addEventListener(pause, syncUI); audio.addEventListener(ended, function() { audio.currentTime 0; syncUI(); });下面我拆几点关键的。为什么用audio.paused而不是自己定义布尔变量因为audio.paused是浏览器帮你维护的“当前真实状态”。比如你在播放音乐时来了个电话系统会把音频自动暂停audio.paused会自动变成true但你自己定义的isPlaying变量还是trueUI 就会骗人。用系统状态判断你的 UI 永远和真实行为同步。这也是为什么我不建议第一版就搞一堆自定义状态变量先信任浏览器的状态后面有复杂需求再引入状态机。为什么 UI 更新写在play/pause事件里而不是点击回调里因为audio在被系统暂停、被ended结束、被load()打断时都会触发pause事件。只要 UI 更新集中在事件监听里任何原因导致的状态变化都会被正确反映到界面上。你如果只在点击回调里改按钮文字遇到系统自动暂停就露馅了。formatTime是干什么的这是给状态栏用的。audio.currentTime返回的是秒比如一秒钟可能是83.40921直接显示给用户看非常不友好。formatTime把它转成1:23这种格式。同时注意duration和currentTime在元数据加载完成之前是NaN方法里我先用isNaN判断一下避免显示NaN:NaN。关于typeof的调试技巧写 JavaScript 时最怕类型搞混。新手常见的问题是把audio.paused当成字符串使用比如if (audio.paused true)这样永远进不去。调试的时候你可以在控制台敲一句console.log(typeof audio.paused)确认它返回的是boolean然后再判断。JavaScript 判断数据类型的方法是基本功排查这种诡异 bug 非常管用。3.4 完整可运行代码把上面所有内容合到一起就是一个可以直接保存成index.html、放一个music.mp3在同目录就能运行的单文件播放器。完整代码如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title我的 MP3 播放器/title style * { box-sizing: border-box; margin: 0; padding: 0; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #1a1a2e; font-family: Arial, sans-serif; } .player { width: 420px; background: #16213e; border-radius: 16px; padding: 24px; color: #fff; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5); } .cover { height: 180px; background: linear-gradient(135deg, #e94560, #533483); border-radius: 8px; display: flex; justify-content: center; align-items: center; font-size: 18px; color: rgba(255, 255, 255, 0.8); margin-bottom: 16px; } .info { margin-bottom: 16px; } .song-name { font-size: 20px; font-weight: bold; margin-bottom: 6px; } .song-status { font-size: 14px; color: rgba(255, 255, 255, 0.6); } .controls { display: flex; gap: 12px; } .ctrl-btn { flex: 1; padding: 12px 0; border: none; border-radius: 24px; background: #2c3e50; color: #fff; font-size: 16px; cursor: pointer; transition: all 0.2s ease; outline: none; position: relative; text-align: center; } .ctrl-btn:hover { background: #34495e; } .ctrl-btn:active { transform: scale(0.96); animation: ripple 0.5s ease-out; } .ctrl-btn.primary { background: #e94560; min-width: 110px; } .ctrl-btn.primary:hover { background: #c0392b; } keyframes ripple { 0% { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.45); } 100% { box-shadow: 0 0 0 18px rgba(255, 255, 255, 0); } } /style /head body div classplayer div classcover封面区域/div div classinfo div classsong-name idsongName本地歌曲/div div classsong-status idsongStatus未播放/div /div audio idaudio srcmusic.mp3 preloadmetadata/audio div classcontrols button idrestartBtn classctrl-btn重听/button button idplayBtn classctrl-btn primary播放/button button idstopBtn classctrl-btn停止/button /div /div script const audio document.getElementById(audio); const playBtn document.getElementById(playBtn); const restartBtn document.getElementById(restartBtn); const stopBtn document.getElementById(stopBtn); const songStatus document.getElementById(songStatus); function togglePlay() { if (audio.paused) { audio.play().catch(err { songStatus.textContent 播放被拦截 err.message; }); } else { audio.pause(); } } function restartAudio() { audio.currentTime 0; audio.play().catch(err { songStatus.textContent 播放失败 err.message; }); } function stopAudio() { audio.pause(); audio.currentTime 0; } function formatTime(seconds) { if (isNaN(seconds)) return 0:00; const min Math.floor(seconds / 60); const sec Math.floor(seconds % 60); return min : (sec 10 ? 0 sec : sec); } function syncUI() { if (audio.paused) { playBtn.textContent 播放; songStatus.textContent 已暂停; if (audio.currentTime 0) { songStatus.textContent 未播放; } } else { playBtn.textContent 暂停; songStatus.textContent 播放中 formatTime(audio.currentTime); } } playBtn.addEventListener(click, togglePlay); restartBtn.addEventListener(click, restartAudio); stopBtn.addEventListener(click, stopAudio); audio.addEventListener(play, syncUI); audio.addEventListener(pause, syncUI); audio.addEventListener(ended, function() { audio.currentTime 0; syncUI(); }); /script /body /html你把这个文件保存好在它旁边放一个music.mp3双击用浏览器打开四个功能就都能用了。第一首歌来源不重要自己电脑里随便一首没版权的歌都行大不了录一段自己的声音当测试素材。3.5 手动测试清单写交互最怕“感觉能用了”实际上某个边界没覆盖。我每次写完播放器都会手动过一遍这个清单打开页面状态显示“未播放”按钮文字是“播放”。点击播放声音响起按钮变成“暂停”状态显示“播放中 0:01”。再次点击声音停住按钮变回“播放”状态显示“已暂停”。暂停状态下点播放声音从刚才停住的位置继续。播放中点“重听”声音立刻回到开头重新播放按钮显示“暂停”。暂停中点“重听”也是从开头重新播放而不是停留在原地。任何状态下点“停止”声音停止按钮变为“播放”状态变为“未播放”。让它一直播到歌曲结束状态自动变为“未播放”按钮恢复“播放”。清单走完基本功能就是稳的。4. 常见问题与排查实录写代码不踩坑是不可能的下面这些是我在实现类似功能时真实遇到过的问题整理成速查表你对照排查会快很多。现象可能原因排查方法 / 解决方案点击播放完全没反应控制台有红色报错play()被浏览器拦截或 JS 运行时报错打开控制台看具体报错多数是NotAllowedError确认是否在用户手势里调用按钮能切换但声音不响 / 爆音src路径不对或音频格式不支持控制台 Network 面板看音频请求是否 404换一首 mp3 试设置currentTime 0没效果在元数据加载前就设置最好在loadedmetadata事件回调里设置或直接用load()重置停止后马上点播放总觉得慢半拍停止时用了load()导致重新加载资源改用pause()currentTime 0除非你要换歌多次快速点击按钮文字和实际状态不一致UI 更新散落在各点击回调里统一在audio的play/pause事件里调用syncUI()播放到末尾后按钮还显示“暂停”没有处理ended事件监听ended在回调里归零并刷新 UI音频文件跨域导致无法播放服务器没配跨域头本地文件一般没这个问题线上部署建议用同源路径页面打开时自动播放失败浏览器自动播放策略不要在页面加载时直接调play()必须由用户手势触发4.1 点击播放没反应先看控制台报错新手犯的最多的错就是忽略控制台。我见过不少人问“为什么我的播放器点了没声音”结果打开控制台一看明明写着Uncaught ReferenceError: audio is not defined。这一眼就知道是id写错或 JS 在 HTML 加载之前执行了。我们这里把script放在/body之前所以不存在 DOM 未加载问题。如果 JS 要放head里就得包window.onload或DOMContentLoaded。还有一种情况你点了按钮play()返回的 Promise 被 reject 了但你没有.catch()控制台就显示Uncaught (in promise) NotAllowedError。所以每个.play()后面我都要加 catch。很多 JavaScript 运行时报错都可以靠 catch 拿到更友好的提示然后在界面上显示出来而不是让用户面对一个死按钮。4.2 currentTime 设置不生效的坑currentTime是很常用的属性重听、停止、进度条跳转都要靠它。但它有一个前置条件音频的元数据已经加载完成。你在页面加载后立刻写audio.currentTime 30很可能没反应因为音频时长还不知道浏览器不知道 30 秒是否合法。稳妥做法是监听一次loadedmetadataaudio.addEventListener(loadedmetadata, function() { console.log(音频总时长 audio.duration); });有了这个事件你可以在拿到总时长后再做跳转逻辑。另外给currentTime赋非法值比如负数、超过时长的数会被浏览器静默忽略不会报错所以也别指望它提示你。4.3 快速连点导致按钮状态错乱我在测试时特别喜欢狂点播放暂停按钮目的是看 UI 会不会乱掉。如果你把所有 UI 更新都写在点击回调里快速连点特别容易导致按钮文字和实际状态不一致。比如你点了一下调用play()play()还没成功你又点了一下调用了pause()这时候 UI 可能停在“暂停”实际音频却开始播了。解决思路我在代码里已经说了UI 不跟着点击走跟着audio的系统事件走。play和pause事件代表了音频真实的“开始播放”和“暂停”状态只要在这两个事件里统一刷新按钮不管点击多快UI 永远和真实状态对齐。这是我在做交互时最推荐的一个习惯。4.4 停止后重新播放明显变慢有些播放器一启动就preloadauto把整个文件都预加载了这样点停止后立即播放很快。但如果你用的是preloadmetadata并且停止时调用了load()就会重新发起网络请求表现就是点播放后要等一会儿才有声。尤其是 mp3 文件比较大、网络一般的时候体验很差。我现在的做法是同一首歌的“停止”不用load()而是pause()加currentTime 0。这样音频数据还在内存里再次播放几乎是秒开。只有切歌或释放资源时才考虑load()。如果你想更彻底地复位把audio.removeAttribute(src)配合load()也能做但日常用不上。4.5 音频文件加载失败与兜底提示本地开发时如果music.mp3不存在点击播放会触发error事件控制台报 404但界面上可能什么都没发生。为了不让用户一头雾水可以监听error事件在状态栏里给出提示audio.addEventListener(error, function() { songStatus.textContent 音频加载失败请检查文件路径; });另外多说一句如果你的播放器以后要接入在线音频接口注意跨域问题。播放本身一般不受 CORS 限制但如果你想用 Web Audio API 做频谱可视化就是那种跟着音乐跳动的柱状图canvas是画图的关键就需要服务端配合跨域头。这个到后面做进阶功能时再聊。最后分享一点个人体会。我第一次做播放器时按钮文字、状态栏、封面动画全在各自的点击回调里更新结果加进度条的时候改得焦头烂额。后来我悟出一个习惯所有和播放器状态有关的界面变化都收敛到一个syncUI()函数里让系统事件统一推动它。这个习惯救了我很多次你以后加音量条、倍速按钮、歌词高亮都会用得上。顺带说一句倍速功能其实特别简单audio.playbackRate 1.5就能实现 HTML5 音频倍速和视频倍速是一个 API。这篇先到这下一篇我会继续做播放进度条和音量控制到时候把currentTime、duration、syncUI()这些基础再往上垒。
返回列表