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 中,useAudio与useVideo共享同一套底层实现——它们都来自工厂函数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的核心工作方式:
- 调用
useAudio({ src, autoPlay }),传入一个普通的 props 对象; - 从返回值中解构出
audio(要渲染进组件树的<audio>元素)、state(实时状态,可直接 JSON 序列化展示)、controls(一组控制函数)以及ref(底层 DOM 元素引用); - 把
{audio}插入到渲染树中——这是状态能同步、控制能生效的前提; - 通过
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) |
time | number | 当前播放位置(秒) |
duration | number | 音频总时长(秒) |
paused | boolean | 是否处于暂停状态 |
muted | boolean | 是否静音 |
volume | number | 音量,范围 0~1 |
playing | boolean | 是否正在播放中 |
其中playing需要特别注意:文档明确指出,playing表示音频正在播放且未受网络影响——如果音频开始缓冲(buffer)数据,playing会变为false。也就是说playing与paused并不等价: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>元素接受的所有属性(src、autoPlay、loop、crossOrigin、preload等)。文档说明 "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 的字段 |
|---|---|---|
onPlay | onPlay | paused: false |
onPlaying | onPlaying | playing: true |
onWaiting | onWaiting | playing: false(开始缓冲等待) |
onPause | onPause | paused: true, playing: false |
onVolumeChange | onVolumeChange | muted、volume(读自真实元素) |
onDurationChange | onDurationChange | duration、buffered |
onTimeUpdate | onTimeUpdate | time(读自el.currentTime) |
onProgress | onProgress | buffered(读自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):
- 把真实元素的
volume、muted、paused初始值回填进 state(保证与浏览器实际状态一致); - 若设置了
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时有几点建议:
- 务必渲染返回的
audio元素,并让它保持在组件树中(可用 CSS 隐藏,但不要卸载),否则状态不会更新、控制方法全部失效; - 把
state视为"事件驱动的最新快照":time只在timeupdate事件触发时更新,做进度条时无需自行轮询;若需要更细粒度的时间刷新,可结合requestAnimationFrame类 Hook 平滑插值; playing与paused分开判断:展示"加载中/缓冲中"状态时应读取playing,而不要用paused反推;- 访问
ref.current前判空,并优先使用controls方法而不是直接操作 DOM,以享受钳制、锁等内置保护; - 控制音量后 state 已同步回写,UI 上直接展示
state.volume即可,无需额外维护本地状态。
九、与useVideo的关系
useAudio与useVideo是同一套工厂的两个实例(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),仅供参考