Middleware
中间件这个词已经不算陌生了,因为在使用 Express 搭建后台服务时,就写过相关功能的中间件:用于后台系统相关操作的权限验证,无权限则将直接返回 403 及相关提示信息
它的一个优秀特性就是可以 链式组合
(如 Express 的 next()
方法可以将本中间件处理的结果交给下个中间件进行后续处理)
Redux Middleware
Redux 的 middleware 就是用于解决不同的问题的,与上面 Express 中间件的概念是类似的,但它提供的是 action 发起之后,到达 reducer 之前的功能扩展
。因而我们可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等
它类似于插件,可以在不影响原本功能,并且不改动原本代码的基础上,对其功能进行增强。在 Redux 中,中间件主要用于增强 dispatch 函数。实现 Redux 中间件的 基本原理
就是更改 store 的 dispatch 函数
Redux 中间件书写
中间件本身是一个函数,该函数接受一个 store 参数,表示创建的仓库,该仓库并非一个完整的仓库对象,仅仅包含 getState
,dispatch
(这是由于 applyMiddleware 函数只会将此对象的这两个属性传递进来)。该函数的运行时间是:在创建仓库之后
// redux: createStore.js
export default function createStore(
reducer,
preloadedState,
enhancer
) {
/* source code */
}
如上,createStore 方法可以接受三个参数:reducer
,[preloadedState]
,enhancer
reducer (Function)
:接受两个参数 (state, action),返回新的 state[preloadedState] (any)
:作为创建 store 时的初始状态值 (可不传递)enhancer (Function)
:它是一个组合 store creator 的函数,返回一个新的经过强化的 store creator,它与 middleware 相似,允许通过复合函数来改变 store 接口
第二个参数传递函数,不传递第三个参数时,会将此函数作为第三个参数使用 (内部会校验传递的函数是否符合它的要求),并将仓库的默认状态值置为 undefined
// 普通用法
import {createStore} from 'redux'
const initialState = {}
// 第二个参数传递非函数,则作为 store 的初始化状态
const store = createStore(reducer, initialState)
console.log(store.getState()) // 打印:{}
由于创建仓库后需要自动运行设置的中间件函数,因而我们需要在创建仓库时就告诉它该运行的中间件,即通过调用 Redux
提供的 applyMiddleware
函数,并将此函数的返回值传递给 createStore
函数的 第二个参数 (只传两个参数时)
或 第三个参数 (第二个参数为状态初始值)
:
import {createStore, applyMiddleware} from 'redux'
import reducer from './reducer'
/* applyMiddleware 需要的中间件函数写法 */
function logger(store) {
/* 分发 action 的日志记录 */
// next() 是去触发下一个中间件的 dispatch 函数 (洋葱模型)
return function(next) {
// 返回最终要应用的 dispatch 函数
return function(action) {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
// ⚠️注意:如果在这里调用 store.dispatch 分发 action
// 由于链式调用的关系,会无限递归导致浏览器卡死
next(action)
console.log('之后的状态:', store.getState())
}
}
}
// 因而,logger 可以使用 箭头函数进行简写:
const logger = store => next => action => {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
next(action)
console.log('之后的状态:', store.getState())
}
function catchError(store) {
/* 错误捕获 */
}
// 应用中间件,方式 1:
// 多个中间件依次传入此函数即可
const store = createStore(reducer, applyMiddleware(logger, catchError))
applyMiddleware
函数:用于记录需要应用哪些中间件,它返回一个 函数 a
,函数 a
用于记录创建仓库的方法,给 函数 a
传入创建仓库的函数 createStore
会返回 函数 b
,函数 b
接受 reducer
创建返回仓库对象 store
:
import {createStore, applyMiddleware} from 'redux'
import reducer from './reducer'
// 应用中间件,方式 2 (一般不会这么写):
const storeCreator = applyMiddleware(logger, catchError)
const store = storeCreator(createStore)(reducer)
栗子:日志记录与错误捕获
当我们想要在每次分发 action 改变数据时,均希望打印日志记录 之前的状态
、分发的 action
、之后的状态
。当开发阶段出现数据异常,我们就可以通过日志记录查看是哪个 action 导致的状态异常
1. 手动添加
从需求点出发,我们最直接的解决方案就是在 store.dispatch(action)
前后手动添加打印日志的代码 (为了这个点去实现,我们还需要去每个 store.dispatch(action)
前后均去加相同的重复代码 <然而它还算不上是一个解决方案>)
2. 封装 dispatch 函数
基于上面一点的思考,我们可以考虑去封装 dispatch 函数
,使它能够做到打印前后状态的功能,并能分发 action (它已经可以实现需求点的功能分离,但我们还需要在每次使用时进行导入)
function loggerDispatch(store, action) {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
store.dispatch(action)
console.log('之后的状态:', store.getState())
}
// 后续在分发 action 时,替换 store.dispatch(action) 方法,如:
// store.dispatch(createAddUserAction({id: 1008, name: 'K.'}))
loggerDispatch(store, createAddUserAction({id: 1008, name: 'K.'}))
3. 覆盖 store.dispatch 函数
通过上一篇文章:《手写实现 createStore ...》,我们知道通过调用 createStore 返回的仅是一个普通对象,而且能在其 dispatch 函数中能够拿到之前的状态,分发的 action 以及更新后的状态,而 dispatch
仅仅是一个分发 action 的函数,那么我们就可以通过重写 store.dispatch
函数来达到打印日志的功能:
// 存储分发 action 功能的 dispatch 函数
let next = store.dispatch
store.dispatch = function loggerDispatch(store, action) {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
const result = next(action)
console.log('之后的状态:', store.getState())
return result
}
next = store.dispatch
store.dispatch = function catchErrorDispatch(store, action) {
try {
return next(action)
} catch(error) {
console.log('catch an error: ', error)
window.alert(error.message)
throw error
}
}
// 那么我们后续再调用 store.dispatch 方法时
// 在任何地方都会打印日志且能捕获错误了
至此,我们已经得到了想要的结果
4. 抽离覆盖逻辑
当我们想要在分发 action 时附加不同的功能,那么我们就得根据需要去封装 覆盖
store.dispatch
方法去扩展 dispatch 函数的功能 (比如:日志打印
与 错误捕获
),我们将它们分为不同的模块导出,在需要使用某个功能时对 store 的 dispatch 方法进行覆盖 (函数用法,那自然也可以将它们顺序调用去重复覆盖从而组合所有的附加功能)
// addDispatchLogger.js
export default function addDispatchLogger(store) {
const next = store.dispatch
store.dispatch = function loggerDispatch(action) {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
const result = next(action)
console.log('之后的状态:', store.getState())
return result
}
}
// catchDispatchError.js
export default function addCatchDispatchError(store) {
const next = store.dispatch
store.dispatch = function catchErrorDispatch(action) {
try {
return next(action)
} catch(error) {
console.log('catch an error: ', error)
window.alert(error.message)
throw error
}
}
}
// 依次调用则可以将多个功能进行组合:
addDispatchLogger(store)
addCatchDispatchError(store)
5. 改动 store 对象覆盖 ➡️ 返回新函数覆盖
上面的思想是去 覆盖 store.dispatch
函数,但我们考虑不去直接改动它,而是返回一个新的函数,再通过一个组合功能的辅助函数去组合附加的功能:
// addDispatchLogger.js
export default function addDispatchLogger(store) {
const next = store.dispatch
// 之前的写法:
// store.dispatch = function loggerDispatch(action) {
return function loggerDispatch(action) {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
const result = next(action)
console.log('之后的状态:', store.getState())
return result
}
}
// catchDispatchError.js
export default function addCatchDispatchError(store) {
const next = store.dispatch
return function catchErrorDispatch(action) {
try {
return next(action)
} catch(error) {
console.log('catch an error: ', error)
window.alert(error.message)
throw error
}
}
}
// 辅助方法 applyFeatures.js
export default function applyFeatures(store, features) {
features = features.slice()
features.forEach(feature => {
store.dispatch = feature(store)
})
}
// 组合使用:
applyFeatures(store, [addDispatchLogger, addCatchDispatchError])
6. 柯里化
上面的内容均是将中间件串联起来,依次去覆盖 store.dispatch
方法,这样做的目的是我们可以在后面直接通过 store.dispatch
调用每个中间件,而且每个中间件都可以操作前一个中间件封装后的 store.dispatch
方法 (那么,若初始应用中间件或某一环覆盖 store.dispatch
方法失败,那么就可能导致之后的中间件获取到的 dispatch 方法
不是预期的结果)
本文开头描述 Express 中间件时,每个中间件函数都会接受三个参数:request
、reponse
和 next
,Express 中间件函数的 next 方法是决定是否交给下一个中间件处理
这里我们也引入 next 方法,但它是新的 dispatch 函数,用于去触发下一个中间件的 dispatch 函数,为使 redux 的中间件实现链式调用的效果 (并不会覆盖 store 本身的 dispatch 方法):
// addDispatchLogger.js
export default function addDispatchLogger(store) {
return function withLoggerDispatch(next) {
return function loggerDispatch(action) {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
const result = next(action)
console.log('之后的状态:', store.getState())
return result
}
}
}
// 使用 ES6 箭头函数 (柯里化) (看起来就有点吓人了):
const addDispatchLogger = store => next => action => {
console.log('之前的状态:', store.getState())
console.log('分发的 Action:', action)
const result = next(action)
console.log('之后的状态:', store.getState())
return result
}
这就是 Redux middleware
的样子,middleware 接受一个 next()
作为 dispatch 函数,并返回一个新的 dispatch 函数,返回的这个函数又会作为下一个 middleware 的 next()
,以此类推......
7. 实现 applyMiddleware
由上,我们可以写一个 applyMiddleware
方法替换掉之前的 applyFeatures
方法,在这个方法中,我们取得新的 dispatch 函数并进行链式传递,且返回 store 的副本:
function applyMiddleware(store, middlewares) {
// 洋葱模型链式调用需要逆序
middlewares = [...middlewares].reverse()
let dispatch = store.dispatch
middlewares.forEach(middlware => {
// middleware 函数柯里化可见第 6 点
dispatch = middleware(store)(dispatch)
})
// return Object.assign({}, store, {dispatch})
return {
...store,
dispatch
}
}
当然,上述方法只是简单地实现中间件的应用,与 Redux 本身的实现方案是不同的,部分取自 【Redux 自述 —— Middleware】
更进一步实现 applyMiddleware
由于上文 Redux 中间件书写 中写到 applyMiddleware
函数的第二种使用方式:
const store = applyMiddleware(...middlewares)(createStore)(reducer)
接下来,让我们把它完善一下:
/**
* @param {any} middlewares 需要应用的所有中间件
*/
export default function applyMiddleware(...middlewares) {
/**
* @param {Fcuntion} createStore 仓库创建函数
*/
return function (createStore) {
/**
* @param {Fcuntion} reducer
* @param {any | undefined} preloadedState 仓库初始值
*/
return function (reducer, preloadedState) {
const store = createStore(reducer, preloadedState)
const {getState, dispatch: storeDispatch} = store
const simpleStore = {
getState,
/* 更新 dispatch */
dispatch: (...args) => dispatch(...args)
}
// 初次调用,则报错处理
let dispatch = () => {
throw new Error('现在还不能使用 dispatch 函数')
}
const dispatchCreators = middlewares.map(middleware => middleware(simpleStore))
// 给 dispatch 赋值
dispatch = compose(...dispatchCreators)(storeDispatch)
return {
...store,
dispatch
}
}
}
}
/**
* 组合多个函数,返回一个新的函数
* @param {Function[]} funcs
*/
function compose(...funcs) {
const len = funcs.length
if (len === 0) {
return args => args
} else if (len === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
/*
组合多个函数,将后一个函数返回的结果传给上一个函数进行调用
return function (...args) {
let result = null
for (let i = len - 1; i >= 0; i --) {
const func = funcs[i]
if (i === len - 1) {
result = func(...args)
} else {
result = func(result)
}
}
return result
}
*/
}
至此,中间件部分的基本内容就结束了 (那么上篇手写 createStore 方法要兼容 applyMiddleware 方法则需要进行修改)