從使用到原理,手擼一個自己的redux中介軟體

恍然小悟發表於2018-08-09

中介軟體是個什麼東西?

redux中管理資料的流程是單向的,就是說,從派發動作一直到釋出訂閱觸發渲染是一條路走到頭,那麼如果想要在中間新增或是更改某個邏輯就需要找到action或是reducer來修改,有沒有更方便的做法呢?

redux的流程:

button -觸發事件-> dispath -派發動作-> reducer -釋出訂閱-> view

而中介軟體(middleware)就是一個可插拔的機制,如果想找擴充套件某個功能,比如新增日誌,在更新前後列印出state狀態,只需要將日誌中介軟體裝到redux上即可,於是便有了日誌功能,當不想使用時可再拿掉,非常方便。

中介軟體的使用

先說說用法,只有會用了,再說原理。

redux-logger

redux提供了好多個現成的中介軟體,比如上面提到的日誌中介軟體,安裝它即可使用:

npm i --save redux-logger
複製程式碼

redux包提供了一個方法可以裝載中介軟體:applyMiddleware

在建立store物件的時候,可以傳入第二個引數,它就是中介軟體:

import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
//使用applyMiddleware載入中介軟體
let store = createStore(reducer, applyMiddleware(ReduxLogger));
複製程式碼

這樣在每一次更新state,會在控制檯列印更新日誌:

redux-logger

redux-thunk

redux-thunk中介軟體可以支援非同步action。

載入中介軟體:

import reduxThunk from "redux-thunk";
let store = createStore(reducer, applyMiddleware(reduxThunk));
複製程式碼

當載入了redux-thunk中介軟體,action函式可以支援返回一個函式,將非同步操作封裝在裡面:

function add(payload) {
    return function(dispatch, getState) {
      setTimeout(() => {
        dispatch({ type: ADD, payload });
      }, 2000); //延時2秒執行
    };
}
複製程式碼

可以看到action又返回一個函式,其中的引數dispatch和getState就是redux提供的方法,它將這兩個函式的使用權交給了我們,讓我們等待非同步操作完成 時再呼叫,完成非同步action編寫。

redux-promise

有了redux-thunk中介軟體我們可以編寫非同步action,但我們想更進一步,讓非同步action支援Promise,那麼redux-promise中介軟體可派上用場。

還是安裝redux-promise然後載入它:

import reduxPromise from "redux-promise";
let store = createStore(
  reducer,
  applyMiddleware(reduxPromise)
);
複製程式碼

redux-promise中介軟體可以支援action返回的物件payload為一個Promise:

let action = {
  add: function(payload) {
    return {
      type: ADD,
      //payload是一個Promise物件,非同步操作封裝到裡面
      payload: new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(payload); //執行成功,將引數傳到reducer
        }, 1000);
      })
    };
  },
  minus: function(payload) {
    return {
      type: MINUS,
      payload: new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(payload); //執行失敗,將引數傳到reducer
        }, 1000);
      })
    };
  }
};
複製程式碼

可以看到,payload不再是直接返回引數,而是改為一個Promise物件,這樣就可以把非同步程式碼封裝到裡面。

注意!如果你使用redux-promise中介軟體,payload引數名是固定的,不可隨意改名

比如:

{
  type: MINUS,
  num: new Promise((resolve, reject) => {
    //...
  })
};//此處引數名是num,redux-promise不能正確識別,若使用redux-promise必須叫payload
複製程式碼

中介軟體的原理

通過以上三個中介軟體,可以清楚了它們的用法,都是在state更新的前後擴充套件一些功能,那麼它們的原理是什麼呢?

拿第一個中介軟體redux-logger來舉例,日誌是列印在state更新的前後,那麼改寫store.dispatch()方法是一個方案:

let temp = store.dispatch;//暫存原dispatch方法
store.dispatch = function(action) {
  console.log("舊state:", store.getState());
  temp(action);//執行原dispatch方法
  console.log("新state:", store.getState());
};
複製程式碼

可以看到,首先將原來的dispatch方法臨時儲存到了變數中,並將現有的dispatch方法改寫,增加了輸出日誌的功能,在state未更新之前先輸出,再呼叫暫存的dispatch更新state即可,這樣就相當於實現了redux-logger中介軟體。

雖然這種寫法很噁心,但是這就是redux中介軟體的原理:暫存原dispatch方法,修改dispatch擴充套件功能並返回

中介軟體的通用寫法

原理明白了,但是每次都手動去覆蓋dispath顯然太過麻煩,有沒有通用的寫法呢?顯然是有的。

redux原始碼中是使用高階函式去實現一箇中介軟體,它的方法籤明是這樣的:


let middleware = store => next => action => {
    //具體中介軟體的邏輯...
};
複製程式碼

可以看到,箭頭函式的寫法非常優雅,它是一個三層巢狀的函式,也就是高階函式,它的最終返回值仍是一個方法,這個方法就是最終“擴充套件”了功能的“dispatch”方法。

不好理解?我們可以寫成普通函式的形式,更容易看清邏輯:

function middleware(store) {
  //next為原dispatch方法
  return function(next) {
    //action為傳入派發器的action物件
    return function(action) {
      //中介軟體的具體邏輯寫在這兒...
    };
  };
}
複製程式碼

即然中介軟體就是改寫原dispath方法,那麼我們可以想一想,要想擴充套件原來的dispath都需要哪些東西?應該是以下這些:

  • store倉庫物件(有了store物件才能覆蓋之前的dispath方法)
  • dispatch方法(之前的dispatch)
  • action物件 (派發動作需要action物件)

以上這三個物件必不可少,可以看到這三個物件正是三層函式的引數。第一層store引數實際是createStore()的返回值,就是倉庫;第二層的next引數就是原dispatch方法;最內層的函式引數則是action物件。

搞清楚了方法簽名的結構,我們就可以自己寫出一個redux-logger中介軟體:

export function reduxLogger(store) {
  //next為原dispatch方法
  return function(next) {
    //action為傳入派發器的action物件
    return function(action) {
      console.log("更新前:", store.getState());
      next(action);
      console.log("更新後:", store.getState());
    };
  };
}
複製程式碼

很簡單,在next()執行的前後列印state的狀態即可。

applyMiddleware方法

我們手寫了一箇中介軟體,還要在需要時載入中介軟體,在redux中提供了一個applyMiddleware方法來載入中介軟體:

applyMiddleware(reduxLogger);
複製程式碼

將所需的中介軟體依次傳入即可載入中介軟體。那麼它的原理呢?不防也來看一看。

applyMiddleware的方法簽名仍是一個三層的高階函式,

let applyMiddleware = middlewares => createStore => reducer => {
  //載入中介軟體的邏輯...  
};
複製程式碼

還是一樣,我們改寫成普通函式來分析:

function applyMiddleware(middlewares) {
  //createStore即redux提供的方法
  return function(createStore) {
    //reducer就是傳入更新state的函式
    return function(reducer) {
      //載入中介軟體的邏輯...  
    };
  };
}
複製程式碼

想一下,在應用中介軟體的過程中,目的就是將外界傳入的中介軟體的新dispath方法覆蓋原有的store.dispatch,這樣返回給使用者的store物件的dispatch方法已經由中介軟體擴充套件了,比如這裡就是列印日誌。

那麼applyMiddleware都需要哪些東西呢?

  • 需要應用的中介軟體
  • createStore方法(有了它就可以建立store物件)
  • reducer(建立store物件時需要reducer引數)

可以看到這些正是三層高階函式的引數,這樣我們就可以寫出applyMiddleware的邏輯:

function applyMiddleware(middlewares) {
  return function(createStore) {
    return function(reducer) {
      let store = createStore(reducer); //取得store物件
      let dispatch = middlewares(store)(store.dispatch); //取得新的dispatch方法
      return { ...store, dispatch }; //將新dispatch覆蓋舊的store.dispatch
    };
  };
}
複製程式碼

其中重點是這一條語句:

let dispatch = middlewares(store)(store.dispatch);
複製程式碼

這條語句就是取得中介軟體改寫後的dispatch方法,還記得中介軟體的簽名麼?不防對照它來看一下就會明白:

let middleware = store => next => action => {};
複製程式碼

中介軟體要求傳入第一個引數store物件,通過createStore(reducer)建立; 中介軟體要求傳入第二個引數next,就是原dispatch,那麼store.dispatch就是原倉庫的dispatch。此時返回的結果就是新dispath方法了,最後使用展開運算子將原store物件上的dispatch覆蓋並返回即可。

到此,手寫中介軟體和應用中介軟體的全部原理已經分析完畢。

相關文章