在函數式編程中使用自定義React Hooks

2021-02-19 前端之巔
在本文中我決定走技術路線,分享我編寫自定義 hooks 和集成某些函數式編程策略的經驗。本文介紹了一個自定義 hook:useRecorder()。

本文最初發布於 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>()

為了緩存每個實例的 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,
  }
}

隨著 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

相關焦點

  • 函數式編程看React Hooks(一)簡單React Hooks實現
    前言函數式編程介紹(摘自基維百科)函數式編程(英語:functional programming)或稱函數程序設計、泛函編程,是一種編程範式,它將計算機運算視為函數運算,並且避免使用程序狀態以及易變對象。
  • React Hooks使用小結
    在hooks出生之前,react沒有將可復用性行為添加到組件到能力,針對這個問題廣大開發者開始使用renderProps 或者 高階組件來實現此目的。但是,說實話這些方法還是比較麻煩的,需要重新組織項目或者組件的組織結構。
  • 10分鐘教你手寫8個常用的自定義hooks
    它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。本文是一篇以實戰為主的文章,主要講解實際項目中如何使用hooks以及一些最佳實踐,不會一步步再介紹一遍react hooks的由來和基本使用,因為寫hooks的文章很多,而且官網對於react hooks的介紹也很詳細,所以大家不熟悉的可以看一遍官網。
  • 30 分鐘精通 React 新特性——React Hooks
    ——擁有了hooks,你再也不需要寫Class了,你的所有組件都將是Function。你還在為搞不清使用哪個生命周期鉤子函數而日夜難眠嗎?——擁有了Hooks,生命周期鉤子函數可以先丟一邊了。你在還在為組件中的this指向而暈頭轉向嗎?——既然Class都丟掉了,哪裡還有this?你的人生第一次不再需要面對this。
  • 超性感的React Hooks(四):useEffect
    許多朋友試圖利用class語法中的生命周期來類比理解useEffect,也許他們認為,hooks只是語法糖而已。那麼,即使正在使用hooks,也有可能對我上面這一段話表示不理解,甚至還會問:不類比生命周期,怎麼學習hooks?我不得不很明確的告訴大家,生命周期和useEffect是完全不同的。
  • 你可能不知道的 React Hooks
    由於 Level01 函數在每次渲染發生時被調用,所以每次觸發渲染時這個組件都會創建新的 interval。突變、訂閱、計時器、日誌記錄和其他副作用不允許出現在函數組件的主體中(稱為 React 的 render 階段)。 這樣做會導致用戶界面中的錯誤和不一致。
  • React Hooks 設計思想
    此時,NewsItem 便不是一個純的組件了,因為與外部有了交互,這種與外部的交互被稱為副作用(函數式編程裡沒有任何副作用的函數被稱為純函數)。組件的副作用是不可避免的,最常見的有 fetch data,訂閱事件,進行 DOM 操作,使用其他 JavaScript 庫(比如 jQuery,Map 等)。
  • 寫React Hooks前必讀
    必開規則:{ "plugins": ["react-hooks"], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" }}其中,react-hooks/exhaustive-deps
  • 使用React Hooks代替類的6大理由
    在本文中,我們將探討考慮使用 React Hooks 的六個原因。1. 擴展函數式組件時,不必將其重構為類組件經常會有這種情況,那就是一個 React 組件從一個函數式組件開始開發,一開始這個函數式組件只依賴 props,後來演變為具有狀態的類組件。從函數式組件更改為類組件需要一些重構工作,具體取決於組件的複雜程度。
  • 30 分鐘精通 React 今年最勁爆的新特性 —— React Hooks
    ——擁有了hooks,你再也不需要寫Class了,你的所有組件都將是Function。你還在為搞不清使用哪個生命周期鉤子函數而日夜難眠嗎? ——擁有了Hooks,生命周期鉤子函數可以先丟一邊了。你在還在為組件中的this指向而暈頭轉向嗎? ——既然Class都丟掉了,哪裡還有this?你的人生第一次不再需要面對this。
  • React Hooks 還不如類?
    你必須遵循一些嚴格而怪異的規則:https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level需要注意代碼放置的位置,並且這裡面存在許多陷阱。我不能將一個 hook 放在一個 if 語句中,因為 hooks 的內部機制是基於調用順序的,這簡直太瘋狂了!
  • React 16.8發布:hooks終於來了!
    相反,可以在一些新組件中嘗試使用 hooks,並讓我們知道你的想法。使用 hooks 的代碼仍然可以與使用類的現有代碼並存。是的!建議啟用一個叫作 eslint-plugin-react-hooks(https://www.npmjs.com/package/eslint-plugin-react-hooks)的 lint 規則來強制執行 hooks 的最佳實踐,它很快會被包含在 Create React App 中。
  • React Hook起飛指南
    但是,我們經常無法進一步破壞複雜組件,因為邏輯是有狀態的,無法提取到函數或其他組件中。而hook讓我們可以將組件內部的邏輯組織成可重用的隔離單元。所以,一句話總結hook帶來的變革就是:將可復用的最小單元從組件層面進一步細化到邏輯層面。
  • React Hooks 原理與最佳實踐
    Custom Hooks對於 react 來說,在函數組件中使用 state 固然有一些價值,但最有價值的還是可以編寫通用 custom hooks 的能力。想像一下,一個單純不依賴 UI 的業務邏輯 hook,我們開箱即用。不僅可以在不同的項目中復用,甚至還可以跨平臺使用,react、react native、react vr 等等。
  • 寫React Hooks前需要注意什麼?
    "react-hooks/rules-of-hooks": "error",    "react-hooks/exhaustive-deps": "warn"  }}其中, react-hooks/exhaustive-deps 至少warn,也可以是error。
  • React hooks 最佳實踐【更新中】
    1.儘量設計簡單的hookshooks 設計的初衷就是為了使開發更加快捷簡便,因此在使用hooks 的時候,我們不應該吝嗇使用較多的hooks,例如我們處理不同狀態對應不同邏輯的時候,按照寫class的邏輯,我們經常會在一個生命周期函數裡寫下多個邏輯,並用if區分;在寫hooks的時候,因為沒有shouldComponentUpdate這類的生命周期函數,我們應該將他們分離開,將他們寫在不同的useEffect
  • 【實戰總結篇】寫React Hooks前必讀
    "react-hooks/rules-of-hooks": "error",    "react-hooks/exhaustive-deps": "warn"  }}其中, react-hooks/exhaustive-deps 至少warn,也可以是error。
  • React Hooks 與Vue3.0 Function based API的對比?
    引用官網的一段話:從概念上講,React 組件更像是函數。而 Hooks 則擁抱了函數,同時也沒有犧牲 React 的精神原則。Hooks 提供了問題的解決方案,無需學習複雜的函數式或響應式編程技術。另外,Hooks 是100%向後兼容的,也是完全可選的。
  • 「混合雙打」之如何在 Class Components 中使用 React Hooks
    前情提要React 在 v16.8.0 版本中推出了 Hook,作為純函數組件的增強,給函數組件帶來了狀態、上下文等等;之前一篇關於 React Hooks 的文章介紹了如何使用一些官方鉤子和如何自建鉤子,如果想要了解這些內容的同學可以訪問《看完這篇,你也能把 React Hooks 玩出花》。
  • react中關於hook介紹及其使用
    前言最近由於公司的項目開發,就學習了在react關於hook的使用,對其有個基本的認識以及如何在項目中去應用hook。簡單來說就是可以使用函數組件去使用react中的一些特性所要解決的問題:解決組件之間復用狀態邏輯很難得問題,hook能解決的就是在你無需修改之前組件結構的情況下復用狀態邏輯,在不使用hook的情況下,需要使用到一些高級的用法如高級組件、provider、customer等,這種方式對於新手來說不太友好