Redux 简单实现(三):中间件

Redux 简单实现 系列文章(三):中间件的实现。

dispatch 的改造

实现记录日志

前面我们已经简单实现了 dispatch。现在让我们试试,能不能在每次派发 action 的时候,通过 console.log 来打印出相关信息,方便我们在控制台看到 state 的每一步变更呢?事实上,这正是 redux-logger 中间件所做的事情。

dispatch 的本质就是用来触发 reducer 里的执行逻辑。如果我们想要获得 state 的变化信息,就需要在调用 dispatch 的前后插入 console.log,就像这样:

1
2
3
4
5
6
7
console.log(action + "will dispatch");
console.log(store.getState());

store.dispatch(action);

console.log(action + "already dispatched");
console.log(store.getState());

当然,我们不可能到处都去加上这样的代码。更加优雅的做法是,扩展 dispatch。大致步骤如下:

  1. 创建一个名为 addLoggingToDispatch 的函数,取代默认的 dispatch 方法。这个函数会拦截 store 中的 dispatch,并命名为 rawDispatch。它最终会被赋值给 store.dispatch,所以需要返回一个函数,并模拟 rawDispatch 的行为:

    1
    2
    3
    4
    5
    6
    7
    const addLoggingToDispatch = store => {
    const rawDispatch = store.dispatch;
    // 返回的函数就是添加更新日志之后的全新 dispatch
    return action => {
    // ...
    }
    }
  2. 在返回的函数中,我们当然需要调用真实的 rawDispatch 方法,还原它的行为,同时又进行日志记录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const addLoggingToDispatch = store => {
    const rawDispatch = store.dispatch;
    // 返回的函数就是添加更新日志之后的全新 dispatch
    return action => {
    // 按照 action 的类型进行分组
    console.group(action.type);
    // 输出更新前的 state
    console.log("previous state", store.getState());
    // 输出当前的 action
    console.log("action", action);

    // 调用默认的 dispatch 并记录返回值
    const returnValue = rawDispatch(action);

    // 输出更新后的 state
    console.log("next State", store.getState());
    // 结束分组
    console.groupEnd(action.type);

    return returnValue;
    }
    }
  3. 美化 和 兼容 当然是必不可少的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    const addLoggingToDispatch = store => {
    const rawDispatch = store.dispatch;

    if (!console.group) {
    return rawDispatch;
    }

    // 返回的函数就是添加更新日志之后的全新 dispatch
    return action => {
    // 按照 action 的类型进行分组
    console.group(action.type);
    // 使用灰色输出更新前的 state
    console.log("%c previous state", "color: grey", store.getState());
    // 使用蓝色输出当前的 action
    console.log("%c action", "color: blue", action);

    // 调用默认的 dispatch 并记录返回值
    const returnValue = rawDispatch(action);

    // 使用绿色输出更新后的 state
    console.log("%c next State", "color: green", store.getState());
    // 结束分组
    console.groupEnd(action.type);

    return returnValue;
    }
    }
  4. 最后,再加上环境判断:

    1
    2
    3
    if (process.env.NODE_ENV !== "production") {
    store.dispatch = addLoggingToDispatch(store);
    }

识别 Promise

我们有时候会遇到需要派发异步 action 的场景,如果 dispatch 能够接收一个 Promise 对象,就能处理 Redux 架构下的异步问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
const addPromiseSupportToDispatch = store => {
const rawDispatch = store.dispatch;
return action => {
if (typeof action.then === "function") {
return action.then(rawDispatch);
}
return rawDispatch(action);
}
}

if (process.env.NODE_ENV !== "production") {
store.dispatch = addPromiseSupportDispatch(store);
}

我们这里通过检查 action 否有个 then 的函数方法来判断接收到的 action 是否是一个 Promise 对象。如果是,那么等待它 resolve 的时候,接收到作为 resolve 的 action 对象,并使用原始的 dispatch 对其进行派发。如果接收的 action 并不是 Promise,那么就直接用 rawDispatch(action) 操作。

现在,既然有了 2 个增强 dispatch 的方法,我们就都来使用:

1
2
store.dispatch = addLoggingToDispatch(store);
store.dispatch = addPromiseSupportToDispatch(store);

注意,这里的顺序不能换。如果先使用了 addPromiseSupportToDispatch,那么当接收到 Promise 对象的 action 时,日志处理那边就不能正常解析了。

为了提升开发效率,在实际开发中我们可能需要经常改写 dispatch。那么 Redux 是如何协调这么多 dispatch 的改写呢?这就涉及到中间件及中间件串联的知识了。其实每一次对 dispatch 的改写,都可以被封装成一个独立的中间件。

糅合多种 dispatch

前面我们介绍了 dispatch,并提供了 2 种增强功能的例子,它们就是 redux-logger 和 redux-thunk 这两个著名中间件的雏形,同时也奠定了理解 Redux 中间件的基础。

下面汇总之前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const addLoggingToDispatch = store => {
const rawDispatch = store.dispatch;

if (!console.group) {
return rawDispatch;
}

return action => {
console.group(action.type);
console.log("%c previous state", "color: grey", store.getState());
console.log("%c action", "color: blue", action);

const returnValue = rawDispatch(action);

console.log("%c next State", "color: green", store.getState());
console.groupEnd(action.type);
return returnValue;
}
}

const addPromiseSupportToDispatch = store => {
const rawDispatch = store.dispatch;
return action => {
if (typeof action.then === "function") {
return action.then(rawDispatch);
}
return rawDispatch(action);
}
}

为了让这两种包装同时运作,我们写一个初始化 store 的函数,以丰富 store.dispatch 的功能:

1
2
3
4
5
6
7
8
9
10
11
const configureStore = () => {
const store = createStore(App);

if (process.env.NODE_ENV !== "production") {
store.dispatch = addLoggingToDispatch(store);
}

store.dispatch = addPromiseSupportToDispatch(store);

return store;
}

现在,当我们执行 configStore() 后,就获得了一个拥有增强型 dispatch 的 store。仔细研究这个函数,我们发现 addPromiseSupportToDispatch 方法返回了一个符合正常用法的 dispatch,并且支持参数是 Promise。当它内部使用 rawDispatch 进行 action 派发的时候,是最原始的那个 dispatch 吗?显然不是。因为我们已经提前使用 addLoggingToDispatch 对 store.dispatch 进行了修改。

换句话说,当执行到 addPromiseSupportToDispatch 的时候,store.dispatch 是一个已经被包装过的版本。那么我们之前的命名:rawDispatch,就显得极为不合适。那么接下来,我们就给他改名成:next

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const addLoggingToDispatch = store => {
const next = store.dispatch;

if (!console.group) {
return next;
}
return action => {
// ...
const returnValue = next(action);
// ...
return returnValue;
}
}

const addPromiseSupportToDispatch = store => {
const next = store.dispatch;
return action => {
if (typeof action.then === "function") {
return action.then(next);
}
return next(action);
}
}

这样管理太麻烦了。我们可以很自然地想到应该用数组来进行统一管理,即中间件数组。它的每一项就是一个中间件,然后统一根据中间件来增强 dispatch。

说了这么多,那么中间件到底是什么呢?其实就是上面提到的 addLoggingToDispatchaddPromiseSupportToDispatch

所以,Redux 的核心思想就是将 dispatch 增强改造的函数(中间件)先存起来,然后提供给 Redux,由它负责依次执行。这样,每一个中间件都对 dispatch 依次进行改造,并将改造后的 dispatch,即 next,向下传递,将控制权移交给下一个中间件,完成进一步的增强。具体实现就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
const configureStore = () => {
const store = cresateStore(App);
const middlewares = [];

if (process.env.NODE_ENV !== "production") {
middlewares.push(addLoggingToDispatch);
}
middlewares.push(addPromiseSupportToDispatch);

wrapDispatchWithMiddlewares(store, middlewares);

return store;
}

如何编写 wrapDispatchWithMiddlewares

1
2
3
4
5
const wrapDispatchWithMiddlewares = (store, middlewares) => {
middlewares.forEach(middleware => {
store.dispatch = middleware(store)(store.dispatch);
})
}

与此同时,我们也需要修改中间件。之前设计的是直接读取一个增强后的 dispatch,而在连接各个中间件的时候,需要返回一个返回函数的函数。这样做的意义在于:各个中间件不必再到 store 里面去读取 dispatch,而是将增强的 dispatch 作为参数进行传递和连接,进而层层递进完成控制权的转移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const promiise = store => next => action => {
if (typeof action.then === "function") {
return action.then(next);
}
return next(action);
}

const logger = store => next => action => {
if (!console.group) {
return next(action);
}
// ...
returnValue = next(action);
// ...
return returnValue;
}

Redux 源码探索——中间件的秘密

源码剖析

Redux 本身暴露了一个 applyMiddleware 的接口,我们只需要引入:

1
import { applyMiddleware } from "redux";

同时,对于上面的 2 个例子,有现成的库可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import promise from "redux-promise";
import createLogger from "redux-logger";

const configureStore = () => {
const middlewares = [];

if (process.env.NODE_ENV !== "production") {
middlewares.push(createLogger());
}
middlewares.push(promise);

return createStore(
reducer,
applyMiddleware(...middlewares)
)
}

applyMiddleware 返回的内容我们称之为 enhancer。在 Redux 源码中,涉及中间件的脚本有 applyMiddleware.jscreateStore.jscompose.js。那么在 createStore 中,applyMiddleware(...middlewares) 会发生什么事呢?我们找到 createStore.js 的源码部分:

1
2
3
4
5
6
7
8
9
10
11
export default function createStore(reducer, preloadedState, enhancer) {
// ...
if (typeof enhancer !== "undefined") {
if (typeof enhancer !== "function") {
throw new Error("...");
}

return enhancer(createStore)(reducer, preloadedState);
}
// ...
}

我们再来看看 appliMiddleware 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch = store.dispatch
let chain = []

const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

这几行中代码中,应用了大量的函数式编程思想,如 高阶函数、函数组合、柯里化等。下面我们进行拆解。

export default function applyMiddleware(...middlewares)

这里使用了扩展运算符,使得 applyMiddleware 可以接收任意个数的中间件。接下来,它会返回一个函数:

return (createStore) => (reducer, preloadedState) => {...}

对应于 createStore.js 里的代码,它作为一个三级柯里化的函数,相当于:

applyMiddleware(...middlewares)(createStore)(reducer, initialState)

这里借用了原始的 createStore 方法,创建了一个新的增强版 store。

1
2
3
const store = createStore(reducer, preloadedState)
let dispatch = store.dispatch
let chain = []

这里记录了原始的 store 和 dispatch 方法,并准备了一个 chain 数组。

1
2
3
4
5
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))

middlewareAPI 是提供给第三方中间件它们需要使用的参数,其中包括了原始的 store.getStatedispatch 方法,至于用不用是看它们自己的需求。

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

最后,通过 compose 方法把各个中间件串联起来。它是怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
export default 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)))
}

事实上,最后的结果就像这个样子:

middlewareA(middlewareB(middlewareC(store.dispatch)))

A -> B -> C -> 最原始的 dispatch

写一个中间件的套路

我们已经了解了中间件的工作原理,它的编写是有固定模式的:

1
2
3
const customMiddleware = store => next => action => {
// ...
}

我们设想一个场景:应用存在多套主题皮肤可供用户切换选择。这些皮肤在一定时间内往往都是有固定样式的,在初始化整个应用的时候,使用一套默认的主题皮肤。在用户切换主题的情况下,我们希望用户离开应用后,下次再访问时,仍然可以直接切入上一次切换后的主题,而不是默认主题。

切换一套主题的 action 如下:

1
2
3
4
store.dispatch({
type: "CHANGE_THEME",
payload: "light"
});

那么,我们可以定义一个 CHANGE_THEME 的中间件:

1
2
3
4
5
6
7
8
9
10
const CHANGE_THEME = store => next => action => {
// 拦截目标 action
if (action.type === "CHANGE_THEME") {
if (localStorage.get("theme") !== action.payload) {
localStorage.setItem("theme", action.payload)
}
}

return next(action);
}

所以每次当用户切换主题的时候,我们通过中间件拦截到了 主题信息,并将其存储到了 localStorage 中。下次访问,我们只需要从 localStorage 里取就可以了。

相关链接

Redux 简单实现(一):状态管理器
Redux 简单实现(二):多文件协作
Redux 简单实现(四):react-redux

参考资料:

完全理解 redux(从零实现一个 redux)

React 状态管理与同构实战