Categories
程式開發

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


本文最初發佈於Orizens博客,經原作者Oren Farhi授權,由InfoQ中文站翻譯並分享。

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

“useRecorder()”規範

我為ReadM™創建了useRecorder(),ReadM™是一款免費且易用的閱讀Web應用,它可以激勵孩子們通過實時反饋來練習、學習、閱讀和講出英語,並提供了很好的體驗。

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

這個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 (
    
) }

ReadM™ recorder顯示在圖中第一句話“the power of your subconscious mind”的右側,是一個帶有白色“播放”圖標的黑色橢圓形。

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

可重用的React自定義Hook:實現

針對useRecorder()的音頻錄製功能,我發現了一個不錯的軟件包,可以抽象並簡化錄音操作:mic-recorder-to-mp3

由於使用了這個模塊,我的hook的代碼變得非常短。但它也簡化了自己的構建塊。

我創建了兩個狀態分別用來保存音頻和播放器。

const [audio, setAudio] = useState()
const [player, setPlayer] = useState()

為了緩存每個實例的recorder,我使用了一個ref:

const recorderInstance = useRef(() => 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()
  const [player, setPlayer] = useState()
  const recorderInstance = useRef(() => 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/