useSound
一个用于音效的 React Hook
网页需要更多(恰到好处的)声音!
- 👂 让你的网站能够通过2种人类感官而不是1种来进行交流
- 🔥 声明式 Hooks API
- ⚡️ 在你的打包文件中只有<1kb大小(gzip压缩后)! ~10kb异步加载。
- ✨ 使用 Typescript 构建
- 🗣 使用强大、经过实战检验的音频工具库: Howler.js
这个库只适用于 React DOM,但 @remigallego 为 React Native 创建了一个替代方案!查看 react-native-use-sound。
项目状态
这个项目处于"半维护"状态 😅
我目前没有精力去研究边缘情况的问题或帮助排查问题,但我计划随着 React 的主要版本更新来保持项目的更新,并修复那些既严重又常见的问题。
如果你有功能想法,或遇到奇怪的问题,我强烈建议你 fork 这个项目并使其成为你自己的!这可能看起来有点吓人,但源代码并不像其他许多 NPM 包那样复杂;我将所有困难的音频工作都交给了 Howler。如果你已经使用 React 一段时间并且熟悉 hooks,你应该会对这个包的代码感到很熟悉。
安装
可以使用 yarn 添加包:
yarn add use-sound
或者使用 NPM:
npm install use-sound
UMD 构建可在 unpkg 上获得。
如果你的项目使用 TypeScript,你还应该将 @types/howler
包作为开发依赖项安装。
演示
教程 包含了许多演示,以及寻找和准备音效的说明。这是一个很好的起点。
你还可以 查看 storybook,其中包含了许多快速示例。
示例
点击时播放声音
import useSound from 'use-sound';
import boopSfx from '../../sounds/boop.mp3';
const BoopButton = () => {
const [play] = useSound(boopSfx);
return <button onClick={play}>Boop!</button>;
};
悬停时播放
这个演示只在鼠标悬停在元素上时播放声音。当鼠标离开元素时,声音会暂停:
注意:许多浏览器会禁用声音,直到用户在页面上点击某个地方。如果你在这个例子中听不到任何声音,试着在页面上随意点击一下,然后再试一次。
import useSound from 'use-sound';
import fanfareSfx from '../../sounds/fanfare.mp3';
const FanfareButton = () => {
const [play, { stop }] = useSound(fanfareSfx);
return (
<button onMouseEnter={() => play()} onMouseLeave={() => stop()}>
<span role="img" aria-label="trumpet">
🎺
</span>
</button>
);
};
每次点击时提高音调
使用 playbackRate
选项,你可以改变样本的速度/音调。这个例子播放一个声音,并且每次都让它快 10%:
import useSound from 'use-sound';
import glugSfx from '../../sounds/glug.mp3';
export const RisingPitch = () => {
const [playbackRate, setPlaybackRate] = React.useState(0.75);
const [play] = useSound(glugSfx, {
playbackRate,
// `interrupt` 确保如果声音在结束前再次开始,
// 它将被截断。否则,声音可能会重叠。
interrupt: true,
});
const handleClick = () => {
setPlaybackRate(playbackRate + 0.1);
play();
};
return (
<Button onClick={handleClick}>
<span role="img" aria-label="Person with lines near mouth">
🗣
</span>
</Button>
);
};
使用注意事项
导入/引用音频文件
useSound
需要一个音频文件的路径,但在 React 应用中如何提供这个路径并不是很明显。
使用 create-react-app
,你可以"导入"一个 MP3 文件。它会解析为一个动态生成的路径:
import someAudioFile from '../sounds/sound.mp3';
console.log(someAudioFile); // "/build/sounds/sound-abc123.mp3"
如果你尝试在其他 React 构建系统中使用这个技巧,比如 Next.js,你可能会得到这样的错误:
你可能需要一个适当的加载器来处理这种文件类型,目前没有配置加载器来处理这个文件。
问题在于 Webpack(在底层用于生成 JS 包的打包工具)不知道如何处理 MP3 文件。
如果你可以访问 Webpack 配置,你可以更新它以使用 file-loader,这将创建一个动态的、可公开访问的文件路径。
另外,大多数工具都会给你一个"public"(create-react-app, Next.js)或"static"(Gatsby)文件夹。你可以将音频文件放在那里,然后使用字符串路径。
你将与 use-sound
一起使用的声音文件遵循与其他静态资源(如图像或字体)相同的规则。按照你所选择的元框架的指南:
⚠️ 异步声音路径? ⚠️ 如果你的音频文件的 URL 是异步加载的,你可能会遇到一些问题。这个包可能不适合这种用例。
加载后立即无声音
为了用户着想,浏览器不允许网站在用户与之交互(例如通过点击)之前产生声音。在用户点击、触摸或触发某些操作之前,不会产生声音。
useSound
利用了这一点:因为我们知道在加载后不会立即需要声音,我们可以延迟加载第三方依赖。
useSound
会向你的包中添加约 1kb gzip 大小的代码,并在加载后异步获取一个额外的包,大小约为 9kb gzip。
如果用户恰好在这个依赖被加载和获取之前点击了会产生声音的东西,它将是一个空操作(一切仍然会正常工作,但不会播放音效)。根据我的经验,这种情况非常罕见。
响应式配置
考虑以下代码片段:
const [playbackRate, setPlaybackRate] = React.useState(0.75);
const [play] = useSound('/path/to/sound', { playbackRate });
playbackRate
不仅仅是音效的初始值。如果 playbackRate
改变,声音将立即以新的速率开始播放。这对传递给 useSound
hook 的所有选项都是如此。
API 文档
useSound
hook 接受两个参数:
- 要加载的声音的 URL
- 一个配置对象 (
HookOptions
)
它返回一个包含两个值的数组:
- 一个你可以调用来触发声音的函数
- 一个包含额外数据和控制的对象 (
ExposedData
)
当调用函数来播放声音时,你可以传递一组选项 (PlayOptions
)。
让我们依次了解每一个。
HookOptions
调用 useSound
时,你可以传递各种选项:
名称 | 值 |
---|---|
volume | number |
playbackRate | number |
interrupt | boolean |
soundEnabled | boolean |
sprite | SpriteMap |
[delegated] | — |
volume
是一个从0
到1
的数字,其中1
是全音量,0
是完全静音。playbackRate
是一个从0.5
到4
的数字。它可以用来减慢或加快样本。像唱片机一样,速度的变化也会影响音调。interrupt
指定如果在声音结束前再次调用play
函数,声音是否应该能够"重叠"。soundEnabled
允许你传递一个值(通常来自 context 或 redux 或其他地方)来静音所有声音。注意,这可以在PlayOptions
中被覆盖,见下文。sprite
允许你为多个音效使用单个useSound
hook。参见下面的"Sprites"。
[delegated]
指的是你在 HookOptions
中传递的任何额外参数都将被转发到 Howl
构造函数。有关更多信息,请参见下面的"Escape hatches"。
play
函数
当调用 hook 时,你会在元组的第一项中得到一个 play 函数:
const [play] = useSound('/meow.mp3');
// ^ 我们正在讨论的内容
当你想触发声音时,你可以不带任何参数调用这个函数。你也可以用一个 PlayOptions
对象来调用它:
名称 | 值 |
---|---|
id | string |
forceSoundEnabled | boolean |
playbackRate | number |
id
用于精灵标识。详见下面的"精灵"部分。forceSoundEnabled
允许你覆盖传递给HookOptions
的soundEnabled
布尔值。通常你不会想这样做。我发现唯一的例外是:在"静音"按钮上触发声音。playbackRate
是另一种设置新播放速率的方法,与HookOptions
中的相同。一般来说,你应该优先通过HookOptions
来设置,这只是一个后备选项。
ExposedData
该钩子产生一个包含两个选项的元组,即播放函数和一个 ExposedData
对象:
const [play, exposedData] = useSound('/meow.mp3');
// ^ 我们正在讨论的内容
名称 | 值 |
---|---|
stop | 函数 ((id?: string) => void) |
pause | 函数 ((id?: string) => void) |
duration | 数字 (或 null) |
sound | Howl (或 null) |
stop
是一个可以用来提前停止声音的函数。pause
类似于stop
,但可以从同一点恢复。除非你知道要恢复,否则应该使用stop
;pause
会占用资源,因为它预期在某个时候会恢复。duration
是样本的长度,以毫秒为单位。在样本加载完之前,它将为null
。注意,对于精灵,它是整个文件的长度。sound
是一个后备选项。它让你访问底层的Howl
实例。查看 Howler 文档 了解如何使用它。注意,在组件挂载后的最初几个时刻,这将是null
。
高级
精灵
音频精灵是一个包含多个样本的单个音频文件。你可以加载一个单独的文件,并将其切分成多个可以独立触发的部分,而不是加载许多单独的声音。
这可能会带来性能优势,因为它减少了并行网络请求,但如果单个组件需要多个样本,这也可能值得这么做。参见 鼓机示例 作为例子。
对于精灵,我们需要定义一个 SpriteMap
。它看起来像这样:
const spriteMap = {
laser: [0, 300],
explosion: [1000, 300],
meow: [2000, 75],
};
SpriteMap
是一个对象。键是个别声音的 id
。值是一个包含 2 个项目的元组(固定长度的数组):
- 样本的开始时间,以毫秒为单位,从样本的最开始计算
- 样本的长度,以毫秒为单位。
这个可视化可能会使它更清晰:
我们可以将 SpriteMap 作为 HookOptions 之一传递:
const [play] = useSound('/path/to/sprite.mp3', {
sprite: {
laser: [0, 300],
explosion: [1000, 300],
meow: [2000, 75],
},
});
要播放特定的精灵,我们在调用 play
函数时传递其 id
:
<button
onClick={() => play({id: 'laser'})}
>
后备选项
Howler 是一个非常强大的库,我们在 useSound
中只暴露了它能做的很小一部分。我们提供两个后备选项来给你更多控制。
首先,你传递给 HookOptions
的任何未识别的选项都将被委托给 Howl
。你可以在 Howler 文档中看到完整列表。这里是一个例子,展示了我们如何使用 onend
在声音停止播放时触发一个函数:
const [play] = useSound('/thing.mp3', {
onend: () => {
console.info('声音结束了!');
},
});
如果你需要更多控制,你应该能够直接使用 sound
对象,它是 Howler 的一个实例。
例如:Howler 暴露了一个 fade
方法,让你可以淡入或淡出声音。你可以直接在 sound
对象上调用这个方法:
const Arcade = () => {
const [play, { sound }] = useSound('/win-theme.mp3');
return (
<button
onClick={() => {
// 你赢了!淡入胜利主题
sound.fade(0, 1, 1000);
}}
>
点击获胜
</button>
);
};