抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

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

2020 年 4 月 30 日

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

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


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


“useRecorder()”规范

我为ReadM™创建了 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"的右侧,是一个带有白色“播放”图标的黑色椭圆形。



可重用的 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 Playerngx-infinite-scroll等。他撰写了《Angular和NgRx的响应式编程》一书。这里是他的开源项目列表


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


2020 年 4 月 30 日 14:32807

评论

发布
暂无评论
发现更多内容

话题讨论|做程序员五年后是什么样子?

饭饭

程序员 职业规划 发展现状 内卷 IT行业

🕋【Redis干货领域】从底层彻底吃透AOF重写(原理篇)

李浩宇/Alex

redis持久化 aof Redis 核心技术与实战 5月日更

话题讨论|程序员在520最想收到什么礼物?

饭饭

程序员 程序员恋爱 恋爱 520 单身

现在后端都在用什么数据库存储数据?

读字节

MySQL 数据库 postgresql hadoop HBase

Springboot结合Netty实战聊天系统

Damon

音视频

去年创建的个人网站,我又给它加多了一些新功能。

彭宏豪95

写作 网站 博客 5月日更

数字化转型助推,200亿元数据治理市场空间充满想象

DT极客

Sentinel在docker中获取CPU利用率的一个BUG

捉虫大师

Java Docker sentinel

五一去见了一些身价数千万的成功人士,学到的认知和启示

陆陆通通

程序员 赚钱 认知

打破思维定式(十二)

Changing Lin

实践解析 | 如何用 OpenGL 实现跨平台应用高效渲染

拍乐云Pano

Android开发

详解支撑7亿用户搜索的百度图片处理收录中台

百度Geek说

中台 搜索 图片处理

手撕友商7nm FPGA?英特尔“亲儿子”上阵

新闻科技资讯

限量!Alibaba首发“SpringBoot实战笔记”,差距不是一点点

互联网架构师小马

Java spring 微服务 Spring Boot

前端领域的数据状态统一管理机制

鲸品堂

前端 数据 流程图 state

在 Mac 上玩网游的简单方式

懒得勤快

华为发布HarmonyOS Connect品牌升级计划 帮伙伴做好产品、卖好产品、运营好产品

科技汇

2021年5月墨天轮国产数据库排行榜:十强榜单固若金汤

墨天轮

数据库 腾讯云 阿里云 国产化 dba

面试官:啥是请求重放呀?

why技术

Java

鸿蒙轻内核M核源码分析:数据结构之任务就绪队列

华为云开发者社区

鸿蒙 数据结构 数组 双向循环链表 任务就绪队列

Rust从0到1-集合-Hash Map

rust hashmap 集合 Collections hash map

Elasticsearch数据库优化实战:让你的ES飞起来

华为云开发者社区

数据库 大数据 elasticsearch 日志 ES

Apache Flink在 bilibili 的多元化探索与实践

Apache Flink

大数据 flink 流计算 实时计算

ShardingSphere 源码

云淡风轻

ShardingSphere

iOS 面试策略之系统框架-网络、推送与数据处理

iOSer

ios

百度 Serverless 函数计算引擎 EasyFaaS 正式开源

百度开发者中心

百度 开源

快时代的知识形态

Ryan Zheng

如何自学 Java ?不报班只白嫖行不行?

Java架构师迁哥

云时代的数据之约

CloudQuery社区

数据库 云计算 运维 云服务 dba

牛!马士兵亲自教授坦克大战+精通23种设计模式,视频+笔记+源码

Java架构追梦

Java 架构 面试 23种设计模式 坦克大战

模块四作业

Chris Cheng

架构实战营

Study Go: From Zero to Hero

Study Go: From Zero to Hero

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