必须在 loadedmetadata 事件后监听 ended,否则 Safari 和部分 Android WebView 不触发;常见失效原因包括监听过早、pause/跳转中断、loop 开启、MP3 帧损坏、静音策略拒绝 play();正确流程:preload 设置、loadedmetadata 后绑定 ended、动态 src 需手动 load();play() 必须用户手势触发并处理 Promise 拒绝;SPA 跳转前需 pause() + currentTime = 0 清理。

必须监听 ended 事件,且监听动作必须在音频元数据加载完成(loadedmetadata)之后执行,否则 Safari 和部分 Android WebView 中大概率不触发。
为什么 ended 事件经常不触发
不是代码写错了,而是时机不对。常见失效原因包括:
-
ended监听绑得太早——比如在DOMContentLoaded时就加,此时audio.src可能还没解析,duration是NaN,浏览器直接跳过事件注册 - 音频被
pause()或currentTime跳转中断过,ended只在自然播到末尾时才触发 - 用了
loop="true",循环模式下永远不会走到“结束” - MP3 文件末尾帧损坏或 ID3 不规范,iOS Safari 会提前终止播放,不发
ended -
play()被静音策略拒绝(控制台报NotAllowedError),整个播放流程没启动,后续事件全失效
正确监听 ended 的三步顺序
必须按这个链条走,缺一不可:
- 确保
标签有preload="auto"或至少preload="metadata",加快元数据加载 - 监听
loadedmetadata事件,在回调里再绑定ended—— 这是 Safari 兼容性的硬性要求 - 如果音频是动态设置
src(比如audio.src = 'sound.mp3'),赋值后必须手动调用audio.load(),否则loadedmetadata不会触发
示例:
把任意文章或文档内容转换成精美的全屏翻页 HTML 演示文件(幻灯片风格)。当用户发来一篇文章、文档链接、Markdown 内容、或粘贴的文本,并且希望生成演示用的 HTML 时触发。支持任意内容来源:iWiki 链接、KM 链接、本地文件路径、直接粘贴的文本。生成的 HTML 文件保存到本地工作区,并通过企微直接发送给用户。触发词示例:帮我把这篇文章做成PPT、转成演示文档、生成HTML幻灯片、做个演示、转成可以演示的格式、article to deck、make a slide。
const audio = document.querySelector('audio');
audio.preload = 'auto';
audio.addEventListener('loadedmetadata', () => {
audio.addEventListener('ended', () => {
console.log('音效播完了');
window.location.href = '/success';
});
});
// 若 src 是后来设的:
// audio.src = 'click.mp3';
// audio.load(); // 必须加这一行
跳转前要检查 play() 是否真成功了
ended 不触发,很多时候是因为 play() 根本没跑起来。现代浏览器(Chrome、Safari、Edge)强制要求首次播放必须由用户手势同步触发,不能延迟、不能包装在 Promise.then 里。所以:
- 把
audio.play()放在按钮点击事件最开头,别加setTimeout或await delay() - 一定要处理
play()返回的 Promise:audio.play().catch(e => console.warn('播放被拒:', e)) - 如果
catch到了错误,说明用户还没和页面交互过,此时即使绑了ended也没意义——因为根本没播 - 不要复用同一个
Audio实例跨多个按钮做不同音效;每个音效单独预建实例,避免 Safari 因资源未缓存而静音拦截
SPA 场景下跳转不卸载页面,音频不会自动停
用 history.pushState、Vue Router 或 React Router 时,页面没刷新,audio 实例还活着,ended 即使触发了,跳转逻辑也可能被路由守卫拦截或执行错上下文。这时候得主动清理:
- 在路由切换前(如 Vue 的
beforeEach、React 的useEffect清理函数)调用audio.pause()和audio.currentTime = 0 - 不要用
audio.remove()或delete,没效果;pause()+currentTime = 0就够了 - 如果音效是瞬时反馈型(比如按钮音),播完就不管;如果是背景循环音,跳转前必须
pause(),否则会跨页继续响
真正难的不是写 ended 回调,而是得同时兜住 error、abort、stalled 这些可能中途掐断播放的事件——ended 只代表“顺利走到了最后一帧”,不代表你预期的业务流程一定完成了。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










