在函数式编程中使用自定义React Hooks
本文最初发布于 Orizens 博客,经原作者 Oren Farhi 授权,由 InfoQ 中文站翻译并分享。
我为 ReadM(https://readm.netlify.app/)创建了 useRecorder(),ReadM 是一款免费且易用的阅读 Web 应用,它可以激励孩子们通过实时反馈来练习、学习、阅读和讲出英语,并提供了很好的体验。
这个 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"的右侧,是一个带有白色“播放”图标的黑色椭圆形。
针对 useRecorder() 的音频录制功能,我发现了一个不错的软件包,可以抽象并简化录音操作:
mic-recorder-to-mp3(https://www.npmjs.com/package/mic-recorder-to-mp3)
由于使用了这个模块,我的 hook 的代码变得非常短。但它也简化了自己的构建块。
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)
}
}
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,
}
}
随着 ReadM 的发展,我更深入地尝试了在 JavaScript 中的函数式编程。
import { compose } from "redux"
compose() 函数接受任意数量的参数。这些参数必须是函数。compose() 从 最后 一个参数开始依次调用这些函数(pipe 也会执行相同的操作,但会从第一个参数开始)。
每个函数的结果将传递到下一个函数。由函数的最终目标来决定返回值是什么——这就实现了某种“可链接性”,所以可以与 compose() 序列一起使用。
使用 record() 时,首先运行的是 setupMic(),然后一个接一个地调用函数,同时接收后者的返回值。
const record = compose(
attachStopRecording,
startRecording,
setupMic
)
function setupMic() {
return new MicRecorder({
bitRate: 128,
})
}
function startRecording(recorder: MicRecorder) {
recorder.start()
return recorder
}
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,这款程序能通过实时反馈树立儿童阅读和讲出英语的信心(更多语种正在开发中):
https://readm.netlify.app/
我会基于 ReadM 的开发经验,撰写更多有用的文章。
Oren Farhi 是前端工程师和 JS 顾问。他的作品包括 ReadM、Echoes Player、ngx-infinite-scroll 等。他撰写了《Angular 和 NgRx 的响应式编程》一书。这里是他的开源项目列表:
https://github.com/orizens
https://orizens.com/about