在函数式编程中使用自定义 React Hooks

阅读数:2 2020 年 4 月 30 日 14:32

在函数式编程中使用自定义React Hooks

本文最初发布于 Orizens 博客,经原作者 Oren Farhi 授权,由 InfoQ 中文站翻译并分享。

在本文中我决定走技术路线,分享我编写自定义 hooks 和集成某些函数式编程策略的经验。本文介绍了一个自定义 hook:useRecorder()。

“useRecorder()”规范

我为 ReadM™创建了 useRecorder(), ReadM™是一款免费且易用的阅读 Web 应用,它可以激励孩子们通过实时反馈来练习、学习、阅读和讲出英语,并提供了很好的体验。

在函数式编程中使用自定义React Hooks

这个 hook 的功能是提供一个录制器:

  • 它应该能录制一段音频
  • 它应该允许重播
  • 它应该提供明确的控件来开始和停止记录
  • 它应在应用程序处于活动状态时持续存在(在应用刷新 / 关闭时刷新)
  • 它应该提供对音频和播放器的完全访问权限

用法

我设计的 useRecorder() hook 是与段落组件一起使用的——这个段落组件由 3 个组件组成:分别是一个 Speaker、一个 Speech Tester 和一个 Recorder Button。Recorder Button 实际上是一个简单的圆形按钮,一旦用户读出了句子并得到了反馈,它就会出现。这样,用户点击录制按钮就可以重听自己最后一次录音。

上面的描述是在下面这段代码中实现的(我删除了一些实际代码来简化文章):

复制代码
export function Paragraph({ text, ...props }: ParagraphProps) {
const { start, stop, player } = useRecorder()
const handleEndResult = () => {
stop()
}
const handleStart = result => {
start()
}
return (
<section>
<Speaker
text={text}
disable={isReading}
verified={speechResult}
highlight={verified}
speed={speed}
/>
<SpeechTester onStart={handleStart} onResult={handleEndResult} />
<ButtonIcon
icon="play-circle"
title="Listen to your voice"
onClick={playRecording}
/>
</section>
)
}

ReadM™ recorder 显示在图中第一句话“the power of your subconscious mind" 的右侧,是一个带有白色“播放”图标的黑色椭圆形。

在函数式编程中使用自定义React Hooks

可重用的 React 自定义 Hook:实现

针对useRecorder()的音频录制功能,我发现了一个不错的软件包,可以抽象并简化录音操作: mic-recorder-to-mp3

由于使用了这个模块,我的 hook 的代码变得非常短。但它也简化了自己的构建块。

我创建了两个状态分别用来保存音频和播放器。

复制代码
const [audio, setAudio] = useState<File>()
const [player, setPlayer] = useState<HTMLAudioElement>()

为了缓存每个实例的 recorder,我使用了一个 ref:

复制代码
const recorderInstance = useRef<MicRecorder>(() => undefined)

start()函数使用一个新的录制实例来更新 recorderInstance。这个实例是用来停止录制的函数。我决定使用useEffect()Observables,将构造函数的返回值用作destroy/cancel功能(请注意,我正在检查这里是否支持录制,后文具体介绍):

复制代码
const start = () => {
if (supportsRecordingWithSpeech) recorderInstance.current = record()
}

record()函数是三个函数的函数式组合,本节中将具体介绍。
接下来,async stop()函数返回对 Blob 音频文件的引用,以及一个可在任何给定时间播放音频的音频播放器实例。这些保存在这个 hook 开始的状态之内。

复制代码
const stop = async () => {
if (supportsRecordingWithSpeech) {
const { file, audioPlayer } = await recorderInstance.current()
setAudio(file)
setPlayer(audioPlayer)
}
}

目前为止,Android 中还无法通过 WebAPI 录制语音。我正在使用 navigator 的 userAgent 对象来确定代码是在移动平台还是 Android 平台上运行。为了避免这个 hook 错误,**start()stop()** 都会在运行之前执行检查。

复制代码
const supportsRecordingWithSpeech =
navigator.userAgent.match(/(mobile)|(android)/im) === null
export function useRecorder() {
const [audio, setAudio] = useState<File>()
const [player, setPlayer] = useState<HTMLAudioElement>()
const recorderInstance = useRef<MicRecorder>(() => undefined)
const start = () => {
if (supportsRecordingWithSpeech) recorderInstance.current = record()
}
const stop = async () => {
if (supportsRecordingWithSpeech) {
const { file, audioPlayer } = await recorderInstance.current()
setAudio(file)
setPlayer(audioPlayer)
}
}
return {
start,
stop,
audio,
player,
}
}

函数式 Javascript:创建一个 Recorder

随着 ReadM™的发展,我更深入地尝试了在 JavaScript 中的函数式编程。

由于 ReadM™利用了 Redux 来编写record()函数,因此我导入了 redux 的compose()

复制代码
import { compose } from "redux"

**compose()函数接受任意数量的参数。这些参数必须是函数。compose()最后一个参数开始依次调用这些函数(pipe也会执行相同的操作,但会从第一个参数开始)。
每个函数的结果将传递到下一个函数。由函数的最终目标来决定返回值是什么——这就实现了某种“可链接性”,所以可以与
compose()** 序列一起使用。

使用record()时,首先运行的是setupMic(),然后一个接一个地调用函数,同时接收后者的返回值。

复制代码
const record = compose(
attachStopRecording,
startRecording,
setupMic
)

setupMic()创建 recorder 的新实例并返回它:

复制代码
function setupMic() {
return new MicRecorder({
bitRate: 128,
})
}

接下来,以 recorder 实例作为参数调用startRecording(recorder)。它也返回 recorder。虽说这个函数只是在更广泛的上下文中调用start(),但它允许执行与启动音频有关的其他逻辑或其他一些操作:

复制代码
function startRecording(recorder: MicRecorder) {
recorder.start()
return recorder
}

最后,使用相同的 recorder 实例作为参数调用attachStopRecording(recorder)。此函数返回一个新函数——recorder 的stop()功能,该函数返回文件(blob 缓冲区)和加载了此文件的音频播放器实例。
汇总在一起:

复制代码
function setupMic() {
return new MicRecorder({
bitRate: 128,
})
}
function startRecording(recorder: MicRecorder) {
recorder.start()
return recorder
}
function attachStopRecording(recorder: MicRecorder) {
return () =>
recorder
.stop()
.getMp3()
.then(([buffer, blob]) => {
const file = new File(buffer, "reading.mp3", {
type: blob.type,
lastModified: Date.now(),
})
const audioPlayer = new Audio(URL.createObjectURL(file))
return { file, audioPlayer }
})
.catch(e => {
console.error(`Something went wrong with the recording ${e}`)
})
}
const record = compose(
attachStopRecording,
startRecording,
setupMic
)

如果你喜欢箭头函数,则代码将变为:

复制代码
const setupMic = () => new MicRecorder({ bitRate: 128 })
const startRecording = (recorder: MicRecorder) => recorder.start() && recorder
const attachStopRecording = (recorder: MicRecorder) => () =>
recorder
.stop()
.getMp3()
.then(([buffer, blob]) => {
const file = new File(buffer, "reading.mp3", {
type: blob.type,
lastModified: Date.now(),
})
const audioPlayer = new Audio(URL.createObjectURL(file))
return { file, audioPlayer }
})
.catch(e => {
console.error(`Something went wrong with the recording ${e}`)
})
const record = compose(
attachStopRecording,
startRecording,
setupMic
)

函数式编程的好处

在开发过程中,我一直在问一个问题:它能给我带来什么好处?

首先,我从几个函数开始来编写和创建功能,并确保它们以某种方式链接在一起,让“链”得以正常运转。这些函数可重用于其他目的——我可能在其他场景中用它们实现其他操作或功能。

测试变得更加模块化,更加精确,并与可自我操作的单元隔离开来。每个单元的职责变得更小,只需测试一个简单任务即可。

总的来说,我很满意最后的结果。写出来的代码小巧、简单且易于维护。几个月后再回来看这段代码,我也可以很快地阅读并理解它。

进一步改善

我一直在思考如何改进现有代码。可以将一些可选配置添加到这个 hooks 的函数签名中,例如:结果文件名、录制比特率、不同的文件类型等。

我们可以进一步提高实现的响应性,并创建单个“activate()”函数来使 **start()stop()** 函数作为 effects,让前者触发这两个操作。

请查看我们的革命性应用 ReadM™,这款程序能通过实时反馈树立儿童阅读和讲出英语的信心(更多语种正在开发中)。

我会基于 ReadM™的开发经验,撰写更多有用的文章。

作者介绍

Oren Farhi 是前端工程师和 JS 顾问。他的作品包括 ReadM™ Echoes Player ngx-infinite-scroll 等。他撰写了《 Angular 和 NgRx 的响应式编程》一书。这里是他的开源项目列表

原文链接: https://orizens.com/blog/how-to-functional-programming-with-custom-react-hooks/

评论

发布