vlambda博客
学习文章列表

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

作者 | Oren Farhi
译者 | 王强
策划 | 李俊辰
在本文中我决定走技术路线,分享我编写自定义 hooks 和集成某些函数式编程策略的经验。本文介绍了一个自定义 hook:useRecorder()。

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

“useRecorder()”规范

我为 ReadM(https://readm.netlify.app/)创建了 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 自定义 Hook

针对 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>()
为了缓存每个实例的 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,
  }
}
函数式 JS:创建一个 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,这款程序能通过实时反馈树立儿童阅读和讲出英语的信心(更多语种正在开发中):

https://readm.netlify.app/

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

作者介绍

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

https://github.com/orizens

原文链接

https://orizens.com/about