理解 Redux 的中间件

将该思想抽象出来,其实和 Redux 就无关了。问题变成,怎样实现在截获函数的执行,以在其执行前后添加自己的逻辑。

为了演示,我们准备如下的示例代码来模拟 Redux dispatch action 的场景:

const store = {
  dispatch: action => {
    console.log("dispating action:", action);
  }
};

store.dispatch({ type: "FOO" });
store.dispatch({ type: "BAR" });

我们最终需要实现的效果是 Redux 中 applyMiddleware(...middlewares) 的效果,接收一个中间件数据(函数数组),执行真正的 dispatch 前顺次执行这些中间件。

以打日志为例,我们想在调用 dispatch 时进行日志输出。

尝试1 - 手动

直接的做法就是手动进行。

console.log("before dispatch `FOO`");
store.dispatch({ type: "FOO" });
console.log("before dispatch `FOO`");

console.log("before dispatch `BAR`");
store.dispatch({ type: "BAR" });
console.log("before dispatch `BAR`");

但其实这并不算一个系统的解决方案,至少需要摆脱手动这种方式。

尝试2 - 包装

既然所有 dispatch 操作都会打日志,完全有理由抽取一个方法,将 dispatch 进行包装,在这个方法里来做这些事情。

function dispatchWithLog(action) {
  console.log(`before dispatch ${action.type}`);
  store.dispatch(action);
  console.log(`after dispatch ${action.type}`);
}

但调用的地方也得变,不能直接使用原始的 store.disatch 而需要使用封装后的 dispatchWithLog

- store.dispatch({ type: "FOO" });
- store.dispatch({ type: "BAR" });
+ dispatchWithLog({ type: "FOO" });
+ dispatchWithLog({ type: "BAR" });

尝试3 - 替换实现/Monkeypatching

如果我们直接替换掉原始函数的实现,便可以做到调用的地方不受影响而实现新增的 log 功能,虽然修改别人提供的方法容易引起 bug 且不太科学。

const original = store.dispatch;
store.dispatch = function log(action) {
  console.log(`before dispatch ${action.type}`);
  original(action);
  console.log(`after dispatch ${action.type}`);
};

store.dispatch({ type: "FOO" });
store.dispatch({ type: "BAR" });

尝试4 - 多个函数的截获

除了添加 log,如果还想对每次 dispatch 进行错误监控,只需要拿到前面已经替换过实现的 dispatch 方法再次进行替换包装即可。

const original = store.dispatch;
store.dispatch = function log(action) {
  console.log(`before dispatch ${action.type}`);
  original(action);
  console.log(`after dispatch ${action.type}`);
};

const next = store.dispatch;
store.dispatch = function report(action) {
  console.log("report middleware");
  try {
    next(action);
  } catch (error) {
    console.log(`error while dispatching ${action.type}`);
  }
};

所以针对单个功能的中间件,我们可以提取出其大概的样子来了:

function middleware(store) {
  const next = store.dispatch;
  store.dispatch = function(action) {
    // 中间件中其他逻辑
    next(action);
    // 中间件中其他逻辑
  };
}

改写日志和错误监控为如下:

function log(store) {
  const next = store.dispatch;
  store.dispatch = function(action) {
    console.log(`before dispatch ${action.type}`);
    next(action);
    console.log(`after dispatch ${action.type}`);
  };
}

function report(store) {
  const next = store.dispatch;
  store.dispatch = function(action) {
    console.log("report middleware");
    try {
      next(action);
    } catch (error) {
      console.log(`error while dispatching ${action.type}`);
    }
  };
}

然后按需要应用上述中间件即可:

log(store);
report(store);

上面中间件的调用可专门编写一个方法来做:

function applyMiddlewares(store, middlewares) {
  middlewares.forEach(middleware => middleware(store));
}

隐藏 Monkeypatching

真实场景下,各中间件由三方编写,如果每个中间件都直接去篡改 store.dispatch 不太科学也不安全。如此的话,中间件只需要关注新添加的逻辑,将新的 dispatch 返回即可,由框架层面拿到这些中间件后逐个调用并重写原来的 dispatch,将篡改的操作收敛。

所以中间件的模式更新成如下:


function middleware(store) {
  const next = store.dispatch;
-  store.dispatch = function(action) {
+  return function(action) {
    // 中间件中其他逻辑
    next(action);
    // 中间件中其他逻辑
  };
}

改写 logreport 中间件:

function log(store) {
  const next = store.dispatch;
-  store.dispatch = function(action) {
+  return function(action) {
    console.log(`before dispatch ${action.type}`);
    next(action);
    console.log(`after dispatch ${action.type}`);
  };
}

function report(store) {
  const next = store.dispatch;
-  store.dispatch = function(action) {
+  return function(action) {
    console.log("report middleware");
    try {
      next(action);
    } catch (error) {
      console.log(`error while dispatching ${action.type}`);
    }
  };
}

更新 applyMiddlewares 方法:

function applyMiddlewares(store, middlewares) {
  middlewares.forEach(middleware => {
    store.dispatch = middleware(store);
  });
}

最后,应用中间件:

applyMiddlewares(store, [log, report]);

进一步优化

之所以在应用中间件过程中每次都重新给 store.dispatch 赋值,是想让后续中间件在通过 store.dispatch 访问时,能够拿到前面中间件修改过的 dispatch 函数。

如果中间件中不是直接从 store 身上去获取 store.dispatch,而是前面已经执行过的中间件将新的 dispatch 传递给中间件,则可以避免每次对 store.dispatch 的赋值。

function applyMiddlewares(store, middlewares) {
  store.dispatch = middlewares.reduce(
    (next, middleware) => middleware(next),
    store.dispatch
  );
}

忽略掉实际源码中的一些差异,以上,大致就是 Redux 中间件的创建和应用了。

测试

function m1(next) {
  return function(action) {
    console.log(`1 start`);
    next(action);
    console.log(`1 end`);
  };
}
function m2(next) {
  return function(action) {
    console.log(`2 start`);
    next(action);
    console.log(`2 end`);
  };
}
function m3(next) {
  return function(action) {
    console.log(`3 start`);
    next(action);
    console.log(`3 end`);
  };


applyMiddlewares(store, [m1, m2, m3]);
store.dispatch({ type: "FOO" });
store.dispatch({ type: "BAR" });
}

输出结果:

3 start
2 start
1 start
dispating action: { type: 'FOO' }
1 end
2 end
3 end
3 start
2 start
1 start
dispating action: { type: 'BAR' }
1 end
2 end
3 end

相关资源