在express和koa框架中,中间件是指 可以被嵌入在框架接收请求到产生相应过程之中的代码 ,例如,express和koa的中间件可以完成添加cors头,记录日志和内容压缩等工作。
中间件最优秀的就是可以被链式组合。可以在一个项目中多次使用多个独立的第三方中间件。
redux的中间件提供 action出发后,reducer之前 的扩展点。可以用来
- 记录日志
- 创建崩溃报告
- 调用异步接口
- 调用路由
问题:记录日志
最简单的日志
记录日志可以使用 console.log :
const action = addTodo('Use Redux')console.log('dispatching', action)store.dispatch(action)console.log('next state', store.getState())
使用 console.log 非常简单,但是非常不方便。每次调用action都需要执行打印语句。因此可以将面的 disaptch 过程封装到一个函数里面。
封装输出日志的过程到函数
将以上日志输入封装如下:
function dispatchAndLog(store, action) {console.log('dispatching', action)store.dispatch(action)console.log('next state', store.getState())}
然后用这个 dispatch 替换 store 原来的 dispatch :
// 原生redux使用store.dispatch()dispatchAndLog(store, addTodo('Use Redux'))
封装 store.dispatch 后,每次调用都需要导入函数,依然麻烦
Monkeypatching Dispatch
redux的store只是一个包含方法的普通对象,因此我们可以直接替换 sotre 中的 dispatch 方法。这样我们就不用每次都导入封装后的 dispatch 函数了
const next = store.dispatchstore.dispatch = function dispatchAndLog(action) {console.log('dispatching', action)let result = next(action)console.log('next state', store.getState())return result}
问题:崩溃报告
上面的例子中,我们只想 store.dispatch 中增加了日志功能,如果还要扩展其功能呢?
试想当发起一个 action 的结果是一个异常时,我们将包含调用堆栈,引起错误的 action 以及当前的 state 等错误信息通通发到类似于 Sentry 这样的报告服务中,不是很好吗?这样我们可以更容易地在开发环境中重现这个错误。
然而,将日志记录和崩溃报告分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建一个由这些工具组成的生态系统。(提示:我们正在慢慢了解 middleware 的本质到底是什么!)
如果将崩溃报告和日志分离,扩展结果会像下面这样:
function patchStoreToAddLogging(store) {const next = store.dispatchstore.dispatch = function dispatchAndLog(action) {console.log('dispatching', action)let result = next(action)console.log('next state', store.getState())return result}}function patchStoreToAddCrashReporting(store) {const next = store.dispatchstore.dispatch = function dispatchAndReportErrors(action) {try {return next(action)} catch (err) {console.error('捕获一个异常!', err)Raven.captureException(err, {extra: {action,state: store.getState()}})throw err}}}
如果以上功能通过不同模块发布,使用将会是下面这样:
patchStoreToAddLogging(store)patchStoreToAddCrashReporting(store)
然而在实际开发中,我们需要同时打印日志和崩溃报告,同时还要让两者分别属于两个模块。
隐藏monkyPatch
monkey本质上是一种hack,将任意的方法替换成目标方法,此时的api是什么样的呢?
在之前,我们用我们自己的函数替换掉了 store.dispatch ,如果不做替换,而是返回一个新的 dispatch 呢?(注意异步action创建函数)
function logger(store) {const 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 都会对store执行操作,可以理解为包装或者封装。每一次包装都会返回新的store。当所有middleWare都执行完成后,所有 middleWare 提供的功能都会集成到store中。
我们可以应用多个middleWare :
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
尽管我们做了很多,实现方式依旧是 monkeypatching。
因为我们仅仅是将它隐藏在我们的框架内部,并没有改变这个事实。
移除monkeyPatch
为什么我们要替换原来的 dispatch 呢?当然,这样我们就可以在后面直接调用它,但是还有另一个原因:就是每一个 middleware 都可以操作(或者直接调用)前一个 middleware 包装过的 store.dispatch:
function logger(store) {// 这里的 next 必须指向前一个 middleware 返回的函数:const next = store.dispatchreturn 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依然没有替换原始 dispatch 方法。这样下去,所有 middleware 都不会包装原市 dispatch 方法。
但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取。
// logger的目标是包装dispatch 并返回新的dispatchfunction logger(store) {// 上个例子中我们先获取dispatch,然后再包装// 这里wrap函数从参数中获取dispatch,从而代替了上面的const next = store.dispatchreturn function wrapDispatchToAddLogging(next) {return function dispatchAndLog(action) {console.log('dispatching', action)let result = next(action)console.log('next state', store.getState())return result}}}
对与以上过程,可以编写demo函数来理解:调用和输出如下:
// 定义function a(x) {console.log("a", x);return function b(y) {console.log("b", y);return function c(z) {console.log("c", z);}}}
现在是“我们该更进一步”的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些串联函数很吓人。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 中被使用。
单纯”地使用 Middleware
我们可以写一个 applyMiddleware() 方法替换掉原来的 applyMiddlewareByMonkeypatching()。在新的 applyMiddleware() 中,我们取得最终完整的被包装过的 dispatch() 函数,并返回一个 store 的副本:
// 警告:这只是一种“单纯”的实现方式!// 这 *并不是* Redux 的 API.function applyMiddleware(store, middlewares) {middlewares = middlewares.slice()middlewares.reverse()let dispatch = store.dispatchmiddlewares.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 非常有用,正如我们在之前的章节中提到的。在创建阶段调用dispatch时你需要特别注意,详见下方警告。 - 为了保证你只能应用 middleware 一次,它作用在
createStore()上而不是store本身。因此它的签名不是(store, middlewares) => store, 而是(...middlewares) => (createStore) => createStore。
由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。
由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。
警告:在创建阶段 dispatch
执行
applyMiddleware建立你的 middleware 时,store.dispatch函数会指向createStore创建的原生版本。这时进行 dispatch 会导致没有任何 middleware 被应用。如果你准备在创建阶段与另一个 middleware 交互,你恐怕要失望了。由于这个行为出乎意料,如果你尝试在创建阶段结束前 dispatch 一个 action,applyMiddleware会抛出一个错误。想要达到这个目的,你可以通过一个普通对象直接与其他 middleware 通信(例如对于一个负责 API 调用的 middleware,使用 API 客户端对象与之通信),或者使用回调函数等待 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'const todoApp = combineReducers(reducers)const store = createStore(todoApp,// applyMiddleware() 告诉 createStore() 如何处理中间件applyMiddleware(logger, crashReporter))
就是这样!现在任何被发送到 store 的 action 都会经过 logger 和 crashReporter:
// 将经过 logger 和 crashReporter 两个 middleware!store.dispatch(addTodo('Use Redux'))
注意事项
在大概看了一边官方文档中的指导后,我以为我会写了,结果在写的时候还是犯了一些细节错误。
在写中间件的过程中,我们的最终目标是扩展 dispatch 的功能,而有一些细节问题会导致扩展失败。在以上步骤中可能是替换失败等。
- 编码过程中,需要注意
return的使用。如果没有return,很可能导致失败。 - 注意最单纯使用 middleware 中
applyMiddleware方法,此方法返回新的 store, 因此在调用时必须将调用结果赋值给原来的 store 来替换原来的 store, 否则此函数相当于没有执行:let store = create(reducers);store = applyMiddleWare(store, [addLog, addCrast]);export default store;

