上一篇文章,我介紹了 Redux 的基本做法:使用者發出 Action,Reducer 函式算出新的 State,View 重新渲染。
但是,一個關鍵問題沒有解決:非同步操作怎麼辦?Action 發出以後,Reducer 立即算出 State,這叫做同步;Action 發出以後,過一段時間再執行 Reducer,這就是非同步。
怎麼才能 Reducer 在非同步操作結束後自動執行呢?這就要用到新的工具:中介軟體(middleware)。
一、中介軟體的概念
為了理解中介軟體,讓我們站在框架作者的角度思考問題:如果要新增功能,你會在哪個環節新增?
(1)Reducer:純函式,只承擔計算 State 的功能,不合適承擔其他功能,也承擔不了,因為理論上,純函式不能進行讀寫操作。
(2)View:與 State 一一對應,可以看作 State 的視覺層,也不合適承擔其他功能。
(3)Action:存放資料的物件,即訊息的載體,只能被別人操作,自己不能進行任何操作。
想來想去,只有傳送 Action 的這個步驟,即store.dispatch()
方法,可以新增功能。舉例來說,要新增日誌功能,把 Action 和 State 列印出來,可以對store.dispatch
進行如下改造。
let next = store.dispatch; store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action); next(action); console.log('next state', store.getState()); }
上面程式碼中,對store.dispatch
進行了重定義,在傳送 Action 前後新增了列印功能。這就是中介軟體的雛形。
中介軟體就是一個函式,對store.dispatch
方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,新增了其他功能。
二、中介軟體的用法
本教程不涉及如何編寫中介軟體,因為常用的中介軟體都有現成的,只要引用別人寫好的模組即可。比如,上一節的日誌中介軟體,就有現成的redux-logger模組。這裡只介紹怎麼使用中介軟體。
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) );
上面程式碼中,redux-logger
提供一個生成器createLogger
,可以生成日誌中介軟體logger
。然後,將它放在applyMiddleware
方法之中,傳入createStore
方法,就完成了store.dispatch()
的功能增強。
這裡有兩點需要注意:
(1)createStore
方法可以接受整個應用的初始狀態作為引數,那樣的話,applyMiddleware
就是第三個引數了。
const store = createStore( reducer, initial_state, applyMiddleware(logger) );
(2)中介軟體的次序有講究。
const store = createStore( reducer, applyMiddleware(thunk, promise, logger) );
上面程式碼中,applyMiddleware
方法的三個引數,就是三個中介軟體。有的中介軟體有次序要求,使用前要查一下文件。比如,logger
就一定要放在最後,否則輸出結果會不正確。
三、applyMiddlewares()
看到這裡,你可能會問,applyMiddlewares
這個方法到底是幹什麼的?
它是 Redux 的原生方法,作用是將所有中介軟體組成一個陣列,依次執行。下面是它的原始碼。
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer); var dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return {...store, dispatch} } }
上面程式碼中,所有中介軟體被放進了一個陣列chain
,然後巢狀執行,最後執行store.dispatch
。可以看到,中介軟體內部(middlewareAPI
)可以拿到getState
和dispatch
這兩個方法。
四、非同步操作的基本思路
理解了中介軟體以後,就可以處理非同步操作了。
同步操作只要發出一種 Action 即可,非同步操作的差別是它要發出三種 Action。
- 操作發起時的 Action
- 操作成功時的 Action
- 操作失敗時的 Action
以向伺服器取出資料為例,三種 Action 可以有兩種不同的寫法。
// 寫法一:名稱相同,引數不同 { type: 'FETCH_POSTS' } { type: 'FETCH_POSTS', status: 'error', error: 'Oops' } { type: 'FETCH_POSTS', status: 'success', response: { ... } } // 寫法二:名稱不同 { type: 'FETCH_POSTS_REQUEST' } { type: 'FETCH_POSTS_FAILURE', error: 'Oops' } { type: 'FETCH_POSTS_SUCCESS', response: { ... } }
除了 Action 種類不同,非同步操作的 State 也要進行改造,反映不同的操作狀態。下面是 State 的一個例子。
let state = { // ... isFetching: true, didInvalidate: true, lastUpdated: 'xxxxxxx' };
上面程式碼中,State 的屬性isFetching
表示是否在抓取資料。didInvalidate
表示資料是否過時,lastUpdated
表示上一次更新時間。
現在,整個非同步操作的思路就很清楚了。
- 操作開始時,送出一個 Action,觸發 State 更新為"正在操作"狀態,View 重新渲染
- 操作結束後,再送出一個 Action,觸發 State 更新為"操作結束"狀態,View 再一次重新渲染
五、redux-thunk 中介軟體
非同步操作至少要送出兩個 Action:使用者觸發第一個 Action,這個跟同步操作一樣,沒有問題;如何才能在操作結束時,系統自動送出第二個 Action 呢?
奧妙就在 Action Creator 之中。
class AsyncApp extends Component { componentDidMount() { const { dispatch, selectedPost } = this.props dispatch(fetchPosts(selectedPost)) } // ...
上面程式碼是一個非同步元件的例子。載入成功後(componentDidMount
方法),它送出了(dispatch
方法)一個 Action,向伺服器要求資料 fetchPosts(selectedSubreddit)
。這裡的fetchPosts
就是 Action Creator。
下面就是fetchPosts
的程式碼,關鍵之處就在裡面。
const fetchPosts = postTitle => (dispatch, getState) => { dispatch(requestPosts(postTitle)); return fetch(`/some/API/${postTitle}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(postTitle, json))); }; }; // 使用方法一 store.dispatch(fetchPosts('reactjs')); // 使用方法二 store.dispatch(fetchPosts('reactjs')).then(() => console.log(store.getState()) );
上面程式碼中,fetchPosts
是一個Action Creator(動作生成器),返回一個函式。這個函式執行後,先發出一個Action(requestPosts(postTitle)
),然後進行非同步操作。拿到結果後,先將結果轉成 JSON 格式,然後再發出一個 Action( receivePosts(postTitle, json)
)。
上面程式碼中,有幾個地方需要注意。
(1)
fetchPosts
返回了一個函式,而普通的 Action Creator 預設返回一個物件。(2)返回的函式的引數是
dispatch
和getState
這兩個 Redux 方法,普通的 Action Creator 的引數是 Action 的內容。(3)在返回的函式之中,先發出一個 Action(
requestPosts(postTitle)
),表示操作開始。(4)非同步操作結束之後,再發出一個 Action(
receivePosts(postTitle, json)
),表示操作結束。
這樣的處理,就解決了自動傳送第二個 Action 的問題。但是,又帶來了一個新的問題,Action 是由store.dispatch
方法傳送的。而store.dispatch
方法正常情況下,引數只能是物件,不能是函式。
這時,就要使用中介軟體redux-thunk
。
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; // Note: this API requires [email protected]>=3.1.0 const store = createStore( reducer, applyMiddleware(thunk) );
上面程式碼使用redux-thunk
中介軟體,改造store.dispatch
,使得後者可以接受函式作為引數。
因此,非同步操作的第一種解決方案就是,寫出一個返回函式的 Action Creator,然後使用redux-thunk
中介軟體改造store.dispatch
。
六、redux-promise 中介軟體
既然 Action Creator 可以返回函式,當然也可以返回其他值。另一種非同步操作的解決方案,就是讓 Action Creator 返回一個 Promise 物件。
這就需要使用redux-promise
中介軟體。
import { createStore, applyMiddleware } from 'redux'; import promiseMiddleware from 'redux-promise'; import reducer from './reducers'; const store = createStore( reducer, applyMiddleware(promiseMiddleware) );
這個中介軟體使得store.dispatch
方法可以接受 Promise 物件作為引數。這時,Action Creator 有兩種寫法。寫法一,返回值是一個 Promise 物件。
const fetchPosts = (dispatch, postTitle) => new Promise(function (resolve, reject) { dispatch(requestPosts(postTitle)); return fetch(`/some/API/${postTitle}.json`) .then(response => { type: 'FETCH_POSTS', payload: response.json() }); });
寫法二,Action 物件的payload
屬性是一個 Promise 物件。這需要從redux-actions
模組引入createAction
方法,並且寫法也要變成下面這樣。
import { createAction } from 'redux-actions'; class AsyncApp extends Component { componentDidMount() { const { dispatch, selectedPost } = this.props // 發出同步 Action dispatch(requestPosts(selectedPost)); // 發出非同步 Action dispatch(createAction( 'FETCH_POSTS', fetch(`/some/API/${postTitle}.json`) .then(response => response.json()) )); }
上面程式碼中,第二個dispatch
方法發出的是非同步 Action,只有等到操作結束,這個 Action 才會實際發出。注意,createAction
的第二個引數必須是一個 Promise 物件。
看一下redux-promise
的原始碼,就會明白它內部是怎麼操作的。
export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) ? action.payload.then( result => dispatch({ ...action, payload: result }), error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); } ) : next(action); }; }
從上面程式碼可以看出,如果 Action 本身是一個 Promise,它 resolve 以後的值應該是一個 Action 物件,會被dispatch
方法送出(action.then(dispatch)
),但 reject 以後不會有任何動作;如果 Action 物件的payload
屬性是一個 Promise 物件,那麼無論 resolve 和 reject,dispatch
方法都會發出 Action。
中介軟體和非同步操作,就介紹到這裡。下一篇文章將是最後一部分,介紹如何使用react-redux
這個庫。
(完)