news 2026/9/18 23:39:12

react-use 之 useAudio:创建 `<audio>` 元素、追踪播放状态并暴露播放控制器的 React Hook 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react-use 之 useAudio:创建 `<audio>` 元素、追踪播放状态并暴露播放控制器的 React Hook 实战指南

react-use 之 useAudio:创建<audio>元素、追踪播放状态并暴露播放控制器的 React Hook 实战指南

【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use

导读

useAudio是 react-use 库中用于封装 HTML5 音频播放能力的核心 Hook:它会替你创建一个<audio>元素,实时追踪音频的播放状态(时长、音量、缓冲、播放/暂停等),并暴露一组简洁的播放控制方法(play、pause、mute、seek 等)。阅读完本篇,你将掌握useAudio的两种调用形式、四元组返回值[audio, state, controls, ref]的完整语义、基于 HTMLMediaElement 事件驱动的状态同步原理,以及它在 react-use 仓库中的源码级实现细节,从而能够直接用它搭建自定义音频播放器界面。


一、useAudio是什么

useAudio是一个专门为<audio>元素设计的 React Hook,其职责一句话概括为:创建<audio>元素、追踪其状态并暴露播放控制能力(原文:Creates<audio>element, tracks its state and exposes playback controls,见 docs/useAudio.md)。

它隶属于 react-use 的 UI 类 Hook。在 react-use 中,useAudiouseVideo共享同一套底层实现——它们都来自工厂函数createHTMLMediaHook(见 src/useAudio.ts 与 src/useVideo.ts):

// src/useAudio.ts import createHTMLMediaHook from './factory/createHTMLMediaHook'; const useAudio = createHTMLMediaHook<HTMLAudioElement>('audio'); export default useAudio;

也就是说,useAudio就是调用createHTMLMediaHook<HTMLAudioElement>('audio')得到的专用 Hook,其全部能力均来自 src/factory/createHTMLMediaHook.ts 这个通用媒体 Hook 工厂。库的入口 src/index.ts 中将其以命名导出的形式暴露给使用者(export { default as useAudio } from './useAudio')。

二、安装与导入

useAudio是 react-use 的一部分,直接安装 react-use 即可使用:

npm install react-use # 或 yarn add react-use

导入方式:

import { useAudio } from 'react-use';

仓库的 Storybook 演示(stories/useAudio.story.tsx)中也使用了与文档完全一致的导入与用法,可以作为可运行示例参考。

三、基础用法:文档示例

官方文档给出了一段完整的用法示例,我们原样继承并逐行拆解:

import { useAudio } from 'react-use'; const Demo = () => { const [audio, state, controls, ref] = useAudio({ src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3', autoPlay: true, }); return ( <div> {audio} <pre>{JSON.stringify(state, null, 2)}</pre> <button onClick={controls.pause}>Pause</button> <button onClick={controls.play}>Play</button> <br/> <button onClick={controls.mute}>Mute</button> <button onClick={controls.unmute}>Un-mute</button> <br/> <button onClick={() => controls.volume(.1)}>Volume: 10%</button> <button onClick={() => controls.volume(.5)}>Volume: 50%</button> <button onClick={() => controls.volume(1)}>Volume: 100%</button> <br/> <button onClick={() => controls.seek(state.time - 5)}>-5 sec</button> <button onClick={() => controls.seek(state.time + 5)}>+5 sec</button> </div> ); };

这段代码演示了useAudio的核心工作方式:

  1. 调用useAudio({ src, autoPlay }),传入一个普通的 props 对象;
  2. 从返回值中解构出audio(要渲染进组件树的<audio>元素)、state(实时状态,可直接 JSON 序列化展示)、controls(一组控制函数)以及ref(底层 DOM 元素引用);
  3. {audio}插入到渲染树中——这是状态能同步、控制能生效的前提;
  4. 通过controls.*驱动播放行为:暂停/播放、静音/取消静音、设置音量(10% / 50% / 100%)、基于state.time前后快进/快退 5 秒。

四、API 参考:返回值四元组详解

useAudio支持两种等价调用形式(见文档 Reference 部分):

// 形式一:传入 props 对象,Hook 内部为你创建 <audio> 元素 const [audio, state, controls, ref] = useAudio(props); // 形式二:传入一个现成的 <audio> React 元素(可携带任意子节点与属性) const [audio, state, controls] = useAudio(<audio {...props}/>);

从源码(src/factory/createHTMLMediaHook.ts)可以看到,工厂内部通过React.isValidElement(elOrProps)判断入参是 React 元素还是 props 对象:

  • 若传入的是合法 React 元素,则直接使用该元素,并取出其props
  • 若传入的是普通对象,则将其视为 props,在内部通过React.createElement(tag, {...})创建<audio>元素。

两种方式殊途同归,最终返回相同的四元组。下面逐一展开这四个返回值的语义。

4.1audio—— 必须渲染进组件树的<audio>元素

audio是一个 React 元素,你必须把它插入到渲染树中的某个位置,否则 Hook 拿不到真实的 DOM 元素,状态与控制的底层操作将无法工作。文档中的示例为:

<div>{audio}</div>

源码层面,工厂会把这个元素以controls: false强制渲染(同时合并用户传入的 props 与内部的事件代理),也就是说浏览器自带的原生控制条会被关闭——这正是为了让开发者用controls方法自定义播放器 UI。

4.2state—— 实时音频状态

state追踪音频的当前状态,其形状如下(文档原文示例):

{ "buffered": [ { "start": 0, "end": 425.952625 } ], "time": 5.244996, "duration": 425.952625, "paused": false, "muted": false, "volume": 1, "playing": true }

各字段含义如下:

字段类型含义
buffered{start, end}[]已缓冲的时间区间数组,由TimeRanges解析而来(见 src/misc/parseTimeRanges.ts)
timenumber当前播放位置(秒)
durationnumber音频总时长(秒)
pausedboolean是否处于暂停状态
mutedboolean是否静音
volumenumber音量,范围 0~1
playingboolean是否正在播放中

其中playing需要特别注意:文档明确指出,playing表示音频正在播放且未受网络影响——如果音频开始缓冲(buffer)数据,playing会变为false。也就是说playingpaused并不等价:paused描述"用户侧是否暂停",而playing描述"此刻是否真正出声"(可能因缓冲等待而短暂中断)。

在源码中,state 的初始值定义于 src/factory/createHTMLMediaHook.ts,并通过useSetState管理:

const [state, setState] = useSetState<HTMLMediaState>({ buffered: [], time: 0, duration: 0, paused: true, muted: false, volume: 1, playing: false, });

4.3controls—— 播放控制方法集合

controls是一组播放控制方法,其 TypeScript 接口如下(文档原文):

interface AudioControls { play: () => Promise<void> | void; pause: () => void; mute: () => void; unmute: () => void; volume: (volume: number) => void; seek: (time: number) => void; }

各方法语义:

  • play():开始播放,返回Promise<void> | void
  • pause():暂停播放;
  • mute():静音;
  • unmute():取消静音;
  • volume(volume: number):设置音量(0~1);
  • seek(time: number):跳转到指定时间(秒)。

4.4ref—— 底层 DOM 元素引用

ref是对 HTML<audio>元素的 React 引用,通过ref.current访问真实 DOM 元素。文档特别提醒:ref.current可能为null——例如元素尚未挂载或你忘记渲染audio时,因此访问前应做好空值判断。在测试(tests/useAudio.test.ts)中正是通过手动给ref.current赋一个document.createElement('audio')来验证mute/unmute/volume控制方法的正确性。

4.5props—— 透传所有<audio>支持的属性

最后一个入参props<audio>元素接受的所有属性(srcautoPlayloopcrossOriginpreload等)。文档说明 "all props that<audio>accepts",在源码类型定义中体现为 src/factory/createHTMLMediaHook.ts 的HTMLMediaProps

export interface HTMLMediaProps extends React.AudioHTMLAttributes<any>, React.VideoHTMLAttributes<any> { src: string; }

其中src为必填项。注意:虽然autoPlay被透传给了元素,但真正触发自动播放的逻辑在 Hook 内部(见下文第六节),因此仅靠autoPlay属性本身在部分浏览器(如启用了自动播放策略的 Chrome)中未必生效,这是实现层面需要理解的一个细节。

五、状态如何被追踪:事件驱动的同步机制

useAudio的"实时状态"并不是轮询得来的,而是监听HTMLMediaElement的原生事件驱动的。工厂在创建元素时,会以"用户事件 + 内部代理"的方式挂载 8 个事件处理器(src/factory/createHTMLMediaHook.ts),wrapEvent保证用户自己传入的同类事件回调也能先于(或后于)内部逻辑执行,互不干扰:

事件内部处理器同步到 state 的字段
onPlayonPlaypaused: false
onPlayingonPlayingplaying: true
onWaitingonWaitingplaying: false(开始缓冲等待)
onPauseonPausepaused: true, playing: false
onVolumeChangeonVolumeChangemutedvolume(读自真实元素)
onDurationChangeonDurationChangedurationbuffered
onTimeUpdateonTimeUpdatetime(读自el.currentTime
onProgressonProgressbuffered(读自el.buffered

其中buffered字段并非直接透传浏览器的TimeRanges对象,而是通过 src/misc/parseTimeRanges.ts 将其解析为易用的{start, end}[]数组:

export default function parseTimeRanges(ranges) { const result: { start: number; end: number }[] = []; for (let i = 0; i < ranges.length; i++) { result.push({ start: ranges.start(i), end: ranges.end(i) }); } return result; }

这解释了文档示例中buffered呈现为[{ "start": 0, "end": 425.952625 }]的原因:一个已缓冲区间,起点 0 秒、终点与时长一致,说明整个音频已缓冲完成。

六、控制方法背后的实现细节

controls各方法并非简单调用原生 API,源码在 src/factory/createHTMLMediaHook.ts 中做了若干健壮性处理,值得深入了解:

6.1play()的 Promise 锁机制

部分浏览器(如 Chrome)的HTMLMediaElement.play()会返回 Promise,若在 Promise 尚未 resolve 时再次调用play()pause(),可能抛出异常(源码注释引用了 Chromium issue #593273)。为此,工厂维护了一个lockPlay布尔锁:

let lockPlay: boolean = false; play: () => { const el = ref.current; if (!el) return undefined; if (!lockPlay) { const promise = el.play(); const isPromise = typeof promise === 'object'; if (isPromise) { lockPlay = true; const resetLock = () => { lockPlay = false; }; promise.then(resetLock, resetLock); } return promise; } return undefined; }

即:play()返回 Promise 时上锁,待 Promise 无论成功还是失败都解锁;锁未释放期间,后续play()/pause()调用会被安全地忽略,避免竞态异常。

6.2seek()的时间钳制

seek(time)会把目标时间钳制在[0, duration]区间内,防止越界:

time = Math.min(state.duration, Math.max(0, time)); el.currentTime = time;

这保证了controls.seek(state.time + 5)在接近结尾时不会产生非法跳转。

6.3volume()的音量钳制与状态回写

volume(volume)同样将入参钳制在[0, 1],在写入el.volume后还会主动setState({ volume }),让 state 立即反映新的音量值:

volume = Math.min(1, Math.max(0, volume)); el.volume = volume; setState({ volume });

6.4mute()/unmute()

两者直接读写el.muted属性,最终通过onVolumeChange事件把muted状态同步回 state。

七、autoPlay的挂载期处理

在元素挂载后的useEffect中(依赖props.src),工厂会做两件事(src/factory/createHTMLMediaHook.ts):

  1. 把真实元素的volumemutedpaused初始值回填进 state(保证与浏览器实际状态一致);
  2. 若设置了props.autoPlay且元素当前处于暂停状态,则调用controls.play()触发自动播放。
useEffect(() => { const el = ref.current!; if (!el) { /* 开发环境下打印错误提示 */ return; } setState({ volume: el.volume, muted: el.muted, paused: el.paused }); if (props.autoPlay && el.paused) { controls.play(); } }, [props.src]);

注意此处的错误提示:在非生产环境(NODE_ENV !== 'production')下,如果挂载时ref.current为空(即没有渲染返回的audio元素),控制台会打印一条明确的错误信息:"useAudio() ref to<audio>element is empty at mount. It seem you have not rendered the audio element..."。这条信息对排查"为什么 Hook 不工作"非常有用——忘渲染{audio}是最常见的误用方式

该行为在测试 tests/useAudio.test.ts 中得到了验证:当renderHook只调用 Hook 而不渲染返回的audio元素时,console.error恰好被调用一次。

八、工程实践建议

基于上述原理,在实际项目中使用useAudio时有几点建议:

  1. 务必渲染返回的audio元素,并让它保持在组件树中(可用 CSS 隐藏,但不要卸载),否则状态不会更新、控制方法全部失效;
  2. state视为"事件驱动的最新快照"time只在timeupdate事件触发时更新,做进度条时无需自行轮询;若需要更细粒度的时间刷新,可结合requestAnimationFrame类 Hook 平滑插值;
  3. playingpaused分开判断:展示"加载中/缓冲中"状态时应读取playing,而不要用paused反推;
  4. 访问ref.current前判空,并优先使用controls方法而不是直接操作 DOM,以享受钳制、锁等内置保护;
  5. 控制音量后 state 已同步回写,UI 上直接展示state.volume即可,无需额外维护本地状态。

九、与useVideo的关系

useAudiouseVideo是同一套工厂的两个实例(src/useVideo.ts 中createHTMLMediaHook<HTMLVideoElement>('video')),因此本文所有的 state 字段、controls 接口、事件机制对useVideo同样适用(useVideo文档见 docs/useVideo.md)。如果你后续要处理视频,可以无缝迁移同样的心智模型。

十、小结

useAudio把 HTML5 音频的"元素创建—状态追踪—播放控制"三件事封装为一个 Hook,返回值四元组[audio, state, controls, ref]分工明确、开箱即用。通过阅读 src/factory/createHTMLMediaHook.ts 的源码可以发现,其"实时状态"完全依赖HTMLMediaElement原生事件驱动,控制方法则内建了 Promise 锁、数值钳制等健壮性保护;配套测试 tests/useAudio.test.ts 与 Storybook 演示 stories/useAudio.story.tsx 可直接运行验证。理解这些底层机制后,你就能放心地基于useAudio构建自定义音频播放器、播客界面或任何需要精确控制音频播放的 React 应用。

【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 23:38:48

从火山方舟切到 TaoToken Key,豆包 2.1 Pro 会丢多模态吗

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 23:37:19

oh-my-hermes:统一管理Hermes配置,助力RN性能优化

1. 为什么我会写一个 oh-my-hermes&#xff1a;被 Hermes 配置折腾出来的工具1.1 Hermes 普及之后&#xff0c;痛点反而更多了做过 React Native 性能优化的同学应该都有同感&#xff1a;从 RN 0.70 开始 Hermes 成为默认 JavaScript 引擎之后&#xff0c;大多数团队的"优…

作者头像 李华
网站建设 2026/9/18 23:37:06

OpenClaw 跑 CSDN 发布任务,Key 走 TaoToken 行不行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 23:33:18

Hyperresearch claims命令完全指南:跨来源提取与查询结构化声明

Hyperresearch claims命令完全指南&#xff1a;跨来源提取与查询结构化声明 【免费下载链接】hyperresearch Agent-driven research knowledge base. Agents collect, search, and synthesize web research into a persistent, searchable wiki. 项目地址: https://gitcode.c…

作者头像 李华
网站建设 2026/9/18 23:31:38

STM32+MPU6050摔倒检测报警系统:从传感器到云端全解析

简介&#xff1a;一份基于STM32设计的老人防摔倒报警设备完整方案PDF&#xff0c;面向嵌入式开发者、物联网学习者及电子设计竞赛参赛者&#xff0c;解决老年人摔倒实时检测与远程报警的工程实现问题。文档以实际项目为主线&#xff0c;涵盖需求分析、模块选型、电路连接与运行…

作者头像 李华