前天我們介紹了Flux framework 套件 Redux,昨天實作 Redux 和 React 做串接,裡面提到不少概念:
react-redux 就可以方便把 Redux 和 React 串接今天要談的是最後也最重要的東西: Redux Middleware
所有的範例程式放在 ithelp-30dayfullstack-hello-redux,有需要的請自取。
學習完 middleware,我想當你在套用 Redux 相關套件 react-router-redux, connected-react-router、redux-thunk、redux-observable、redux-saga…等會有很大的信心和理解在做什麼。
Redux 也有 middleware,它類似 express middleware,我不得不說他更能難讓人理解,因為它是 higher-order function。不管理解到什麼程度,只要記得一件事:
Redux middleware 內的實作要把 action 送到 next(action) 中
Redux 在沒有引入 middleware 前的運作都是同步的,引入非同步的 middleware 就可以派分出非同步的 action。Redux Middleware 的威力很強,讓第三方的程式嵌入 Redux 的運作中。
Redux Middleware 和 Express Middleware 有一點相似之處:
res, req, next 換到下一個 res, req, next middleware ,res、req 都是物件,呼叫 next() 換下一個 middleware 執行。dispatch 產生一個新的 dispatch,dispatch 是(處理 action)函數,在它的實作中要把 action 送到 next(action) 中, next 是內層的 dispatch function。文件是用 Flow notation定義,我也列出 JSDoc 定義,選你習慣的看
type Action = Object
type AsyncAction = any
type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }
type BaseDispatch = (a: Action) => Action
type Dispatch = (a: Action | AsyncAction) => any
type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch
Action: object (含有 type 屬性)
AsyncAction: any
MiddlewareAPI: { dispatch: Dispatch, getState: () => State }
function BaseDispatch(a: Action): Action
function Dispatch(a: Action | AsyncAction): any
function Middleware(api: MiddlewareAPI): function(next: Dispatch): Dispatch
上面的 BaseDispatch 一般是指最內層的 dispatch function,會把 action 送到 reducer 中。由 Redux 提供,就是之前提的 store.dispatch。然而,一個 middleware 的作用,會在 store.dispatch 外層在套一個 dispatch function,類似:
const oldDispatch = store.dispatch // 留下舊的 dispatch function
const newDispatch = action => {
return oldDispatch(action);
}
store.dispatch = newDispatch; // 替換成新的 dispatch function
用 middleware 來演示就像:
const oldDispatch = store.dispatch // 留下舊的 dispatch function
const middleware = dispatch => {
const newDispatch = action => {
return oldDispatch(action);
}
return newDispatch;
}
store.dispatch = middleware(oldDispatch); // 替換成新的 dispatch function
以上就是 redux middleware 期望要做的事。
接下來,我們仔細的看 redux middleware 的簽章。 Middleware 會輸入一個 api 參數,回傳一個函數。 分別看它們的型態:
MiddlewareAPI。它是一個物件,有 dispatch, getState 屬性。原始碼 表示只為了把做出包含 api 的閉包。function(next: Dispatch): Dispatch,送入 Dispatch 回傳 Dispatch,所以用箭頭函數寫實作就要出現型如:
next => {
return <Dispatch>
}
主要是這個才是 middleware 的本體。若再把 <Deispatch> 打開
next => {
return (action) => {
return <any>
}
}
這才是我們真的要實作的 middleware。此時的 next 其實就是內層的 dispatch function。為了方便理解 middleware 我們先不要管 api,就直接看真的要實作的 middleware(即作用完 api 的回傳函數)。
考慮兩個 middleware F,G 和 印出資料的 baseDispatch,這裡 F,G的簽章都是 function(next: Dispatch): Dispatch 所以它們可以串接,有兩種串法 F ● G 或 G ● F,我們只考慮 F ● G 合成,我可以得到最後的 dispatch function:
dispatchFG = F(G(baseDispatch));
若把 action 送到 dispatchFG,就是
dispatchFG(action) = F(G(baseDispatch))(action)
令 dispatchF = F(G(baseDispatch))
F(G(baseDispatch))(action) = dispatchF(action)
就是說 action 會先被 dispatchF 作用。
令dispatchG = G(baseDispatch)
得到
F(G(baseDispatch))(action) = F(dispatchG)(action)
Redux 要求:F 內的實作也要把 action 送到 dispatchG(action) 中 。
若把 dispatchG 改叫 next 重寫上面一句話: F 內的實作也要把 action 送到 next(action) 中 。
因此,就得到重要的規則:
Redux middleware 內的實作要把 action 送到 next(action) 中
這就和 express middleware 要求:
Express middleware 內的實作一定要呼叫 next() 一樣
// mimicBasic.js
/**
*
* @callback Dispatch
* @param {Action} action
* @returns {any}
*/
/**
*
* @param {Dispatch} next
* @returns {Dispatch}
*/
function F(next) {
return function dispatchF(action) {
console.log('dispatchF');
action = action + ` -> F`;
next(action); // next = dispatchG = G(baseDispatch)
};
}
/**
*
* @param {Dispatch} next
* @returns {Dispatch}
*/
function G(next) {
return function dispatchG(action) {
console.log('dispatchG');
// action
action = action + ` -> G`;
next(action); // next = baseDispatch
};
}
/**
*
* @type {Dispatch}
*/
function baseDispatch(action) {
console.log(action);
}
/**
* 合成 middleware F, G
* @type {Dispatch}
*/
const dispatchFG = F(G(baseDispatch));
dispatchFG('action');
console.log('done');
結果:
dispatchF
dispatchG
action -> F -> G
done
圖解就是如下:
(C) 是才是範例的圖,(B) 是只看 G 的作用,(A) 是沒有任何 middleware 的圖。(你可能需要花時間自己想圖的作用)
還原成 Redux 原來的定義,把 api 弄進來,最後再寫成箭頭函數,就是 Redux middleware 最完整的簽章。
// mimic.js
const f = (api) => next => action => {
console.log('dispatchF');
action = action + ` -> F`;
next(action); // next = g(api)(baseDispatch)
};
const g = (api) => next => action => {
console.log('dispatchG');
action = action + ` -> G`;
next(action); // next = baseDispatch
};
const baseDispatch = (action) => console.log(action);
function applayMiddleware(f, g) {
const api = {};
const G = g(api);
const F = f(api);
const dispatchG = G(baseDispatch);
const dispatchFG = F(dispatchG);
return dispatchFG; // F(G(baseDispatch)) = f(api)(g(api)(baseDispatch))
}
const dispatchFG = applayMiddleware(f, g);
dispatchFG('action');
console.log('done');
Redux middleware 一定長成 (api: MiddlewareAPI) => (next: Dispatch) => Dispatch,才可以做合成 F ● G。
我們觀察到:
next 是指內層的 dispatch function,因為 (F ● G(BaseDispatch))(action) = F(dispatchG)(action) = F(next)(action)
next(action),action 才能一直往內層送,不然就會斷掉,reducer 就收不到 action 了。action => action,其它中間過程是 action 被改成長什麼樣子都可以,所以你才會看到 AsyncAction: any 這特別的定義。return next(action) 在非同步的 middleware 比較少用。前面我們分析了 redux middleware,看不懂沒關係可能是我寫的不好 ><。
只要記得一件事:
Redux middleware 內的實作要把 action 送到 next(action) 中
這樣才能引起一連串的內部 dispatch function 運作。剩下的只要知道 middleware 簽章,你也可以寫出自己的 middleware。
接下來,我們來套用 redux middleware 到 store 中,只需要 createStore() 中使用 applyMiddleware()。
當沒有用任何 middleware 時,像
const store = createStore(reducer, initState);
此時的 store 的 dispatch function store.dispatch 是「某個 BaseDispatch 的實體」(我們暫時稱為 aBaseDispatch),這是最內層的 dispatch function,內部會把 action 送入 reducer 中。
假如,套用 logger middleware,
function logger({ getState }) {
return next => action => {
console.log('will dispatch: ' + JSON.stringify(action));
// 送 action 到內層 dispatch function
const returnValue = next(action);
console.log('state after dispatch: ' + JSON.stringify( getState()));
// 在同步的 middleware 才有用
return returnValue;
}
}
const { createStore, applyMiddleware } = require('redux');
const store = createStore(reducer, initState, applyMiddleware(logger));
把 middleware 用到 store dispatch function 中,要用 applyMiddleware 這函數。 applyMiddleware(...middleware) 會回傳 enhancer,給 createStore 使用 (就是 enhancer(createStore) 變成新的 store)。
此時 store 的 dispatch function store.dispatch 的真實身份是 logger(aBaseDispatch),
若送出一個 action,
const action = {
type: identityChangeMessage,
payload: {
message: 'change',
},
}
action 經過 logger middleare 會印出
will dispatch: {"type":"CHANGE_MESSAGE","payload":{"message":"change"}}
state after dispatch: {"message":"change"}
這裡雖然是自己做非同步 middleware 但只是學習用,除非你有獨到的見解或其它考量,否則還是建議使用 middleware 套件:redux-thunk、redux-observable、redux-saga。
假設我們可以發出一個帶有 promise 的 action,例如:
const promiseAction = {
type: 'CHANGE_MESSAGE',
promise: Promise.resolve({message: 'changed'})
}
store.dispatch(promiseAction);
PromiseMiddleware: 可以處理 pomise 的 middleware我們需一個 middleware,它要做以下的事
return next(action))_TRIGGER, _SUCCESS, _FAIL)
CHANGE_MESSAGE_TRIGGER
CHANGE_MESSAGE_SUCCESS,且 resolve data 放在 action.payload
CHANGE_MESSAGE_FAIL,且 reject error 放在 action.error
next(action) 讓內層 dispatch fuction 作用把上述寫成程式
// PromiseMiddleware
function PromiseMiddleware(action) {
return next => {
return function dispatchAsync(action) {
if (action.promise instanceof Promise) {
console.log('Promise action');
const { type, promise, ...others } = action;
promise
.then(data => {
next({
type: success(type),
payload: data,
promise,
...others
});
})
.catch(error => {
next({
type: fail(type),
error: error,
promise,
...others
});
});
return next({
type: trigger(type),
promise,
...others
});
} else {
console.log('Not promise action');
return next(action);
}
}
}
}
修改一下之面的 logger 方便我們觀察 action
function logger({ getState }) {
return next => action => {
console.log('========== action dispatching(start) ===============');
console.log('will dispatch: ' + JSON.stringify(action));
const returnValue = next(action);
console.log('state after dispatch: ' + JSON.stringify(getState()));
console.log('========== action dispatching(end) ===============');
return returnValue;
}
}
PromiseMiddleware 和 logger middlewares接下來,使用 PromiseMiddleware 和 logger
const store = createStore(reducer, initState, applyMiddleware(PromiseMiddleware, logger));
要小心,middleware 順序不能換,因為 promise action 要先進到 PromiseMiddleware 的 dispatch function(dispatchAsync) 作用, action 才能進到內層的 logger 中 dispatch function 印出。
你也可以試試看順序倒過來會怎麼樣。
定義一個 promise action
const identityChangeMessage
// Case 1: 建立一個 resolve action
const resolvePromiseAction = {
type: identityChangeMessage,
promise: Promise.resolve({
message: 'changed',
})
};
派分 promise action
store.dispatch(resolvePromiseAction);
結果如下:
========== action dispatching(start) ===============
will dispatch: {"type":"CHANGE_MESSAGE_TRIGGER","promise":{}}
state after dispatch: {"message":"identityChangeMessage trigger"}
========== action dispatching(end) ===============
waiting...
========== action dispatching(start) ===============
will dispatch: {"type":"CHANGE_MESSAGE_SUCCESS","payload":{"message":"changed"},"promise":{}}
state after dispatch: {"message":"changed"}
========== action dispatching(end) ===============
我們發現產生了兩個 action,action type 分別是 CHANGE_MESSAGE_TRIGGER, CHANGE_MESSAGE_SUCCESS,這也符合 Promise 的運作過程,就像是我們模擬 Redux 版本的 fetch() request。當派分 promise action 後,在 reducer 就要收到一個 _TRIGGER 的 action,然後取回資料後,reducer 就要收到一個 _SUCCESS 的 action。
reject promise action 和 normal action 的完整的範例見 middlewareAsync.js
PromiseMiddleware非同步的 middleware 有下列的特性:
PromiseMiddleware 中解讀 promise action, 就可以產生其它 action 來模擬 Promise 的運作過程。next(action),就像我們發出了二個 actions
實務上,在發出 某 action 後可能又要發出其它 action,如下圖
雖然這大量產生的 action 讓人有點詬病,但使用良好的非同步 action 套件,可以一定程度控制我們的程式碼,避免程式碼混亂。
next(action) 中的 action 可以在 middleware 中任意建立、修改、更換,例如:我們建立新的 action {type: success(type), payload: data, promise, ...others} ( action type 加入後綴詞)Redux 是很小的套件,以它為核心已經發展出大量的相關套件,當然你想要什麼功能除了自己實作,也可以用別人的套件。
我的學習方法是先自己試寫看看體驗一下痛苦,再套用大神們的套件,因為自己臨時寫的 API 我不覺得會比大神們經過時間粹煉的套件好用、穩定。
Redux 生態系列表如下:
我還是可以小小的註解一下:
redux-thunk:我第一個用的非同步 middleware 套件。它的原始碼很簡單,把 action 結構改成一個函數(物件),然後派分函數 action。我覺得 action 連發、維護不太好處理,我就轉為 redux-observable。
redux-observable:要用 reactive programming 的概念,RxJS 是 javascript 的 reactive programming 實現套件。 redux-observable 把它們引入 Redux 中。
redux-saga:使用 ES6 的生成器函式(generator function)/ yield 語法不用學新的語法。
react-router 是 react compoent 套件,用來依照網址選擇要渲染的 component。它是 React 相關套件,它與 redux 沒關西。
因為篇福有限,我只點出一件很重要的事: 使用 react-router 要小心版本號
若你要用 react-router 4.x 請用以下組合:
若你要用 react-router 2.x and 3.x 請用以下組合:
今天主要介紹 Redux Middleware,並分別給出了同步和非同步的 middleware 的範例 logger 和 PromiseMiddleware,並在建立 store 時使用 middleware,最後以Redux 生態系為結尾。