Skip to content

Redux 中间件 (Middleware)

1. 前置概念:函数式编程思想

在深入中间件之前,需要理解两个函数式编程的核心概念:函数组合和柯里化。

1.1 函数组合 (Compose)

当需要依次执行多个函数,且上一个函数的返回值是下一个函数的参数时,可以使用函数组合来优化代码结构,提高可读性。

场景: 假设有三个函数 f1, f2, f3,需要将参数 'omg' 依次传入执行。

不优雅的实现 (洋葱模型):

javascript
// 可读性差,难以维护
const result = f1(f2(f3('omg')));

优雅的实现 (使用 compose):

javascript
// 可读性好,易于扩展
const composedFn = compose(f1, f2, f3);
const result = composedFn('omg');

compose(f1, f2, f3) 会返回一个新函数,执行顺序是从右到左 (f3 -> f2 -> f1)。

1.2 函数柯里化 (Currying / 颗粒化)

柯里化是将一个接收多个参数的函数,转变为一系列只接收一个参数的函数的过程。这样做可以使得函数参数单一,便于复用和组合。

普通函数:

javascript
function add(a, b, c) {
    return a + b + c;
}
add(1, 2, 3);

柯里化后的函数:

javascript
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}
curriedAdd(1)(2)(3);

Redux 中间件的 API 设计就大量运用了柯里化的思想。


2. 中间件 (Middleware) 是什么?

2.1 核心作用

Redux 的 dispatch 方法本身只能处理纯对象(Plain Object)形式的 Action。对于异步操作(如 API 请求),直接 dispatch 一个函数会报错。

中间件就像一个“中介”,它位于 dispatch(action)reducer 之间,用于拦截 Action,执行副作用(如异步请求、日志记录等),然后再决定是放行 Action 到下一个中间件/Reducer,还是分发一个新的 Action。

Middleware Diagram

2.2 applyMiddleware API

applyMiddleware 是 Redux 提供的官方 API,用于将一个或多个中间件应用到 createStore 上。

常用中间件:

  • redux-thunk: 允许 dispatch 函数,用于处理简单的异步逻辑。
  • redux-logger: 在控制台打印 Action 信息及前后的 State 状态,便于调试。

使用示例:

javascript
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from './reducers';

// 将中间件作为参数传入 applyMiddleware
const store = createStore(
  rootReducer,
  applyMiddleware(thunk, logger)
);

2.3 中间件的执行顺序

applyMiddleware 会将所有中间件组合成一个链式调用的结构。Action 会依次通过这个链。

重要规则: redux-logger 通常应该放在中间件列表的最后一个

  • 原因: logger 需要记录最终到达 Reducer 的 Action 以及由该 Action 引起的 State 变化。如果放在前面,它可能记录到被其他中间件(如 thunk)处理前的原始 Action(例如一个函数),而不是最终的 Plain Object Action,导致日志信息不准确。

3. 从零实现 Redux 中间件相关 API

3.1 实现 compose 函数

javascript
function compose(...funcs) {
  // 边缘情况:没有函数传入
  if (funcs.length === 0) {
    return (arg) => arg;
  }
  // 边缘情况:只有一个函数
  if (funcs.length === 1) {
    return funcs[0];
  }

  // 使用 reduce 将函数串联起来
  // a 是上一次聚合的结果(一个函数),b 是当前遍历到的函数
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

3.2 实现 applyMiddleware 函数

applyMiddleware 是一个高阶函数,它返回一个 "enhancer"(增强器),这个增强器再接收 createStore 来生成最终的 store。

javascript
import { compose } from './compose';

export function applyMiddleware(...middlewares) {
  // 返回一个 enhancer
  return (createStore) => (reducer) => {
    // 1. 先按照原始方式创建 store
    const store = createStore(reducer);
    // 2. 保存原始的 dispatch 方法
    let dispatch = store.dispatch;

    // 3. 定义要传递给中间件的 API
    const middlewareAPI = {
      getState: store.getState,
      // 这里的 dispatch 必须用箭头函数包装,确保中间件在调用 dispatch 时,
      // 使用的是被增强后的最新 dispatch,而不是创建 store 时的原始 dispatch。
      dispatch: (...args) => dispatch(...args)
    };

    // 4. 将 middlewareAPI 注入到每个中间件中,得到一个函数链
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    
    // 5. 使用 compose 将所有中间件组合起来,并包装原始的 dispatch
    //    最终得到一个被层层增强的 dispatch 方法
    dispatch = compose(...chain)(store.dispatch);

    // 6. 返回新的 store,其中 dispatch 方法已经被替换为增强版
    return {
      ...store,
      dispatch
    };
  };
}

3.3 实现 logger 中间件

所有中间件都遵循 ({ getState, dispatch }) => next => action => { ... } 的柯里化结构。

javascript
const logger = ({ getState }) => next => action => {
  console.log('--------------------');
  console.log('will dispatch', action.type);

  // 1. 打印更新前的 state
  const prevState = getState();
  console.log('prevState', prevState);

  // 2. 调用 next,将 action 传递给下一个中间件或 reducer
  //    next(action) 的返回值非常重要,需要 return 出去
  const returnValue = next(action);

  // 3. 打印更新后的 state
  const nextState = getState();
  console.log('nextState', nextState);
  console.log('--------------------');
  
  return returnValue;
};

3.4 实现 thunk 中间件

thunk 的核心逻辑是判断 Action 是不是一个函数。

javascript
const thunk = ({ dispatch, getState }) => next => action => {
  // 如果 action 是一个函数
  if (typeof action === 'function') {
    // 执行该函数,并将 dispatch 和 getState 作为参数传入
    // 这样在异步操作完成后,就可以在函数内部 dispatch 一个新的 action
    return action(dispatch, getState);
  }

  // 如果 action 不是函数,则直接传递给下一个中间件
  return next(action);
};

4. 其他中间件与进阶

4.1 Promise 中间件

thunk 类似,Promise 中间件可以用来处理 Action 是一个 Promise 的情况。

核心逻辑:

  1. 检查 Action 或 Action.payload 是否为 Promise 对象。
  2. 如果是,则在 .then() 中处理成功和失败的情况,并 dispatch 相应的 Action。
  3. 如果不是,则调用 next(action) 放行。

4.2 Flux Standard Action (FSA)

FSA 是一个社区约定的 Action 对象格式标准,旨在使 Action 更具可读性和一致性。一个标准的 FSA Action 应该:

  • 是一个纯 JavaScript 对象。
  • 必须有一个 type 属性。
  • 可以有 payloaderrormeta 属性。

一些成熟的 Promise 中间件会检查 Action 是否符合 FSA 规范。

4.3 Redux Saga 简介

Redux Saga 是另一个用于处理副作用(尤其是复杂异步流)的中间件。与 thunk 不同,它使用 ES6 的 Generator 函数来让异步代码看起来像同步一样,提供了更强大的控制能力,如任务的取消、并发处理等,但学习曲线也更陡峭。