这是系列教程的第三篇。前两篇我们分别用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核心API
2.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 lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.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 class="player"> <div class="music-icon">♪</div> <div class="song-title">示例歌曲</div> <div class="song-artist">佚名</div> <!-- 进度条和时间显示 --> <div class="progress-wrap"> <div class="progress-bar" id="progressBar"></div> </div> <div class="time-row"> <span id="currentTime">00:00</span> <span id="totalTime">00:00</span> </div> <!-- 控制按钮区域 --> <div class="controls"> <button class="control-btn" id="restartBtn" title="重听">⟲</button> <button class="control-btn play-pause" id="playToggleBtn" title="播放/暂停">▶</button> <button class="control-btn" id="stopBtn" title="停止">■</button> </div> </div> <!-- 这里填写你自己的mp3文件路径,建议放在同一目录下 --> <audio src="your-song.mp3" id="audio"></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自然被事件回调同步过去。
事件监听部分,重点看一下ended:
audio.addEventListener('ended', function() { progressBar.style.width = '0%'; currentTimeLabel.textContent = '00:00'; playToggleBtn.textContent = '▶'; });为什么这里在暂停事件的逻辑之外,还要手动把播放按钮恢复?因为一首歌播放到结尾时,会自动暂停,浏览器确实会触发pause事件,让按钮变成播放图标。但进度条不会自己归零,时间也停在最后。所以ended里我手动把进度条清零,顺便再设置一次按钮图标,它是“兜底”,确保状态完整。哪怕不设置按钮图标,其实后面pause事件也会触发,但手动写一次更保险,防止某些边缘情况下事件顺序不稳定。
3.3 音频文件的路径、格式与跨域
代码能跑了,但很多人会发现,换成自己的歌就播不出来。这个时候大概率是路径问题。
<audio src="your-song.mp3">里的路径,和你在<a href>里写的路径规则一样。最简单的是把mp3文件放在和HTML文件同一个目录下,然后src直接写文件名。如果你把音乐放在子目录music/里,那就写src="music/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,导致播放失败。所以做个人项目时,我建议优先把音频文件放到和页面同源的地方,最省心。
格式方面,浏览器对音频的支持并不统一。mp3(AAC编码)是普及度最高的,主流浏览器全支持。但如果你手里是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回调里更新进度条宽度 |
时间显示NaN | duration还没加载出来就读取了 | 在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免费音乐播放器”,那绕不开音频格式这个问题。做过的朋友都知道,浏览器能支持的音频格式,和你手机上能直接播的,完全是两回事。
目前主流浏览器的音频格式支持大概是这样的:
| 音频格式 | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| MP3 (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 id="audio"> <source src="song.mp3" type="audio/mpeg"> <source src="song.ogg" type="audio/ogg"> <source src="song.wav" type="audio/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当底座,后面就是往上面叠积木的事。