http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
Redux 入門教程(二):中間件與異步操作
上一篇文章,我介紹了 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 redux@>=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
這個庫。