1. 程式人生 > >理解 Redux 的中介軟體

理解 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

相關資源

  • Redux doc - Middleware