上一節,學習了 Redux 的基本用法:用戶發出 Action,Reducer 函數算出新的 State,View 重新渲染。
但有一個關鍵問題沒有解決:異步操作怎麼辦?Action 發出以後,Reducer 立即算出 State,這是同步;Action 發出以後,過一段時間再執行 Reducer,這就是異步。
怎麼才能 Reducer 在異步操作結束後自動執行呢?這就要用到新的工具:中間件(middleware)
一、什麼是中間件
如果我們是框架作者,要添加功能,會在哪個環節添加:
① Reducer:純函數,只承擔計算 State 的功能,不合適承擔其他功能,而且理論上純函數不能進行讀寫操作。
② View:與 State 一一對應,可以看作 State 的視覺層,也不合適承擔其他功能。
③ 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)
)。
上面代碼中,有幾個地方需要注意:
- fetchPosts 返回了一個函數,而普通的 Action Creator 默認返回一個對象。
- 返回的函數的參數是 dispatch 和 getState 這兩個 Redux 方法,普通的 Action Creator 的參數是 Action 的內容。
- 在返回的函數之中,先發出一個 Action( requestPosts(postTitle) ),表示操作開始。
- 異步操作結束之後,再發出一個 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。