owned this note
owned this note
Published
Linked with GitHub
# Middleware of Redux
###### tags: `javascript`、`redux`、`middleware`
Redux 的 action 傳到 reducer 以前,中間還會有 middleware 的處理,下面接著來介紹兩套middleware 的 library。
介紹 library 以前,先聲明在這篇文章所用的程式碼來自於 [Redux 的介紹與基礎概念](/3PEEcAvYRkiJ2EQQvUUjgQ) 的範例。
首先先看到 `PostsContainer.js` 和 `action.js` 兩個檔案的一小部分:
`PostsContainer.js` 檔案中的 `mapDispatchToProps` 函式:
```jsx=
const mapDispatchToProps = dispatch => {
return {
getPostsList: () => {
// 先確認有沒有要開始 GET posts 了,
// 有的話,isLoadingGetPosts 的值會變成 true。
dispatch(actions.getPosts());
// 這裡的 getPosts() 是從 WebAPI.js 檔案 import 進來的。
getPosts().then(res => {
dispatch(actions.getPostsSuccess(res.data))
})
}
}
}
```
原先的 `PostsContainer.js` 檔案中的 `mapDispatchToProps` 函式,裡面有個 `PostsContainer.js` 專屬的 `getPostsList` 這個 props,這個 props 藉由 API 串接可以拿到 post 的所有資料,但是這樣做有個缺點,如果今天其它 component 也要用上面這串程式碼去拿到全部資料,那麼勢必得再用同樣的程式碼,這樣重複性質增加會造成維護性不佳而且效率不高。
為了解決這樣的問題,我們可以把 `getPostsList` 中的程式碼移動到 `actions.js` 裡面去,新增一個 action:
```javascript=
import * as actionTypes from './actionTypes.js';
import { getPosts as getPostsAPI } from './WebAPI.js';
export const handleNavPage = (page) => ({
type: actionTypes.HANDLE_NAV_PAGE,
page,
});
// 拿到全部文章
export const getPosts = () => ({
type: actionTypes.GET_POSTS,
});
export const getPostsSuccess = (data) => ({
type: actionTypes.GET_POSTS_SUCCESS,
data,
});
// 增加此處
export const getPostsList = (dispatch) => {
dispatch(getPosts());
getPostsAPI().then((res) => {
dispatch(getPostsSuccess(res.data));
});
}
```
然後 `PostsContainer.js` 檔案那邊就可以改成這樣:
```javascript=
const mapDispatchToProps = (dispatch) => ({
getPostsList: () => {
actions.getPostsList(dispatch)
},
});
```
變成單純從 `actions.js` 檔案去拿 `getPostsList` 這個方法。因此,之後只要拿取全部文章的 component 都只需要這麼寫即可,而這樣寫如果要改也很好改,只需要到 action 那邊去改即可。
不過,這樣做的話,小專案還行,但大專案 action 項目一多的話, `actions.js` 這個檔案就會變得很亂,有 function,又有 action 的 object,因此,middeleware 的 library 就是為了解決這樣的問題而誕生。
## Redux-thunk
以往 `actions.js` 中的 action 只能單純回傳物件資料,若使用 Redux-thunk 的話就可以在 action 當中回傳一個 function。
Redux-thunk 做的事情和我們上面剛才所更改的程式碼是差不多意思的。通常在非同步會使用這個套件。action 裡面放入一個要 return 的 function 是真正要做的事(假如是非同步動作就是非同步),而 redux-thunk 的用處就在可以對 return 的 function 執行後續動作。
### 建置
安裝 redux-thunk:`npm install redux-thunk`
引入 `applyMiddleware ` 和 `thunk `:
```javascript=
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
```
store 的第二個參數(也就是 store 初始化時的值)新增 `applyMiddleware(thunk)`
```javascript=
const store = createStore(reducers, applyMiddleware(thunk));
```
### dispatch 改成收到 function 的資料
`actions.js` 檔案的 `getPostsList` 函式改成這樣:
```javascript=
export const getPostsList = () => {
return function (dispatch) {
dispatch(getPosts());
getPostsAPI().then((res) => {
dispatch(getPostsSuccess(res.data));
});
}
}
```
`PostsContainer.js` 的 `mapDispatchToProps` 函式則改成這樣:
```javascript=
const mapDispatchToProps = (dispatch) => {
return {
getPostsList: () => {
dispatch(actions.getPostsList())
}
}
};
```
而這整個運作流程是這樣子的:
1. 在`Posts` 這個 component 裡面,呼叫 `this.props.getPostsList`。
2. `mapDispatchToProps` 函式當中的 `getPostsList` 方法就開始運作,首先看到 `dispatch` 包著的 `actions.getPostsList()` 是去呼叫 `actions.js` 檔案的 `getPostsList` 函式。
3. 再來我們去看 `actions.js` 檔案的 `getPostsList` 函式,這個函式被呼叫了,所以回傳裡面一整個 function 的內容。
4. 所以在 `PostsContainer.js` 的 `mapDispatchToProps` 函式當中的 `getPostsList` 方法,`dispatch` 就會接到一個 `actions.getPostsList()` 回傳回來的 function。
5. 因為我們在 store 建立的時候有傳入 redux-thunk 的函式,所以 redux-thunk 就會幫我們運作傳到 `dispatch` 中為 function 的資料(不然以往 disptach 只能處理物件的資料)。
6. 運作完 function 的資料後,後面步驟就和以往使用 Redux 一樣,資料就會被傳到 reducer 了,reducer 那邊就會再做分類的處理。
所以整個流程中,redux-thunk 扮演著一個角色是可以幫我們在 `dispatch` 當中運作 function。
上面這樣做的好處是 `actions.js` 那邊和 `dispatch` 有關的函式,就都不用再傳入 `dispatch` 的參數,因為 redux-thunk 會幫你傳 `dispatch` 進去到 function,而且,不用傳 `dispatch` 參數就會讓 `actions.js` 檔案變得比較簡潔。
## Redux-promise-middleware
### 非同步操作
我們如果要使用非同步操作的 API 串接,可以使用一個方便的 library 叫做 redux-promise-middleware。
### 建置
安裝:`npm i redux-promise-middleware -s`
引入 `applyMiddleware` 和 `promise`:
```javascript=
import { createStore, applyMiddleware } from 'redux';
import promise from 'redux-promise-middleware'
```
store 的第二個參數改成 `promise`
```javascript=
const store = createStore(reducers, applyMiddleware(promise));
```
### dispatch 執行非同步
在上面的 redux-thunk 例子當中,`actions.js` 檔案的 `getPostsList` 函式是長這樣:
```javascript=
export const getPostsList = () => {
return function (dispatch) {
dispatch(getPosts());
getPostsAPI().then((res) => {
dispatch(getPostsSuccess(res.data));
});
}
}
```
我們現在使用 redux-promise,把它改成這樣:
```javascript=
export const getPostsList = () => {
return {
type: 'GET_POST_LIST',
payload: getPostsAPI()
}
}
```
其中,`type` 的用法就和其它 action 的 `type` 一樣是用來當作 reducer 那邊去分辨你是什麼 type 傳進來的,以便 reducer 做相對應的輸出,而 `payload` 則是 redux-promise 固定的參數,這個參數很重要,用來放是 promise 的物件,這邊的 `getPostsAPI()` 因為是個使用 `axios` 套件的一個函式,所以它是個 promise 物件。
接著我們先在 `reducer.js`(這個檔案在開頭那個連結範例裡中有,是負責 redux 操作中的 reducer 這個流程的檔案) 那邊用 `console.log(action)` 看一下有什麼 action 傳進來:
```javascript=
function reducer(globalState = state, action) {
console.log(action) //這裡
switch (action.type) {
case actionTypes.HANDLE_NAV_PAGE:
return {
...globalState,
navPage: action.page,
};
case actionTypes.GET_POSTS:
return {
...globalState,
isLoadingGetPosts: true,
};
case actionTypes.GET_POSTS_SUCCESS:
return {
...globalState,
isLoadingGetPosts: false,
posts: action.data,
};
case actionTypes.GET_SINGLE_POST:
return {
...globalState,
isLoadingGetSinglePost: true,
};
case actionTypes.GET_SINGLE_POST_SUCCESS:
return {
...globalState,
isLoadingGetSinglePost: false,
post: action.data,
};
default:
return globalState;
}
}
```
`console.log` 的結果:

上方看到有個 `GET_POST_LIST_PENDING` 和 `GET_POST_LIST_FULFILLED` 的 type,這兩個 type 就是從 `actions.js` 檔案的 `getPostsList` 函式那邊得來的,其中 `GET_POST_LIST_PENDING` 可以把它看作正在等待非同步資料請求回傳的 type,也就是上方 `reducer.js` 當中的 `case actionTypes.GET_POSTS:` 這個 case 在做的事情,而 `GET_POST_LIST_FULFILLED` 則是資料已經確定回傳回來了的 type,就像 `case actionTypes.GET_POSTS_SUCCESS:` 這個一樣。
所以接著我們把原先在 `reducer.js` 當中拿到全部文章的 case 改成上面那兩個 type:
```javascript=
function reducer(globalState = state, action) {
console.log(action)
switch (action.type) {
case actionTypes.HANDLE_NAV_PAGE:
return {
...globalState,
navPage: action.page,
};
case 'GET_POST_LIST_PENDING': //這裡
return {
...globalState,
isLoadingGetPosts: true,
};
case 'GET_POST_LIST_FULFILLED': // 和這裡
return {
...globalState,
isLoadingGetPosts: false,
posts: action.payload.data, // payload 有個 data 的參數是拿到回傳資料
};
case actionTypes.GET_SINGLE_POST:
return {
...globalState,
isLoadingGetSinglePost: true,
};
case actionTypes.GET_SINGLE_POST_SUCCESS:
return {
...globalState,
isLoadingGetSinglePost: false,
post: action.data,
};
default:
return globalState;
}
}
```
這樣子的話,原先在 `actions.js` 中建立的 `getPosts` 和 `getPostsSuccess` type 就不需要了,因為被 `'GET_POST_LIST_PENDING'` 和 `'GET_POST_LIST_FULFILLED'` 給替代。
另外,`'GET_POST_LIST_PENDING'` 和 `'GET_POST_LIST_FULFILLED'` 的名字由來是從 `actions.js` 的 `getPostsList` 函式裡面的 `type` 值而定,也就是說在 type 的字串當中除了 `PENDING` 和 `FULFILLED` 是 redux-promise 固定的名稱,字串其它的字元是 `type` 的值。
那麼 redux-promise 是怎麼運作的呢?我們再回頭過來看 `actions.js` 檔案的 `getPostsList` 函式:
```javascript=
export const getPostsList = () => {
return {
type: 'GET_POST_LIST',
payload: getPostsAPI()
}
}
```
大概就是這樣:
```javascript=
type = 'GET_POSTS'
=> dispatch({ type: type + '_PENDING'})
=> action.payload.then((data) => dispatch({
type: type + '_FULFILLED',
payload: data
})).catch((err) => {
dispatch({
type: type + '_REJECTED',
payload: err
})
})
```
當偵測到 payload 的值是一個 promise 的時候就會做上面的動作,先 `dispatch` 一個 type 為 `'GET_POSTS' + '_PENDING'`,接著就執行 promise 的動作(`action.payload`),然後 `.then()` 裡面包的就是要接傳回來的資料,會再 `dispatch` 一個 type 為 `'GET_POSTS' + '_FULFILLED'`,然後 `payload` 的值就是放傳回來的 `data`。
另外 redux-promise 還有一個 type 是 `_REJECTED`,是接回傳資料失敗訊息的 type。
### 總結:
redux-promise 是一個用來處理 redux 非同步操作的 library,可以讓你的程式碼更加簡潔易懂。
而 redux-thunk 和 redux-promise 可以擇一使用,各有千秋,不過 redux-thunk 比較多人使用,因為自由度較高,action 不僅限於非同步或 promise 的資料。
## Redux-saga
很難的東西,待研究。
GitHub:[redux-saga](https://github.com/redux-saga/redux-saga)
## Redux-observable
可以處理更複雜的大專案的非同步。以 [RxJS](https://github.com/ReactiveX/RxJS) 為基底技術的一套 middleware。
GitHub:[redux-observable](https://github.com/redux-observable/redux-observable)
## Redux-logger
可以看 action 執行前後狀態的 library,也就是可以看到改變前的 state、實際執行的 action、改變後的 state。
簡單說這個 library 有助於 debug 使用。
注意在和其它 middleware 同時使用時,redux-logger 必須放在 middleware chain 的最後一個,這樣才可以偵測到所有的 middleware 跑完之後的情形。
middleware chain 是下面這種情:
```javascript=
const store = createStore(reducers, applyMiddleware(
promise,
logger
));
```
也就是在 `applyMiddleware` 裡面串了(使用)兩個以上的 middleware。
GitHub:[redux-logger](https://github.com/LogRocket/redux-logger)