## Middleware
我們已經在異步 Action 一節的示例中看到了一些 middleware 的使用。如果你使用過 Express 或者 Koa 等服務端框架, 那么應該對 middleware 的概念不會陌生。 在這類框架中,middleware 是指可以被嵌入在框架接收請求到產生響應過程之中的代碼。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers、記錄日志、內容壓縮等工作。middleware 最優秀的特性就是可以被鏈式組合。你可以在一個項目中使用多個獨立的第三方 middleware。
相對于 Express 或者 Koa 的 middleware,Redux middleware 被用于解決不同的問題,但其中的概念是類似的。它提供的是位于 action 被發起之后,到達 reducer 之前的擴展點。 你可以利用 Redux middleware 來進行日志記錄、創建崩潰報告、調用異步接口或者路由等等。
這個章節分為兩個部分,前面是幫助你理解相關概念的深度介紹,而后半部分則通過一些實例來體現 middleware 的強大能力。對文章前后內容進行結合通讀,會幫助你更好的理解枯燥的概念,并從中獲得啟發。
### 理解 Middleware
正因為 middleware 可以完成包括異步 API 調用在內的各種事情,了解它的演化過程是一件相當重要的事。我們將以記錄日志和創建崩潰報告為例,引導你體會從分析問題到通過構建 middleware 解決問題的思維過程。
#### 問題: 記錄日志
使用 Redux 的一個益處就是它讓 state 的變化過程變的可預知和透明。每當一個 action 發起完成后,新的 state 就會被計算并保存下來。State 不能被自身修改,只能由特定的 action 引起變化。
試想一下,當我們的應用中每一個 action 被發起以及每次新的 state 被計算完成時都將它們記錄下來,豈不是很好?當程序出現問題時,我們可以通過查閱日志找出是哪個 action 導致了 state 不正確。
我們如何通過 Redux 實現它呢?
#### 嘗試 #1: 手動記錄
最直接的解決方案就是在每次調用 `store.dispatch(action) `前后手動記錄被發起的 action 和新的 state。這稱不上一個真正的解決方案,僅僅是我們理解這個問題的第一步。
#### 注意
如果你使用 react-redux 或者類似的綁定庫,最好不要直接在你的組件中操作 store 的實例。在接下來的內容中,僅僅是假設你會通過 store 顯式地向下傳遞。
假設,你在創建一個 Todo 時這樣調用:
`store.dispatch(addTodo('Use Redux'))`
為了記錄這個 action 以及產生的新的 state,你可以通過這種方式記錄日志:
~~~
let action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
~~~
雖然這樣做達到了想要的效果,但是你并不想每次都這么干。
#### 嘗試 #2: 封裝 Dispatch
你可以將上面的操作抽取成一個函數:
~~~
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
~~~
然后用它替換 store.dispatch():
`dispatchAndLog(store, addTodo('Use Redux'))`
你可以選擇到此為止,但是每次都要導入一個外部方法總歸還是不太方便。
#### 嘗試 #3: Monkeypatching Dispatch
如果我們直接替換 store 實例中的 dispatch 函數會怎么樣呢?Redux store 只是一個包含一些方法的普通對象,同時我們使用的是 JavaScript,因此我們可以這樣實現 dispatch 的 monkeypatch:
~~~
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
~~~
這離我們想要的已經非常接近了!無論我們在哪里發起 action,保證都會被記錄。Monkeypatching 令人感覺還是不太舒服,不過利用它我們做到了我們想要的。
### 問題: 崩潰報告
如果我們想對 dispatch 附加超過一個的變換,又會怎么樣呢?
我腦海中出現的另一個常用的變換就是在生產過程中報告 JavaScript 的錯誤。全局的 window.onerror 并不可靠,因為它在一些舊的瀏覽器中無法提供錯誤堆棧,而這是排查錯誤所需的至關重要信息。
試想當發起一個 action 的結果是一個異常時,我們將包含調用堆棧,引起錯誤的 action 以及當前的 state 等錯誤信息通通發到類似于 Sentry 這樣的報告服務中,不是很好嗎?這樣我們可以更容易地在開發環境中重現這個錯誤。
然而,將日志記錄和崩潰報告分離是很重要的。理想情況下,我們希望他們是兩個不同的模塊,也可能在不同的包中。否則我們無法構建一個由這些工具組成的生態系統。(提示:我們正在慢慢了解 middleware 的本質到底是什么!)
如果按照我們的想法,日志記錄和崩潰報告屬于不同的模塊,他們看起來應該像這樣:
~~~
function patchStoreToAddLogging(store) {
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function patchStoreToAddCrashReporting(store) {
let next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('捕獲一個異常!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
}
~~~
如果這些功能以不同的模塊發布,我們可以在 store 中像這樣使用它們:
~~~
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
~~~
盡管如此,這種方式看起來還是不是夠令人滿意。
#### 嘗試 #4: 隱藏 Monkeypatching
Monkeypatching 本質上是一種 hack。“將任意的方法替換成你想要的”,此時的 API 會是什么樣的呢?現在,讓我們來看看這種替換的本質。 在之前,我們用自己的函數替換掉了 store.dispatch。如果我們不這樣做,而是在函數中返回新的 dispatch 呢?
~~~
function logger(store) {
let next = store.dispatch
// 我們之前的做法:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
~~~
我們可以在 Redux 內部提供一個可以將實際的 monkeypatching 應用到 store.dispatch 中的輔助方法:
~~~
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
// 在每一個 middleware 中變換 dispatch 方法。
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}
~~~
然后像這樣應用多個 middleware:
`applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])`
盡管我們做了很多,實現方式依舊是 monkeypatching。
因為我們僅僅是將它隱藏在我們的框架內部,并沒有改變這個事實。
#### 嘗試 #5: 移除 Monkeypatching
為什么我們要替換原來的 dispatch 呢?當然,這樣我們就可以在后面直接調用它,但是還有另一個原因:就是每一個 middleware 都可以操作(或者直接調用)前一個 middleware 包裝過的 store.dispatch:
~~~
function logger(store) {
// 這里的 next 必須指向前一個 middleware 返回的函數:
let next = store.dispatch
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
~~~
將 middleware 串連起來的必要性是顯而易見的。
如果 applyMiddlewareByMonkeypatching 方法中沒有在第一個 middleware 執行時立即替換掉 store.dispatch,那么 store.dispatch 將會一直指向原始的 dispatch 方法。也就是說,第二個 middleware 依舊會作用在原始的 dispatch 方法。
但是,還有另一種方式來實現這種鏈式調用的效果。可以讓 middleware 以方法參數的形式接收一個 next() 方法,而不是通過 store 的實例去獲取。
~~~
function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}
~~~
現在是“我們該更進一步”的時刻了,所以可能會多花一點時間來讓它變的更為合理一些。這些串聯函數很嚇人。ES6 的箭頭函數可以使其 柯里化 ,從而看起來更舒服一些:
~~~
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
~~~
這正是 Redux middleware 的樣子。
Middleware 接收了一個 next() 的 dispatch 函數,并返回一個 dispatch 函數,返回的函數會被作為下一個 middleware 的 next(),以此類推。由于 store 中類似 getState() 的方法依舊非常有用,我們將 store 作為頂層的參數,使得它可以在所有 middleware 中被使用。
#### 嘗試 #6: “單純”地使用 Middleware
我們可以寫一個 applyMiddleware() 方法替換掉原來的 applyMiddlewareByMonkeypatching()。在新的 applyMiddleware() 中,我們取得最終完整的被包裝過的 dispatch() 函數,并返回一個 store 的副本:
// 警告:這只是一種“單純”的實現方式!
// 這 *并不是* Redux 的 API.
~~~
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return Object.assign({}, store, { dispatch })
}
~~~
這與 Redux 中 applyMiddleware() 的實現已經很接近了,但是有三個重要的不同之處:
* 它只暴露一個 store API 的子集給 middleware:dispatch(action) 和 getState()。
* 它用了一個非常巧妙的方式,以確保如果你在 middleware 中調用的是 store.dispatch(action) 而不是 next(action),那么這個操作會再次遍歷包含當前 middleware 在內的整個 middleware 鏈。這對異步的 middleware 非常有用,正如我們在之前的章節中提到的。
* 為了保證你只能應用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的簽名不是 (store, middlewares) => store, 而是 (...middlewares) => (createStore) => createStore。
由于在使用之前需要先應用方法到 createStore() 之上有些麻煩,createStore() 也接受將希望被應用的函數作為最后一個可選參數傳入。
#### 最終的方法
這是我們剛剛所寫的 middleware:
~~~
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
~~~
然后是將它們引用到 Redux store 中:
~~~
import { createStore, combineReducers, applyMiddleware } from 'redux'
let todoApp = combineReducers(reducers)
let store = createStore(
todoApp,
// applyMiddleware() 告訴 createStore() 如何處理中間件
applyMiddleware(logger, crashReporter)
)
~~~
就是這樣!現在任何被發送到 store 的 action 都會經過 logger 和 crashReporter:
// 將經過 logger 和 crashReporter 兩個 middleware!
store.dispatch(addTodo('Use Redux'))
7個示例
如果讀完上面的章節你已經覺得頭都要爆了,那就想象一下把它寫出來之后的樣子。下面的內容會讓我們放松一下,并讓你的思路延續。
下面的每個函數都是一個有效的 Redux middleware。它們不是同樣有用,但是至少他們一樣有趣。
~~~
/**
* 記錄所有被發起的 action 以及產生的新的 state。
*/
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
/**
* 在 state 更新完成和 listener 被通知之后發送崩潰報告。
*/
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
/**
* 用 { meta: { delay: N } } 來讓 action 延遲 N 毫秒。
* 在這個案例中,讓 `dispatch` 返回一個取消 timeout 的函數。
*/
const timeoutScheduler = store => next => action => {
if (!action.meta || !action.meta.delay) {
return next(action)
}
let timeoutId = setTimeout(
() => next(action),
action.meta.delay
)
return function cancel() {
clearTimeout(timeoutId)
}
}
/**
* 通過 { meta: { raf: true } } 讓 action 在一個 rAF 循環幀中被發起。
* 在這個案例中,讓 `dispatch` 返回一個從隊列中移除該 action 的函數。
*/
const rafScheduler = store => next => {
let queuedActions = []
let frame = null
function loop() {
frame = null
try {
if (queuedActions.length) {
next(queuedActions.shift())
}
} finally {
maybeRaf()
}
}
function maybeRaf() {
if (queuedActions.length && !frame) {
frame = requestAnimationFrame(loop)
}
}
return action => {
if (!action.meta || !action.meta.raf) {
return next(action)
}
queuedActions.push(action)
maybeRaf()
return function cancel() {
queuedActions = queuedActions.filter(a => a !== action)
}
}
}
/**
* 使你除了 action 之外還可以發起 promise。
* 如果這個 promise 被 resolved,他的結果將被作為 action 發起。
* 這個 promise 會被 `dispatch` 返回,因此調用者可以處理 rejection。
*/
const vanillaPromise = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
return Promise.resolve(action).then(store.dispatch)
}
/**
* 讓你可以發起帶有一個 { promise } 屬性的特殊 action。
*
* 這個 middleware 會在開始時發起一個 action,并在這個 `promise` resolve 時發起另一個成功(或失敗)的 action。
*
* 為了方便起見,`dispatch` 會返回這個 promise 讓調用者可以等待。
*/
const readyStatePromise = store => next => action => {
if (!action.promise) {
return next(action)
}
function makeAction(ready, data) {
let newAction = Object.assign({}, action, { ready }, data)
delete newAction.promise
return newAction
}
next(makeAction(false))
return action.promise.then(
result => next(makeAction(true, { result })),
error => next(makeAction(true, { error }))
)
}
/**
* 讓你可以發起一個函數來替代 action。
* 這個函數接收 `dispatch` 和 `getState` 作為參數。
*
* 對于(根據 `getState()` 的情況)提前退出,或者異步控制流( `dispatch()` 一些其他東西)來說,這非常有用。
*
* `dispatch` 會返回被發起函數的返回值。
*/
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action)
// 你可以使用以上全部的 middleware!(當然,這不意味著你必須全都使用。)
let todoApp = combineReducers(reducers)
let store = createStore(
todoApp,
applyMiddleware(
rafScheduler,
timeoutScheduler,
thunk,
vanillaPromise,
readyStatePromise,
logger,
crashReporter
)
)
~~~
- js
- js繼承
- keyCode
- 好的網站
- 零散知識點-js
- This
- 對象深拷貝和淺拷貝
- 數組方法
- 數組的深拷貝和淺拷貝
- JS 引擎的執行機制
- js中的new
- 常用正則
- 函數柯里化
- 會修改當前數組的方法
- 不會修改當前數組的方法
- 函數式編程
- 循環遍歷
- 基礎知識
- 異步
- js知識總結
- fileReader
- HTML
- 零散知識點
- html5新特性
- viewport
- CSS
- cursor
- css3新特性
- 水平居中
- 垂直居中
- display解析
- 塊級元素和行內元素
- css技巧和方法
- 清除浮動
- Less
- Sass
- 綜合
- 微信小程序
- 前端面試
- CSS-面試
- JS-面試
- js-web-api
- js知識
- MVC-面試
- jQuery與框架的區別
- 閉包
- promise
- http狀態碼
- cdn
- 離線存儲
- 事件
- web安全
- 性能優化
- 響應式
- 服務器渲染和本地渲染
- 模板是什么?
- VUE流程
- 瀏覽器渲染過程
- this的指向
- new的使用
- HTML-面試
- title和alt區別
- html5元素
- h5新特性
- 圖片格式
- 零散面試總結
- react
- 生命周期-react
- state
- props
- 組件通信
- 虛擬DOM
- 源碼分析
- webstorm-template
- element與component區別
- 組件的理解
- JXS
- vue與react區別
- 16.8版本
- vue
- 生命周期-vue
- 實現流程
- webpack
- 概念
- 入口起點
- 出口
- loader
- 模式
- 插件
- manifest
- redux
- 介紹
- 核心概念
- 三大原則
- 基礎
- action
- reducer
- store
- 數據流
- 高級
- 異步action
- 異步數據流
- middleware
- ES6阮一峰
- ...
- let
- es6箭頭函數
- const
- 塊級作用域
- 頂層對象的屬性
- global 對象
- 變量的解構賦值
- 字符串的擴展
- promise對象
- 正則的擴展
- 數值的擴展
- Math對象的擴展
- 函數的擴展
- 數組的擴展
- 對象的擴展
- symbol
- async函數
- class的基本用法
- Class 的繼承
- Set 和 Map 數據結構
- 開發工具
- 好用的軟件
- chrome插件
- 其他實用工具
- 微信公眾號-前端早讀課
- 【第1352期】map和reduce,處理數據結構的利器
- 微信公眾號-前端大全
- JS 的執行機制
- 一篇文章理解 JS 繼承
- 瀏覽器
- 緩存
- 《Webkit技術內幕》之頁面渲染過程
- 跨域
- 安全
- XSS
- 設計模式
- 發布訂閱模式
- 工廠模式
- MV*模式
- 觀察者模式
- react-router
- 一些小技巧
- js一些小算法
- 1.已知一個數組中的值,在另外一個數組中查找該值
- 累加器
- 數組隨機
- 數組扁平化并去重排序
- Immutable
- 常用命令
- hybrid
- schema封裝
- typescript