ARTICLE DETAIL

资讯详情

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

用JavaScript实现MP3播放器的播放暂停、重听与停止

用JavaScript实现MP3播放器的播放暂停、重听与停止 这是系列教程的第三篇。前两篇我们分别用HTML5搭好了播放器的骨架用CSS把它打扮得像个正经软件一个长得有模有样的mp3免费音乐播放器已经能在浏览器里渲染出来了。但说句实话那时候点任何按钮都是没反应的因为它还缺一个灵魂——JavaScript。这篇文章就把这块补上让播放暂停、重听、停止这三个最核心的交互真正落地。如果你是直接跳到这一篇来看的也没关系文末会给出一个完整的可运行代码你只需要复制到一个HTML文件里双击打开就能玩。如果你是从头跟过来的那正好前面那些结构我们已经非常熟悉了现在只需要专注在JS怎么和HTML、CSS协作这件事上。这篇内容适合已经了解一点HTML标签和CSS选择器但JavaScript还处于“看得懂写不顺”阶段的朋友。我会把每个交互背后的逻辑拆开讲清楚而不是只丢给你一堆代码让你抄。因为播放器这种东西一旦你把交互模型想明白了后面加进度条、加音量控制、加播放列表都是顺水推舟的事。1. 先想清楚JS交互到底在管什么1.1 把“点按钮”翻译成代码逻辑在写任何代码之前我们先建立一个最基本的认知播放器所有交互本质上都是在做两件事——操作audio元素和更新页面UI状态。你可能觉得这句话是废话但真写起来会发现很多新手就是栽在这两者分不清上。比如点播放按钮有人会去改按钮的文字但忘了调play()有人调了play()但忘了改图标。所以我们要在脑子里先画一张动线图用户点击事件 → 事件处理函数 → 调用audio对象的方法 → audio对象触发对应事件 → UI代码监听这些事件去更新界面。这里我习惯用“遥控器、电视机、屏幕”做一个类比按钮是遥控器上的按键audio元素是电视机本身而界面上的播放暂停图标、当前时间、进度条都是电视机屏幕上显示的状态信息。你按遥控器电视执行操作屏幕显示结果。这三者之间是有明确分工的谁也别越权。另外为什么我们不自己解码MP3而是依赖浏览器内置的audio因为音频解码、设备输出、格式解析这些东西极其复杂浏览器底层早就帮你包好了。我们写audio标签本质上是告诉浏览器“这段音频交给你管”然后再通过它暴露出来的接口去控制播放行为。就像你不需要会修发动机也能开车你只需要会踩油门、打方向盘就行。对新手来说永远记住媒体控制这块能交给浏览器的就别自己造轮子。1.2 三个按钮的交互模型先说一下整体交互设计播放/暂停按钮这是最核心的toggle切换操作。同一个按钮当前如果是暂停状态点击后就应该播放如果是播放状态点击后就应该暂停。实现关键是状态判断——最简单的方法是直接读取audio.paused属性而不是自己额外维护一个布尔变量。逻辑流程是这样的用户点击按钮判断audio.paused是true还是false如果是true调用audio.play()然后把按钮图标从“播放”换成“暂停”如果是false调用audio.pause()把图标从“暂停”换回“播放”。重听重新播放按钮它的语义是“从头开始播放”。不管当前是播放中、暂停中还是已结束点击重听后都应该回到currentTime 0并且开始播放。有些人会把重听理解成“只回到开头但不播放”这也可以但从用户直觉来说“重听”两个字带着明显的“再听一遍”含义所以我倾向于让它直接播起来。停止按钮这是最容易搞混的一个。很多新手会下意识找一个stop()方法但原生audio根本不存在stop()它的“停止”需要两步组合先pause()暂停再currentTime 0回到开头。并且停止之后播放按钮图标也要复位成“播放”不然用户会看到明明已经停止了播放按钮却还显示着“暂停”图标非常别扭。我把这三个操作的逻辑对比放在一起你感受一下按钮核心操作关键细节播放/暂停play()或pause()状态判断决定走哪条分支重听currentTime 0, 再play()忽略当前状态强制从头播放停止pause()currentTime 0必须同步复位播放按钮图标这么一拆分你会发现每个按钮的代码量都不多难点在于理解它们之间的细微差别。2. 动手前必须搞懂的audio核心API2.1 高频使用的属性和方法在写具体代码前我把audio元素最常用的成员列一下后面我们全部会用到。方法方面play()开始播放返回一个Promise对象。这个Promise在播放成功时resolve失败时reject。注意play()不一定每次都能成功比如浏览器自动播放策略拦截、音频资源未加载完成都会导致失败。所以我们后续一定要给play()加上.catch()处理否则控制台会刷一条红字错误。pause()暂停播放永远成功没有返回值。属性方面paused只读布尔值true表示当前暂停。ended只读布尔值true表示播放已到底自然结束。currentTime可读可写单位是秒。读取能得到当前播放位置赋值就能跳转。副作用最小是我们实现重听和停止的关键。duration只读返回音频总时长单位也是秒。这个属性在音频元数据加载完成之前是NaN所以不能在页面打开后立刻读取要等loadedmetadata事件触发后再取。volume音量0到1之间默认1。muted是否静音布尔值。src音频地址也可以用source子标签指定。重点理解一下play()返回Promise这一点在比较老的浏览器里play()只是触发播放没有返回值。现在主流浏览器里如果播放失败这个Promise会reject如果你不接住这个错误浏览器就会在控制台报Uncaught (in promise) DOMException。很多新手遇到“明明写了play但是控制台报错”十有八九都是这里。所以我们后面统一写成audio.play().catch(function() { // 播放失败时的提示或兜底逻辑 });2.2 用事件驱动UI刷新有了操作audio对象的方法接下来要解决“界面怎么跟着状态变”。这里我用的是事件驱动而不是自己开个定时器轮询。什么意思audio对象在播放过程中会不断地往外抛事件我们只需要监听这些事件然后在事件回调里更新UI就行了。最常用的事件有这么几个play当播放开始时触发适合把按钮图标切换成“暂停”。pause当暂停时触发适合把按钮图标切换成“播放”。ended当播放自然结束时触发适合把按钮图标复位并把进度条归零。timeupdate播放位置更新时触发频率大概每秒4次适合刷新进度条和当前时间。loadedmetadata音频元数据加载完成后触发此时才能拿到duration。error资源加载失败时触发适合弹提示。以timeupdate为例我已经数不清有多少教程把播放进度刷新的代码写在setInterval里了。能用事件干的事为什么要用定时器事件是浏览器在正确的时机主动告诉你“状态变了”定时器则是你自己瞎猜什么时候该检查一次既不准又浪费性能。所以我的原则是凡是audio暴露了对应事件的就绝不用定时器去轮询。这里还要提一下timeupdate的触发频率大约每秒4次足够我们刷新进度条和时间显示了。如果你要做那种高精度的波形同步那另说普通播放器完全够用。2.3 浏览器自动播放策略曾经有朋友问我“为什么我页面一打开音乐没有自动播放”这其实是浏览器的自动播放策略在起作用。为了不打扰用户Chrome、Firefox这些主流浏览器都规定带声音的媒体不能在没有用户手势比如点击、键盘按键的情况下自动播放。所以把autoplay属性加在audio标签上在移动端和大部分桌面浏览器里是没用的。正确做法就是让用户先点一下播放按钮在click事件里调用play()这时候浏览器认定这是“用户主动行为”才会放行。这也解释了为什么我们教程从始至终都是“点击播放”而不是“加载即播放”。如果你想在页面加载后尝试自动播放也可以但必须这样写window.addEventListener(load, function() { audio.play().catch(function() { // 被拦截了提示用户点击播放按钮 }); });即使被拦截页面也不会报错只是音乐没响而已。这种兜底逻辑在实际项目里非常常见。3. 完整实操一个能直接跑的播放器3.1 完整代码示例下面我直接给一个完整的、可运行的HTML文件。我把样式压缩到最简重点展示JS交互逻辑你可以把它保存成player.html双击用浏览器打开替换成你自己的mp3文件路径就能用了。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title我的MP3播放器/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Segoe UI, PingFang SC, Microsoft YaHei, sans-serif; background: #282c34; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .player { width: 360px; background: #fff; border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.3); padding: 24px; text-align: center; } .music-icon { width: 120px; height: 120px; margin: 10px auto 20px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 48px; color: #fff; box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); } .song-title { font-size: 20px; font-weight: 700; color: #1c1e29; margin-bottom: 4px; } .song-artist { font-size: 14px; color: #888; margin-bottom: 20px; } .progress-wrap { background: #f0f0f0; border-radius: 10px; height: 8px; margin-bottom: 16px; overflow: hidden; } .progress-bar { width: 0%; height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 10px; transition: width 0.2s linear; } .time-row { display: flex; justify-content: space-between; font-size: 13px; color: #999; margin-bottom: 20px; } .controls { display: flex; justify-content: center; gap: 24px; } .control-btn { width: 56px; height: 56px; border: none; border-radius: 50%; background: #f0f0f0; font-size: 22px; cursor: pointer; color: #1c1e29; transition: background 0.2s, transform 0.2s; display: flex; align-items: center; justify-content: center; } .control-btn:hover { background: #e0e0e0; transform: scale(1.05); } .play-pause { width: 72px; height: 72px; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; font-size: 28px; box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); } .play-pause:hover { background: linear-gradient(135deg, #556cd6, #5f3fa8); } /style /head body div classplayer div classmusic-icon♪/div div classsong-title示例歌曲/div div classsong-artist佚名/div !-- 进度条和时间显示 -- div classprogress-wrap div classprogress-bar idprogressBar/div /div div classtime-row span idcurrentTime00:00/span span idtotalTime00:00/span /div !-- 控制按钮区域 -- div classcontrols button classcontrol-btn idrestartBtn title重听⟲/button button classcontrol-btn play-pause idplayToggleBtn title播放/暂停▶/button button classcontrol-btn idstopBtn title停止■/button /div /div !-- 这里填写你自己的mp3文件路径建议放在同一目录下 -- audio srcyour-song.mp3 idaudio/audio script // 1. 获取需要操作的元素 var audio document.getElementById(audio); var playToggleBtn document.getElementById(playToggleBtn); var restartBtn document.getElementById(restartBtn); var stopBtn document.getElementById(stopBtn); var progressBar document.getElementById(progressBar); var currentTimeLabel document.getElementById(currentTime); var totalTimeLabel document.getElementById(totalTime); // 2. 工具函数把秒数格式化为 mm:ss function formatTime(seconds) { if (isNaN(seconds)) return 00:00; var mins Math.floor(seconds / 60); var secs Math.floor(seconds % 60); return String(mins).padStart(2, 0) : String(secs).padStart(2, 0); } // 3. 播放/暂停切换 function togglePlay() { if (audio.paused) { audio.play().catch(function() { alert(音频播放失败请检查文件路径是否有效); }); } else { audio.pause(); } } // 4. 重听从开头重新播放 function restart() { audio.currentTime 0; audio.play().catch(function() { alert(音频播放失败请检查文件路径是否有效); }); } // 5. 停止暂停并回到开头 function stop() { audio.pause(); audio.currentTime 0; // 这里不用手动改按钮图标因为 pause 事件会帮我们复位 } // 6. 事件绑定 playToggleBtn.addEventListener(click, togglePlay); restartBtn.addEventListener(click, restart); stopBtn.addEventListener(click, stop); // 7. 监听audio事件同步UI // 播放开始时按钮图标换成暂停 audio.addEventListener(play, function() { playToggleBtn.textContent ❚❚; }); // 暂停时按钮图标换回播放 audio.addEventListener(pause, function() { playToggleBtn.textContent ▶; }); // 播放自然结束时把进度条清空时间归零按钮复位 audio.addEventListener(ended, function() { progressBar.style.width 0%; currentTimeLabel.textContent 00:00; // play事件还没触发但pause状态会由暂停事件处理 // 不过这里可以直接让按钮变回播放防止状态遗漏 playToggleBtn.textContent ▶; }); // 时间更新刷新进度条和时间 audio.addEventListener(timeupdate, function() { if (audio.duration) { var percent (audio.currentTime / audio.duration) * 100; progressBar.style.width percent %; currentTimeLabel.textContent formatTime(audio.currentTime); } }); // 元数据加载完成显示总时长 audio.addEventListener(loadedmetadata, function() { totalTimeLabel.textContent formatTime(audio.duration); }); /script /body /html这段代码可以直接跑。前提是你在同目录下放一个your-song.mp3文件或者把audio标签里的src改成线上mp3链接。3.2 核心JS逐段拆解先看元素的获取。var audio document.getElementById(audio); var playToggleBtn document.getElementById(playToggleBtn);我用的是getElementById因为这三个元素id唯一没必要用更重的查询方式。如果以后要做列表每个列表项都要绑定事件那才需要事件委托那是后话。然后是格式化的工具函数。function formatTime(seconds) { if (isNaN(seconds)) return 00:00; var mins Math.floor(seconds / 60); var secs Math.floor(seconds % 60); return String(mins).padStart(2, 0) : String(secs).padStart(2, 0); }这里用了padStart补零比以前手写(mins 10 ? 0 : ) mins干净得多。个别老浏览器可能不支持padStart但说实话都2024年了我默认按现代浏览器处理。接着是播放暂停切换。function togglePlay() { if (audio.paused) { audio.play().catch(function() { alert(音频播放失败请检查文件路径是否有效); }); } else { audio.pause(); } }这里判断audio.paused而不是自己维护isPlaying好处是不会有状态丢失的问题。你按停止也好播放自然结束也好audio对象自己永远是真实状态我们只负责读它。注意alert只是一个演示用的兜底实际项目里你可以换成Toast提示。重听按钮function restart() { audio.currentTime 0; audio.play().catch(function() { ... }); }把currentTime设为0再play()。这里有个细节如果音频已经播放到了中间直接把currentTime改成0然后调用play()浏览器会从新位置开始播放。但如果音频正在播放中又调用了一次play()不会引起“重新从头播放”的副作用它只会继续播放。所以我们不需要担心“播放中又点了重听”会导致什么怪异行为它最多就是跳到开头然后继续播这正是我们想要的。停止按钮function stop() { audio.pause(); audio.currentTime 0; }暂停归零。这里我特意注释了一句“不用手动改按钮图标因为pause事件会帮我们复位”。这就是事件驱动的爽点你只管触发行为UI自然被事件回调同步过去。事件监听部分重点看一下endedaudio.addEventListener(ended, function() { progressBar.style.width 0%; currentTimeLabel.textContent 00:00; playToggleBtn.textContent ▶; });为什么这里在暂停事件的逻辑之外还要手动把播放按钮恢复因为一首歌播放到结尾时会自动暂停浏览器确实会触发pause事件让按钮变成播放图标。但进度条不会自己归零时间也停在最后。所以ended里我手动把进度条清零顺便再设置一次按钮图标它是“兜底”确保状态完整。哪怕不设置按钮图标其实后面pause事件也会触发但手动写一次更保险防止某些边缘情况下事件顺序不稳定。3.3 音频文件的路径、格式与跨域代码能跑了但很多人会发现换成自己的歌就播不出来。这个时候大概率是路径问题。audio srcyour-song.mp3里的路径和你在a href里写的路径规则一样。最简单的是把mp3文件放在和HTML文件同一个目录下然后src直接写文件名。如果你把音乐放在子目录music/里那就写srcmusic/your-song.mp3。如果文件在上级目录就写../your-song.mp3。绝对路径file:///E:/music/song.mp3在本地双击也能用但不推荐因为一旦放到服务器上这种路径会直接失效。线上项目还有一个跨域问题。如果你的播放器页面在https://abc.com但音频src指向https://other.com/song.mp3这属于跨域请求。某些环境比如Canvas绘制音频频谱或者某些SDK会要求后端返回正确的Access-Control-Allow-Origin头。但对于普通播放暂停功能来说直接用跨域音频其实很多情况下也能响只是遇到防盗链的站点会返回403导致播放失败。所以做个人项目时我建议优先把音频文件放到和页面同源的地方最省心。格式方面浏览器对音频的支持并不统一。mp3AAC编码是普及度最高的主流浏览器全支持。但如果你手里是flac、ogg、wav这些格式不一定所有浏览器都认。这个话题我放在第五部分专门讲因为里面有大量刚做播放器的人一定会遇到的坑。4. 常见问题与排查实录4.1 问题速查表写播放器的过程中新手遇到的无非就这么几类问题我整理成一个速查表方便你对照排查。现象可能原因解决办法点击播放按钮没反应音频文件路径错误加载失败打开控制台看Network面板确认mp3请求状态是不是404点击播放按钮没反应音频格式浏览器不支持换成mp3或者用浏览器能播的格式点击播放按钮没反应play()只在用户手势内能触发确认代码是在click回调里调用的而不是页面加载自动调控制台报Uncaught (in promise)play()返回的Promise没有接住错误给play()加.catch()处理停止之后按钮图标没变回播放只更新了currentTime没调用pause()停止必须两步pause()currentTime 0点击重听没从头播放currentTime 0赋值失败确认音频已加载完至少loadedmetadata后再赋值进度条不动没有监听timeupdate事件在timeupdate回调里更新进度条宽度时间显示NaNduration还没加载出来就读取了在loadedmetadata里读总时长播放到结尾后再次播放没反应ended后状态未复位按钮判断错乱在ended里把按钮、进度条全复位这几条是很多人私信问过我的也是我五次三番在同事代码里看到的问题。特别是“停止后图标没变”和“时间显示NaN”这两条几乎每次帮人排查都能遇到。4.2 我踩过的几个坑第一个坑就是给audio元素写stop()方法。我自己早期做播放器时很自然地以为audio.stop()存在结果浏览器直接报错audio.stop is not a function。后来才意识到原生audio的设计就是只有pause停止是开发者自己组合出来的语义。这点在博客里不知道写了多少遍了但每过一阵还是会有人问。第二个坑是快速连续点击播放暂停时控制台刷红。原因是play()返回的Promise还没resolve你又调了一次pause()这时候Promise会被reject如果你没catch浏览器就报错。解决方式很简单所有play()后都跟一个.catch()。不要觉得这是小事线上用户手速快一点就来了非常影响体验。第三个坑是重听按钮在音频还没加载完时被点击。比如你刚打开页面audio还没有loadedmetadata这时候点重听currentTime 0虽然不报错但实际可能会被忽略。我的建议是重听按钮最好也做好loadedmetadata前的兜底——直接调用audio.load()并play()让浏览器重新加载并播放。不过这个操作会打断当前加载适合“死等加载完再播”的场景我一般只在极少数异常情况下用。第四坑关于图标状态我用的是字符图标“▶”和“❚❚”其实这有一个小问题——这两个字符在部分手机浏览器上可能显示不一致。如果你要发布一个比较正式的项目建议用两张背景图播放/暂停各一张或者用SVG。字符方案适合快速演示和教学做成产品时还是得换。5. 音频素材准备mp3为什么最省心5.1 浏览器能播哪些格式既然你做的叫“mp3免费音乐播放器”那绕不开音频格式这个问题。做过的朋友都知道浏览器能支持的音频格式和你手机上能直接播的完全是两回事。目前主流浏览器的音频格式支持大概是这样的音频格式ChromeFirefoxSafariEdgeMP3 (AAC)支持支持支持支持AAC (.m4a)支持支持支持支持OGG (Vorbis)支持支持部分支持支持WAV (PCM)支持支持支持支持FLAC支持支持支持支持新版看起来FLAC也支持但文件体积巨大一首歌动辄三五十兆在线播放对流量和加载速度都不友好。WAV更是无压缩一首歌几百兆都正常。真正兼顾兼容性、压缩比和普及度的目前还是MP3。这也解释了为什么你看到几乎所有网页播放器教程默认音频都是mp3。另外提一句有些搜到的素材是“三全音”这类系统提示音或者一些免费音效站点的mp3那些拿来做测试是可以的如果是你自己购买或下载的音乐文件请确保你有权把它们放到自己的播放器里使用。5.2 特殊格式文件怎么办经常有人问我“我手里是.ncm、.mgg、.kgg、.mflac这种文件怎么放到播放器里播”这些是某些音乐平台客户端下载后加密生成的私有格式浏览器直接播放会显示无效源。如果你的文件是合法获取且允许转换的一般有两种通用思路一是用带图形界面的格式转换工具选好输出格式为mp3点转换二是用命令行工具一条命令完成批量转换。具体工具名称我不在这里点名避免广告嫌疑搜索“ncm转mp3”或“mgg转mp3”就能找到一堆挑选时注意看是否开源、是否限速、是否干净。转换完之后记得检查一下输出的mp3文件能否正常打开有些转换工具会把采样率或者声道弄得很奇怪导致播放器能加载但没声音。如果遇到这种情况可以用系统自带的音频播放器试播一把确认文件本身是好的。这一步做完再把它塞进我们的播放器里几乎不会出幺蛾子。如果你不想转格式也可以让播放器更“宽容”一些用source标签同时提供多种格式audio idaudio source srcsong.mp3 typeaudio/mpeg source srcsong.ogg typeaudio/ogg source srcsong.wav typeaudio/wav 你的浏览器不支持audio标签 /audio浏览器会自己选择第一个能播的源。但现实是普通人根本不会准备那么多格式所以我在正式演示代码里还是用的单个src简单直接。还有一个进阶小技巧在JS里可以通过audio.canPlayType(type)提前判断浏览器支持哪种格式然后动态设置音频源。比如var audio document.getElementById(audio); if (audio.canPlayType(audio/mpeg)) { audio.src song.mp3; } else if (audio.canPlayType(audio/ogg)) { audio.src song.ogg; } else { console.log(当前浏览器不支持常见音频格式); }这个方法适合做播放列表时用先写好若干候选文件运行时按能力选择能省掉很多兼容性判断的代码。讲到这里播放器“播放暂停、重听、停止”这三个功能已经全部落地了。我自己最初做版本的时候其实也没有把JS想得多复杂就是把每个按钮的动线梳理清楚然后顺着audio的生命周期去监听对应事件代码量很小但效果意外地完整。等你把这个例子跑起来再想加进度条拖拽、音量控制、播放列表这些功能其实都不难因为你要扩展的那些点背后都已经有对应的事件和属性等着你用了。拿这个demo当底座后面就是往上面叠积木的事。
返回列表