介紹 Redux Middleware,實作同步/非同步 middleware
回憶
前天我們介紹了Flux framework 套件 Redux,昨天實作 Redux 和 React 做串接,裡面提到不少概念:
- reducer: Redux 中實際存放 state 的單元
- React Context: 任何地方的 subcomponent 中可以存取到的資料(store),利用
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 概念:本篇講的 middleware 都是指 Redux Middleware,除非有出現 express middleware 會特別指明是誰的 middleware。
- 實作同步 middleware logger 並學習套用 redux middleware
- 實作非同步 middleware PromiseMiddleware,讓你可以延伸 action 的結構、派分出非同步 action
Redux Middleware
Redux 也有 middleware,它類似 express middleware,我不得不說他更能難讓人理解,因為它是 higher-order function。不管理解到什麼程度,只要記得一件事:
Redux middleware 內的實作要把 action 送到 next(action) 中
Redux 在沒有引入 middleware 前的運作都是同步的,引入非同步的 middleware 就可以派分出非同步的 action。Redux Middleware 的威力很強,讓第三方的程式嵌入 Redux 的運作中。
Redux Middleware 和 Express Middleware 有一點相似之處:
- Express Middleware:處理 「request」 的 middleware, middleware 從
res, req, next換到下一個res, req, nextmiddleware ,res、req都是物件,呼叫next()換下一個 middleware 執行。 - Redux Middleware:處理 「dispatch」 的 middleware, middleware 把
dispatch產生一個新的dispatch,dispatch是(處理 action)函數,在它的實作中要把 action 送到next(action)中,next是內層的 dispatch function。
文件是用 Flow notation定義,我也列出 JSDoc 定義,選你習慣的看
- Flow notation
1type Action = Object 2type AsyncAction = any 3type MiddlewareAPI = { dispatch: Dispatch, getState: () => State } 4 5type BaseDispatch = (a: Action) => Action 6type Dispatch = (a: Action | AsyncAction) => any 7 8type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch - JSDoc
1Action: object (含有 type 屬性) 2AsyncAction: any 3MiddlewareAPI: { dispatch: Dispatch, getState: () => State } 4 5function BaseDispatch(a: Action): Action 6function Dispatch(a: Action | AsyncAction): any 7 8function Middleware(api: MiddlewareAPI): function(next: Dispatch): Dispatch
上面的 BaseDispatch 一般是指最內層的 dispatch function,會把 action 送到 reducer 中。由 Redux 提供,就是之前提的 store.dispatch。然而,一個 middleware 的作用,會在 store.dispatch 外層在套一個 dispatch function,類似:
1const oldDispatch = store.dispatch // 留下舊的 dispatch function
2const newDispatch = action => {
3 return oldDispatch(action);
4}
5store.dispatch = newDispatch; // 替換成新的 dispatch function
用 middleware 來演示就像:
1const oldDispatch = store.dispatch // 留下舊的 dispatch function
2const middleware = dispatch => {
3 const newDispatch = action => {
4 return oldDispatch(action);
5 }
6 return newDispatch;
7}
8
9store.dispatch = middleware(oldDispatch); // 替換成新的 dispatch function
以上就是 redux middleware 期望要做的事。
接下來,我們仔細的看 redux middleware 的簽章。 Middleware 會輸入一個 api 參數,回傳一個函數。 分別看它們的型態:
- 「api」 是
MiddlewareAPI。它是一個物件,有dispatch,getState屬性。原始碼 表示只為了把做出包含api的閉包。 - 「回傳函數」 是
function(next: Dispatch): Dispatch,送入 Dispatch 回傳 Dispatch,所以用箭頭函數寫實作就要出現型如:主要是這個才是 middleware 的本體。若再把1next => { 2 return <Dispatch> 3}<Deispatch>打開這才是我們真的要實作的 middleware。此時的1next => { 2 return (action) => { 3 return <any> 4 } 5}next其實就是內層的 dispatch function。
為了方便理解 middleware 我們先不要管 api,就直接看真的要實作的 middleware(即作用完 api 的回傳函數)。
簡化版 middleware
考慮兩個 middleware F,G 和 印出資料的 baseDispatch,這裡 F,G的簽章都是 function(next: Dispatch): Dispatch 所以它們可以串接,有兩種串法 F ● G 或 G ● F,我們只考慮 F ● G 合成,我可以得到最後的 dispatch function:
1dispatchFG = F(G(baseDispatch));
若把 action 送到 dispatchFG,就是
1dispatchFG(action) = F(G(baseDispatch))(action)
令 dispatchF = F(G(baseDispatch))
1F(G(baseDispatch))(action) = dispatchF(action)
就是說 action 會先被 dispatchF 作用。
令dispatchG = G(baseDispatch)
得到
1F(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() 一樣
1// mimicBasic.js
2/**
3 *
4 * @callback Dispatch
5 * @param {Action} action
6 * @returns {any}
7 */
8
9/**
10 *
11 * @param {Dispatch} next
12 * @returns {Dispatch}
13 */
14function F(next) {
15 return function dispatchF(action) {
16 console.log('dispatchF');
17 action = action + ` -> F`;
18 next(action); // next = dispatchG = G(baseDispatch)
19 };
20}
21
22/**
23 *
24 * @param {Dispatch} next
25 * @returns {Dispatch}
26 */
27function G(next) {
28 return function dispatchG(action) {
29 console.log('dispatchG');
30 // action
31 action = action + ` -> G`;
32 next(action); // next = baseDispatch
33 };
34}
35
36/**
37 *
38 * @type {Dispatch}
39 */
40function baseDispatch(action) {
41 console.log(action);
42}
43
44/**
45 * 合成 middleware F, G
46 * @type {Dispatch}
47 */
48const dispatchFG = F(G(baseDispatch));
49
50dispatchFG('action');
51console.log('done');
結果:
1dispatchF
2dispatchG
3action -> F -> G
4done
圖解就是如下:
Redux 原版 middleware
還原成 Redux 原來的定義,把 api 弄進來,最後再寫成箭頭函數,就是 Redux middleware 最完整的簽章。
1// mimic.js
2const f = (api) => next => action => {
3 console.log('dispatchF');
4 action = action + ` -> F`;
5 next(action); // next = g(api)(baseDispatch)
6};
7const g = (api) => next => action => {
8 console.log('dispatchG');
9 action = action + ` -> G`;
10 next(action); // next = baseDispatch
11};
12const baseDispatch = (action) => console.log(action);
13
14function applayMiddleware(f, g) {
15 const api = {};
16 const G = g(api);
17 const F = f(api);
18
19 const dispatchG = G(baseDispatch);
20 const dispatchFG = F(dispatchG);
21 return dispatchFG; // F(G(baseDispatch)) = f(api)(g(api)(baseDispatch))
22}
23
24const dispatchFG = applayMiddleware(f, g);
25
26dispatchFG('action');
27console.log('done');
Redux middleware 小總結
Redux middleware 一定長成 (api: MiddlewareAPI) => (next: Dispatch) => Dispatch,才可以做合成 F ● G。
我們觀察到:
next是指內層的 dispatch function,因為(F ● G(BaseDispatch))(action) = F(dispatchG)(action) = F(next)(action)- Redux middleware 實作規定,一定要呼叫
next(action),action 才能一直往內層送,不然就會斷掉,reducer 就收不到 action 了。 - 最後一個把 action 送到 reducer 的 dispatch 就叫做 BaseDispatch,它的一定是
action => action,其它中間過程是 action 被改成長什麼樣子都可以,所以你才會看到AsyncAction: any這特別的定義。 - middleware 的目地是把 dispatch function 合併成新的 dispatch function。
return next(action)在非同步的 middleware 比較少用。
使用 Redux middleware: 同步 middleware logger
前面我們分析了 redux middleware,看不懂沒關係可能是我寫的不好 ><。
只要記得一件事:
Redux middleware 內的實作要把 action 送到 next(action) 中
這樣才能引起一連串的內部 dispatch function 運作。剩下的只要知道 middleware 簽章,你也可以寫出自己的 middleware。
接下來,我們來套用 redux middleware 到 store 中,只需要 createStore() 中使用 applyMiddleware()。
當沒有用任何 middleware 時,像
1const store = createStore(reducer, initState);
此時的 store 的 dispatch function store.dispatch 是「某個 BaseDispatch 的實體」(我們暫時稱為 aBaseDispatch),這是最內層的 dispatch function,內部會把 action 送入 reducer 中。
假如,套用 logger middleware,
1function logger({ getState }) {
2 return next => action => {
3 console.log('will dispatch: ' + JSON.stringify(action));
4
5 // 送 action 到內層 dispatch function
6 const returnValue = next(action);
7
8 console.log('state after dispatch: ' + JSON.stringify( getState()));
9
10 // 在同步的 middleware 才有用
11 return returnValue;
12 }
13}
1const { createStore, applyMiddleware } = require('redux');
2const 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,
1const action = {
2 type: identityChangeMessage,
3 payload: {
4 message: 'change',
5 },
6}
action 經過 logger middleare 會印出
1will dispatch: {"type":"CHANGE_MESSAGE","payload":{"message":"change"}}
2state after dispatch: {"message":"change"}
寫自己的非同步 middleware:發出非同步 action
這裡雖然是自己做非同步 middleware 但只是學習用,除非你有獨到的見解或其它考量,否則還是建議使用 middleware 套件:redux-thunk、redux-observable、redux-saga。
promise action : 帶有 promise 的 action
假設我們可以發出一個帶有 promise 的 action,例如:
1const promiseAction = {
2 type: 'CHANGE_MESSAGE',
3 promise: Promise.resolve({message: 'changed'})
4}
5
6store.dispatch(promiseAction);
PromiseMiddleware: 可以處理 pomise 的 middleware
我們需一個 middleware,它要做以下的事
- 它可以判斷 action 是否是 promise action
- 若不是 promise action,不要做任何事,傳 action 到內層 middleware 做事 (即
return next(action)) - 若是 promise action,它會為 action 加入三種狀態 (
_TRIGGER,_SUCCESS,_FAIL)- 觸發 action:
CHANGE_MESSAGE_TRIGGER - promise resolve:
CHANGE_MESSAGE_SUCCESS,且 resolve data 放在action.payload - promise reject:
CHANGE_MESSAGE_FAIL,且 reject error 放在action.error
- 觸發 action:
- 除了第一次的 trigger action,Promise 得到結果後也要引起
next(action)讓內層 dispatch fuction 作用
把上述寫成程式
1// PromiseMiddleware
2function PromiseMiddleware(action) {
3 return next => {
4 return function dispatchAsync(action) {
5 if (action.promise instanceof Promise) {
6 console.log('Promise action');
7 const { type, promise, ...others } = action;
8 promise
9 .then(data => {
10 next({
11 type: success(type),
12 payload: data,
13 promise,
14 ...others
15 });
16 })
17 .catch(error => {
18 next({
19 type: fail(type),
20 error: error,
21 promise,
22 ...others
23 });
24 });
25 return next({
26 type: trigger(type),
27 promise,
28 ...others
29 });
30 } else {
31 console.log('Not promise action');
32 return next(action);
33 }
34 }
35 }
36}
修改一下之面的 logger 方便我們觀察 action
1function logger({ getState }) {
2 return next => action => {
3 console.log('========== action dispatching(start) ===============');
4 console.log('will dispatch: ' + JSON.stringify(action));
5 const returnValue = next(action);
6 console.log('state after dispatch: ' + JSON.stringify(getState()));
7 console.log('========== action dispatching(end) ===============');
8 return returnValue;
9 }
10}
使用 PromiseMiddleware 和 logger middlewares
接下來,使用 PromiseMiddleware 和 logger
1const store = createStore(reducer, initState, applyMiddleware(PromiseMiddleware, logger));
要小心,middleware 順序不能換,因為 promise action 要先進到 PromiseMiddleware 的 dispatch function(dispatchAsync) 作用, action 才能進到內層的 logger 中 dispatch function 印出。
你也可以試試看順序倒過來會怎麼樣。
發出一個 promise action
定義一個 promise action
1const identityChangeMessage
2// Case 1: 建立一個 resolve action
3const resolvePromiseAction = {
4 type: identityChangeMessage,
5 promise: Promise.resolve({
6 message: 'changed',
7 })
8};
派分 promise action
1store.dispatch(resolvePromiseAction);
結果如下:
1========== action dispatching(start) ===============
2will dispatch: {"type":"CHANGE_MESSAGE_TRIGGER","promise":{}}
3state after dispatch: {"message":"identityChangeMessage trigger"}
4========== action dispatching(end) ===============
5waiting...
6========== action dispatching(start) ===============
7will dispatch: {"type":"CHANGE_MESSAGE_SUCCESS","payload":{"message":"changed"},"promise":{}}
8state after dispatch: {"message":"changed"}
9========== 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
總結非同步的 middleware:PromiseMiddleware
非同步的 middleware 有下列的特性:
- 新形態的 action:它讓我們可以派分 promise action 這種新形態的 action,例如:在
PromiseMiddleware中解讀 promise action, 就可以產生其它 action 來模擬 Promise 的運作過程。 - 派分任意次 action:我們可以派分任意多次的
next(action),就像我們發出了二個 actions
實務上,在發出 某 action 後可能又要發出其它 action,如下圖
雖然這大量產生的 action 讓人有點詬病,但使用良好的非同步 action 套件,可以一定程度控制我們的程式碼,避免程式碼混亂。
- action 自由性:
next(action)中的 action 可以在 middleware 中任意建立、修改、更換,例如:我們建立新的 action{type: success(type), payload: data, promise, ...others}( action type 加入後綴詞)
Redux 生態系
Redux 是很小的套件,以它為核心已經發展出大量的相關套件,當然你想要什麼功能除了自己實作,也可以用別人的套件。
我的學習方法是先自己試寫看看體驗一下痛苦,再套用大神們的套件,因為自己臨時寫的 API 我不覺得會比大神們經過時間粹煉的套件好用、穩定。
Redux 生態系列表如下:
- Redux exosystem(offical):裡面可以看到套件程式碼火力展示
- Redux 生態系(中文):當功能的英文關鍵字都不知道就可以查看看
我還是可以小小的註解一下:
非同步 middleware 套件:用來發出非同步 action 的套件
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 語法不用學新的語法。
Routing:頁面切換
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 生態系為結尾。
評論