redux(三)- 中间件

本文探讨下基于洋葱模型的中间件机制,及 redux applyMiddleware 的实现原理。

引言#

当我们需要在某个函数执行前后搞些事情时,通常是将其再次封装成新的函数,如:

function next(action) {
console.log(action);
}
// 封装
function nextWraper(action) {
console.log('nextWrapper 开始');
next(action);
console.log('nextWrapper 结束');
}

这种方式的优点显而易见,即代码结构清晰,易于维护及扩展。但其缺点也较明显,代码可复用性较低。譬如当需要在另一个函数执行前后同样记录“开始”、“结束”时,就需要再封装出一个相同的函数,如:

function fooWrapper(param) {
console.log('fooWrapper 开始');
foo(param);
console.log('fooWrapper 结束');
}

还有一种场景,若需要在函数 fooWrapper 执行前/后再执行其他操作,则需要修改 fooWrapper 或 再次嵌套 fooWrapper

那么,有没有一种方式可以将其抽象并可以根据需要指定相应的wrapper呢?答案就是 compose,一个基于洋葱模型的中间件实现方式。

洋葱模型#

洋葱模型

redux 中有个compose 函数,如下所示:

function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

该函数的主要作用就是遍历所有的中间件函数 funcs后,将其聚合并返回一个聚合函数

tip

注意:执行 compose 后最终返回的是一个函数,只有运行其返回的函数才能让传入 compose 的函数依次执行,即 compose(fn1, fn2, fn3)()。其中,传入的中间件函数的执行顺序是从右到左依次执行的,前一个函数的执行结果作为后一个函数的参数,因此,中间件函数的入参需与返回值的类型保持一致。

接下来,举个🌰详细说明基于洋葱模型的中间件机制。代码如下:

const fn1 = next => {
console.log('【中间件1】');
return action => {
console.log('【函数1】 开始');
const res = next(action);
console.log('【函数1】 结束');
return res;
};
};
const fn2 = next => {
console.log('【中间件2】');
return action => {
console.log('【函数2】 开始');
const res = next(action);
console.log('【函数2】 结束');
return res;
};
};
const fn3 = next => {
console.log('【中间件3】');
return action => {
console.log('【函数3】 开始');
const res = next(action);
console.log('【函数3】 结束');
return res;
};
};
const dispatch = action => {
console.log('【执行 dispatch】: ', action);
};
// 步骤1:使用 compose 组合中间件 fn1、fn2,并返回组合函数 fn
const fn = compose(fn1, fn2, fn3);
// 步骤2:执行组合函数,并传入需要被增强的函数 dispatch
const _dispatch = fn(dispatch);
// 步骤3:执行增强后的函数
_dispatch('我是入参');
  • 步骤1:结合上文中 compose 实现方式,分析其执行过程(可参考文档 MDN | Array.prototype.reduce 了解 reduce 函数),因为传入 fn1fn2fn3 三个参数,因此 reduce 函数会经历两轮循环,如下所示:

    循环a值b值返回值
    第一轮循环fn1fn2(...args) => fn1(fn2(...args))
    第二轮循环(...args) => fn1(fn2(...args))fn3(...args) => fn1(fn2(fn3(...args)))

    故经 compose 处理之后,最终返回的值是 (...args) => fn1(fn2(fn3(...args))) ,即:

    fn = (...args) => fn1(fn2(fn3(...args)))
  • 步骤2:给组合函数 fn 传入需要被增强的函数 dispatch,依次执行函数 fn3fn2fn1,此时打印出:

    【中间件3
    【中间件2
    【中间件1

    执行完毕,返回增强版 _dispatch 为:

    _dispatch = action => {
    console.log('【函数1】 开始');
    const res = next(action); // 注意此时的 next 其实就是 fn2
    console.log('【函数1】 结束');
    return res;
    }

    其中,next 为:

    action => {
    console.log('【函数2】 开始');
    const res = next(action); // 注意此时的 next 其实就是 fn3
    console.log('【函数2】 结束');
    return res;
    }

    其中,next 为:

    action => {
    console.log('【函数3】 开始');
    const res = next(action); // 注意此时的 next 其实就是 dispatch
    console.log('【函数3】 结束');
    return res;
    }

    其中,next 为:

    action => {
    console.log('【执行 dispatch】: ', action);
    };
  • 步骤3:给增强后的_dispatch 函数传入参数并执行,其结果如下:

    【函数1】 开始
    【函数2】 开始
    【函数3】 开始
    【执行 dispatch】: 我是入参
    【函数3】 结束
    【函数2】 结束
    【函数1】 结束

redux middleware#

redux 是个 javascript 状态管理工具,其提供了函数 applyMiddleware,以利用中间件机制实现定制化的增强 dispatch 函数。

middleware 功能#

redux middleware 可以在 Action 被指派到 Reducer 前进行额外的处理。

2020-12-29/中间件

middleware 格式#

首先,middleware 是在 actiondispatch 前/后对其进行处理,因此,middleware 是一个入参为 action 的函数:

function middleware(action) {}

其次,为了能够串联多个 middlewaremiddleware 应该接收一个 dispatch 并返回一个执行 dispatch 的函数:

function middlewareWrapper(dispatch) {
return function(action) {
dispatch(action);
}
}

最后,为了取得当前的 state 或者重新 dispatch 一个 actionmiddleware 需要传入参数 store,因此需将其再包裹一层:

function storeWrapper(store) {
function middlewareWrapper(dispatch) {
return function(action) {
dispatch(action);
}
}
}

用 ES6 箭头函数改写,并将函数及参数重命名后,得:

const middleware = store => next => action => {
next(action)
}

举个例子,当我们需要实现一个用于记录 action 及其产生的新的 state 的中间件,则可以写为:

const middleware = ({ dispatch, getState }) => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', getState())
return result
}

redux applyMiddleware#

applyMiddleware 是一个 store enhancer,用来包装 redux store,让 redux store 拥有 middleware 的功能。其源码如下:

function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}

从源码中可以看出,applyMiddleware 的主要流程为3步:

  1. 通过 createStore 创建一个新的 store;
  2. 将所有的 middleware 函数串联在一起,并在最后串联上 store.dispatch
  3. redux storedispatch 函数改成 middleware chain

第一步#

回顾 createStore 函数,其接收参数 enhancer 函数作为增强器,即:

function createStore(reducer, preloadedState, enhancer) {
...
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
...
}
// 使用
const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware1));

增强器的作用是用于改造 Store 的相关 API,因此需要传入 createStore 以初始化 Store。所以, applyMiddleware 的第一步即为接收 createStore 函数,并初始化 Store。

function applyMiddleware(...middlewares) {
return createStore => (...args) => {
...
}
}

第二步#

为了避免 middleware 在建立过程中调用 store.dispatch 导致串联中间件出错,applyMiddleware 会通过 mapgetState 和改造后 dispatch 传递给每一个中间件。

let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI));

此时,chain 是一组形如 next => action => {...} 的函数数组,即中间件数组:

2020-12-29/chain1

使用 compose 将每个 middleware 中的 next 都指向下一个 middleware

2020-12-29/chain2

最后,将 store.dispatch 传入经 compose 处理后的函数,即可完成整个 middleware chain 的建立。

2020-12-29/chain3

note

问:middlewareAPI 中定义的 dispatch 为什么要抛出异常?

答:applyMiddleware 的目的是将所有的中间件组装成一个超级 dispatch 并让其替换原始 dispatch,因此,需要避免在组装过程中使用 dispatch。需要注意的是,此处是通过 let 进行定义的 dispatch,故可在组装完成后修改其引用地址,保证需要使用 dispatch 时使用的是组装后的超级 dispatch

note

问:middlewareAPI 中的 dispatch 为什么要用匿名函数包裹呢?

答:我们用 applyMiddleware 是为了改造 dispatch 的,所以 applyMiddleware 执行完后,dispatch 是变化了的,而 middlewareAPI 是 applyMiddleware 执行中分发到各个 middleware,所以必须用匿名函数包裹 dispatch, 这样只要 dispatch 更新了, middlewareAPI 中的 dispatch 应用也会发生变化。

第三步#

applyMiddleware 的最后一步就是将修改(一系列 middleware 包装)后的 dispatch 传到 redux 中,替换原有的 dispatch

dispatch = compose(...chain)(store.dispatch)

redux-thunk#

核心思想#

Redux-thunk 代码量极少,只有短短十几行,如下:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// 核心代码 开始
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 核心代码 结束
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

  其核心代码就两行,即判断每个经过它的action:如果是function类型,就调用这个function(并传入 dispatch 和 getState 及 extraArgument 为参数),而不是任由让它到达 reducer,因为 reducer 是个纯函数,Redux 规定到达 reducer 的 action 必须是一个 plain object 类型。

note

当 action 是个函数时,则会执行这个函数并返回,此处并没有通过 next 将中间件执行至下一个,而是在函数 action 内调用 dispatch。那么,在 middleware 中调用 dispatch 会发生什么?

  middleware 中 拿到的 dispatch 和最终 compose 结束后的新 dispatch 是保持一致的,所以在middleware 中调用 store.dispatch() 和在其他任何地方调用效果是一样的,而在 middleware 中调用 next(),效果是进入下一个 middleware。

  正常情况下,如图左,当我们 dispatch 一个 action 时,middleware 通过 next(action) 一层一层处理和传递 action 直到 redux 原生的 dispatch。如果某个 middleware 使用 store.dispatch(action) 来分发 action,就发生了右图的情况,相当于从外层重新来一遍。

  需要注意的是,若 middleware 一直调用 store.dispatch(action),则会形成无限循环。

使用方法#

举个例子,需要发送一个异步请求到服务器获取数据,成功后弹出一个自定义的 Message。

  • 定义全局的 store,并使用 redux-thunk 等中间件。
const store = createStore(
reducer,
compose(applyMiddleware(thunk, promise)),
);
  • 异步 action
const getThenShow = (dispatch, getState) => {
const url = 'http://xxx.json';
fetch(url)
.then(response => {
dispatch({
type: 'SHOW_MESSAGE_FOR_ME',
message: response.json(),
});
}, e => {
dispatch({
type: 'FETCH_DATA_FAIL',
message: e,
});
});
};

  此时,只要在业务代码里面调用 store.dispatch(getThenShow)redux-thunk 就会拦截并执行 getThenShow 这个 action,getThenShow 会先请求数据,如果成功,dispatch 一个显示 Message 的 action,否则 dispatch 一个请求失败的 action。这里的 dispatch 就是通过 redux-thunk middleware 传递进来的。

参考#