在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.dispatch
store.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.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) {
const 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
}
}
}
如果以上功能通过不同模块发布,使用将会是下面这样:
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.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依然没有替换原始 dispatch 方法。这样下去,所有 middleware 都不会包装原市 dispatch 方法。
但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接收一个 next()
方法,而不是通过 store 的实例去获取。
// logger的目标是包装dispatch 并返回新的dispatch
function logger(store) {
// 上个例子中我们先获取dispatch,然后再包装
// 这里wrap函数从参数中获取dispatch,从而代替了上面的const next = store.dispatch
return 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.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 非常有用,正如我们在之前的章节中提到的。在创建阶段调用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;