跳至主要內容

撰寫自訂中間件

您將學到
  • 何時使用自訂中介軟體
  • 中介軟體的標準模式
  • 如何確保中介軟體與其他 Redux 專案相容

Redux 中的中介軟體主要用於

  • 為動作建立副作用,
  • 修改或取消動作,或
  • 修改 dispatch 接受的輸入。

大多數使用案例都屬於第一類:例如 Redux-Sagaredux-observableRTK 監聽器中介軟體 都會建立對動作產生反應的副作用。這些範例也顯示出這是非常常見的需求:能夠對動作產生反應,而不仅仅是狀態變更。

修改動作可用於例如使用來自狀態或外部輸入的資訊來增強動作,或節流、防呆或限制動作。

修改 dispatch 輸入的最明顯範例是 Redux Thunk,它會透過呼叫傳回動作的函式,將函式轉換為動作。

何時使用自訂中介軟體

大多數時候,您實際上不需要自訂中介軟體。中介軟體最有可能的使用案例是副作用,而且有許多套件可以為 Redux 完善封裝副作用,而且使用時間已經夠長,可以解決您在自行建置時會遇到的細微問題。一個良好的起點是 RTK Query,用於管理伺服器端狀態,以及 RTK 監聽器中介軟體,用於其他副作用。

您可能仍想在以下兩種情況之一使用自訂中介軟體

  1. 如果您只有一個非常簡單的副作用,那麼新增一個完整的額外架構可能不值得。只要確保在您的應用程式成長後,您會切換到現有的架構,而不是擴充您自己的自訂解決方案。
  2. 如果您需要修改或取消動作。

中間件的標準模式

為動作建立副作用

這是最常見的中間件。以下是 rtk 偵聽器中間件 的範例

const middleware: ListenerMiddleware<S, D, ExtraArgument> =
api => next => action => {
if (addListener.match(action)) {
return startListening(action.payload)
}

if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}

if (removeListener.match(action)) {
return stopListening(action.payload)
}

// Need to get this state _before_ the reducer processes the action
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()

// `getOriginalState` can only be called synchronously.
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
const getOriginalState = (): S => {
if (originalState === INTERNAL_NIL_TOKEN) {
throw new Error(
`${alm}: getOriginalState can only be called synchronously`
)
}

return originalState as S
}

let result: unknown

try {
// Actually forward the action to the reducer before we handle listeners
result = next(action)

if (listenerMap.size > 0) {
let currentState = api.getState()
// Work around ESBuild+TS transpilation issue
const listenerEntries = Array.from(listenerMap.values())
for (let entry of listenerEntries) {
let runListener = false

try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false

safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate'
})
}

if (!runListener) {
continue
}

notifyListener(entry, action, api, getOriginalState)
}
}
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}

return result
}

在第一部分,它會偵聽 addListenerclearAllListenersremoveListener 動作,以變更稍後應呼叫哪些偵聽器。

在第二部分,程式碼主要會計算動作通過其他中間件和 reducer 之後的狀態,然後將原始狀態以及 reducer 中的新狀態傳遞給偵聽器。

在發送動作後產生副作用很常見,因為這允許同時考量原始狀態和新狀態,而且來自副作用的互動不應影響目前的動作執行(否則它就不會是副作用)。

修改或取消動作,或修改 dispatch 接受的輸入

雖然這些模式較不常見,但其中大部分(取消動作除外)都由 redux thunk 中間件 使用

const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}

// Otherwise, pass the action down the middleware chain as usual
return next(action)
}

通常,dispatch 只能處理 JSON 動作。此中間件新增了處理函式形式動作的能力。它也透過將函式動作的傳回值傳遞為 dispatch 函式的傳回值,來變更 dispatch 函式本身的傳回類型。

建立相容中間件的規則

原則上,中間件是一個非常強大的模式,可以對動作執行任何它想做的事。現有的中間件可能假設周圍的中間件會發生什麼事,而了解這些假設有助於確保您的中間件能與現有常見中間件順利運作。

我們的中間件與其他中間件之間有兩個接觸點

呼叫下一個中間件

當您呼叫 next 時,中間件會預期某種形式的動作。除非您想要明確修改它,否則只要傳遞您收到的動作即可。

更微妙的是,一些中間件預期在呼叫 dispatch 的同時也呼叫中間件,因此您的中間件應該同步呼叫 next

傳回 dispatch 回傳值

除非中間件需要明確修改 dispatch 的回傳值,否則只要傳回從 next 取得的內容即可。如果您確實需要修改回傳值,則您的中間件需要位於中間件鏈中的特定位置才能執行預期的功能 - 您需要手動檢查與所有其他中間件的相容性,並決定它們如何協同運作。

這會產生一個棘手的後果

const middleware: Middleware = api => next => async action => {
const response = next(action)

// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}

return response
}

即使看起來我們沒有修改回應,但我們實際上已經修改了:由於非同步等待,它現在是一個承諾。這會中斷某些中間件,例如 RTK Query 中的那些中間件。

那麼,我們該如何撰寫這個中間件呢?

const middleware: Middleware = api => next => action => {
const response = next(action)

// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
void loadData(api)
}

return response
}

async function loadData(api) {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}

只要將非同步邏輯移至一個獨立的函式中,這樣您仍然可以使用非同步等待,但實際上並未在中間件中等待承諾解析。void 表示給其他正在閱讀程式碼的人知道您決定不顯式等待承諾,而不會對程式碼產生影響。

後續步驟

如果您尚未執行此操作,請參閱 Redux 理解中的中間件部分,了解中間件在幕後如何運作。