Redux 進階:中介軟體的使用
什麼是 middleware
用過 Express 或 Koa 類似框架的同學可能知道,在 Express 中,中介軟體(middleware)就是在 req 進來之後,在我們真正對 req 進行處理之前,我們先對 req 進行一定的預處理,而這個預處理的過程就由 middleware 來完成。
同理,在 Redux 中,middleware 就是擴充套件了在 dispatch action 之後,到 action 到達 reducer 之前之間的中間這段時間,而中間的這段時間就是 dispatch 的過程,所以Redux 的 middleware 的原理就是改造 dispatch 。
自定義 middleware
讓我們先從一個最簡單的日誌 middleware 定義開始:
const logger = store => next => action => { console.group('logger'); console.warn('dispatching', action); let result = next(action); console.warn('next state', store.getState()); console.groupEnd(); return result; };
這個logger
函式就是一個 Redux 中的 middleware ,它的功能是在store.dispatch(action)
(對應 middleware 中的next(action)
) 之前和之後分別打印出一條日誌。從我們的logger
中可以看到,我們向 middleware 中傳入了store
,以便我們在 middleware 中獲取使用store.getState()
獲取 state,我們還在之後的函式中傳入了next
,而最後傳入的action
就是我們平時store.dispatch(action)
中的 action,所以next(action)
對應的就是dispatch(action)
。
最後我們還需要呼叫並next(action)
來執行原本的dispatch(action)
。
使用 middleware
最後我們可以在使用createStore()
建立 store 的時候,把這個 middleware 加入進去,使得每次store.dispathc(action)
的時候都會打印出日誌:
import { createStore, applyMiddleware } from 'redux';// 匯入 applyMiddleware const store = createStore(counter, applyMiddleware(logger));
注意,這裡我們使用了 Redux 提供的applyMiddleware()
來在建立 store 的時候應用 middleware,而applyMiddleware()
返回的是一個應用了 middleware 的 store enhancer,也就是一個增強型的 store。
createStore()
接受三個引數,第一個是 reducer,第二個如果是物件,那麼就被作為 store 的初始狀態,第三個就是 store enhancer,如果第二個引數是函式,那麼就被當作 store enhancer。
關於applyMiddleware
和我們自定義的logger
是如何一起工作的,這個我們稍後再講。
為了說明後一條日誌console.warn('next state', store.getState())
是在執行了 reducer 之後打印出來的,我們在 reducer 中也列印一個訊息。改造後的 reducer:
function counter(state = 0, action) { +console.log('hi,這條 log 從 reducer 中來'); switch(action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default : return state; } }
結果
這裡,我使用了 #1 中的計數器作為例子。
可以看到,在 reducer 中列印的訊息處於 middleware 日誌的中間,這是因為在logger
middleware 中,將let result = next(action);
寫在了最後一條訊息的前面,一旦呼叫了next(action)
,就會進入 reducer 或者進入下一個 middleware(如果有的話)。類似 Koa 中介軟體的洋蔥模型。
其實next(action)
就相當於store.dispatch(action)
,意思是開始處理下一個 middleware,如果沒有 middleware 了就使用原始 Redux 的store.dispatch(action)
來分發動作。這個是由 Redux 的applyMiddleware
來處理的,那麼applyMiddleware()
是如何實現對 middleware 的處理的呢?稍後我們會對它進行簡單的講解 。
:question:applyMiddleware 是如何實現的
從applyMiddleware 的設計思路
中,我們可以看到 Redux 中的 store 只是包含一些方法(dispatch()
、subscribe()
、getState()
、replaceReducer()
)的物件。我們可以使用
const next = store.dispatch;
來先引用原始 store 中的 dispatch 方法,然後等到合適的時機,我們再呼叫它,實現對 dispatch 方法的改造。
Middleware 接收一個名為next
的 dispatch 函式(只是dispatch
函式的引用),並返回一個改造後的 dispatch 函式,而返回的 dispatch 函式又會被作為下一個 middleware 的next
,以此類推。所以,一個 middleware 看起來就會類似這樣:
function logger(next) { return action => { console.log('在這裡中一些額外的工作') return next(action) } }
其中,在 middleware 中返回的 dispatch 函式接受一個action
作為引數(和普通的 dispatch 函式一樣),最後再呼叫 next 函式並返回,以便下一個 middleware 繼續,如果沒有 middleware 則 直接返回。
由於 store 中類似getState()
的方法依舊非常有用,我們將store
作為頂層的引數,使得它可以在所有 middleware 中被使用。這樣的話,一個 middleware 的 API 最終看起來就變成這樣:
function logger(store) { return next => { return action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } }
值得一提的是,Redux 中使用到了許多函數語言程式設計的思想,如果你對
- curring
- compose
- ...
比較陌生的話,建議你先去補充以下函數語言程式設計思想的內容。applyMiddleware 的原始碼
:question:middleware 有什麼應用的場景
store.dispatch(action)
一個使用非同步 action 請求 Github API 的例子
通過仿照redux-thunk ,我們也可以自己寫一個支援非同步 action 的 middleware,如下:
const myThunkMiddleware = store => next => action => { if (typeof action === 'function') {// 如果 action 是函式,一般的 action 為純物件 return action(store.dispatch, store.getState);// 呼叫 action 函式 } return next(action); };
非同步 action creator :
export function fetchGithubUser(username = 'bbbbx') { return dispatch => { // 先 dispatch 一個同步 action dispatch({ type: 'INCREMENT', text: '載入中...' }); // 非同步 fetch Github API fetch(`https://api.github.com/search/users?q=${username}`) .then(response => response.json()) .then(responseJSON => { // 非同步請求返回後,再 dispatch 一個 action dispatch({ type: 'INCREMENT', text: responseJSON }); }); }; }
修改 reducer,使它可以處理 action 中的action.text
:
function counter(state = { value: 0, text: '' }, action) { switch(action.type) { case 'INCREMENT': return { value: state.value + 1, text: action.text }; case 'DECREMENT': return { value: state.value - 1, text: action.text }; default : return state; } }
再改造一下 Counter 元件,展示 Github 使用者:
// Counter.js class Counter extends React.Component { constructor(props) { super(props); this.state = { username: '' }; } handleChange(event) { this.setState({ username: event.target.value }); } handleSearch(event) { event.preventDefault(); if (this.state.username === '') { return ; } this.props.fetchGithubUser(this.state.username); } render() { const { text, value, increment, decrement } = this.props; let users = text; if (text.items instanceof Array) { if (text.items.length === 0) { users = '使用者不存在!'; } else { users = text.items.map(item => ( <li key={item.id}> <p>使用者名稱:<a href={item.html_url}>{item.login}</a></p> <img width={100} src={item.avatar_url} alt='item.avatar_url' /> </li> )); } } return ( <div> Click: {value} times {' '} <button onClick={increment} >+</button>{' '} <button onClick={decrement} >-</button>{' '} <div> <input type='text' onChange={this.handleChange.bind(this)} /> <button onClick={this.handleSearch.bind(this)} >獲取 Github 使用者</button>{' '} </div> <br /> <b>state.text:{users}</b> </div> ); } }
結果
使用已有的 Redux 中介軟體
redux-thunk
利用redux-thunk ,我們可以完成各種複雜的非同步 action,儘管 redux-thunk 這個 middleware 只有數十行 程式碼。先匯入 redux-thunk:
import thunkMiddleware from 'redux-thunk'; const store = createStore( counter, applyMiddleware(thunkMiddleware) );
之後便可定義非同步的 action creator 了:
export function incrementAsync(delay = 1000) { return dispatch => { dispatch(decrement()); setTimeout(() => { dispatch(increment()); }, delay); }; }
使用:
<button onClick={increment} >+</button>{' '} <button onClick={decrement} >-</button>{' '} + <button onClick={() => incrementAsync(1000) } >先 - 1 ,後再 + 1</button>{' '}
注意,非同步 action creator 要寫成onClick={() => incrementAsync(1000) }
匿名函式呼叫的形式。
結果