它提供了一個第三方擴展點,在發送一個 action 和它到達 reducer 的那一刻之間。 --- 中間件
本文完整代碼請查看 Github:https://github.com/YanYuanFE/redux-app
// clone repo
git clone https://github.com/YanYuanFE/redux-app.git
cd redux-app
// checkout branch
git checkout part-5
// install
npm install
// start
npm start
當我們的業務需求變得更為複雜的時候,單純的在 dispatch 和 reducer 中處理業務邏輯已經不具有普遍性。我們需要的是可以組合的,自由插拔的插件機制,redux 借鑒 koa 的中間件思想,實現了 redux 的 middleware。在發出 action 和執行 reducer 之間,使用中間件函數對 store.dispatch 進行改造,所以,redux 的 middleware 是為了增強 dispatch 而生的。
回顧中間件的使用方法,這裡以 redux-thunk 中間件為例,為之前的計數器添加一個延遲計數的功能,即點擊按鈕兩秒後進行加 1 操作,redux 和 react-redux 使用官方實現的,下面是改造後的代碼:
首先是計數器組件,src 下 components 目錄下,Counter.js 中,代碼如下:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
constructor(props) {
super(props);
}
render() {
const { value, onIncrement, onDecrement, incrementAsync } = this.props;
console.log(this.props);
return (
<p>
Clicked: {value} times
{' '}
<button onClick={onIncrement}>
+
</button>
{' '}
<button onClick={onDecrement}>
-
</button>
{' '}
<button onClick={incrementAsync}>
增加異步
</button>
</p>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired
};
export default Counter;
上述代碼中,添加了用於異步加 1 的按鈕,點擊觸發 props 中的 incrementAsync 方法。
下面是 src 下 App.js 的代碼:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Counter from './components/Counter';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
const { onIncrement, onDecrement, counter, incrementAsync } = this.props;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">歡迎來到 React</h1>
</header>
<p className="App-intro">
要開始,編輯 <code>src/App.js</code> 並保存以重新加載。
</p>
<Counter
value={counter}
onIncrement={onIncrement}
onDecrement={onDecrement}
incrementAsync={incrementAsync}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
counter: state
});
function onIncrement() {
return { type: 'INCREMENT' }
}
function onDecrement() {
return { type: 'DECREMENT' }
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
const mapDispatchToProps = {
onIncrement,
onDecrement,
incrementAsync
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
相比上一篇文章,主要定義了 incrementAsync 方法,然後添加到 mapDispatch 中,在 incrementSync 中,返回一個函數,在函數內部執行異步操作發起 action,普通的 action 都是一個對象的形式,但是異步的 action 返回的是一個函數,處理這種情況就需要使用 redux 的中間件:redux-thunk。
下面是 src 下 index.js 的代碼:
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import {applyMiddleware, createStore} from 'redux';
import counter from './reducers';
const store = createStore(counter, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
在 index.js 中,引入 react-thunk 模塊,然後在 createStore 中傳入第二個參數,使用 redux 的 API applymiddleware 對 chunk 中間件進行包裹。
上述就是改進後的計數器,運行項目在瀏覽器預覽下,點擊 incrementAsync 按鈕,達到預期效果。如下圖:
applyMiddleware 實現#
applyMiddleware 是 redux 提供的用於使用中間件的 API,回顧 applyMiddleware 的使用:
const store = createStore(counter, applyMiddleware(logger, thunk));
applyMiddleware 接收多個中間件參數,返回值作為第二個參數傳入 createStore。
在 src 下 redux.js 文件中,首先讓原來的 createStore 支持傳入第二個參數,代碼如下:
export function createStore(reducer, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer);
}
}
在 createStore 中傳入第二個參數 enhancer,enhancer 即調用 applyMiddleware 包裝中間件的函數,然後判斷 enhancer 是否存在,存在即調用 enhancer 傳入 createStore 和 reducer 兩個參數,由此,applyMiddleware 應該是一個高階函數,返回一個新的函數。
下面暴露 applyMiddleware 方法。redux 的 middleware 是支持多個中間件的,此處先實現支持一個中間件的用法。
export function applyMiddleware(middleware) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
dispatch = middleware(midApi)(store.dispatch)
return {
...store,
dispatch
}
}
}
applyMiddleware 的結構是一個多層柯里化的函數,第一層函數執行後返回一個函數,這個函數即 createStore 函數中的參數 enhancer,然後這個函數傳入 createStore 參數,再返回一個函數,這個函數傳入 reducer 參數,使用 ...args 進行解構,在函數內部,首先調用 createStore 獲取到原始的 store 以及 dispatch,然後封裝一個對象 midApi 傳入中間件內部,midApi 包括兩個方法 getState 和 dispatch,getState 對應 store.getState 方法,dispatch 對應 store.dispatch 方法並透傳參數。下面是一個日誌中間件的代碼:
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
由此可見,中間件函數是一個層層包裹的匿名函數,第一層傳入 store,第二層傳入 next 下個中間件,此處指 store.dispatch,第三層是在組件中進行調用時,傳入 action。logger 中間件在 applyMiddleware 中被層層調用,動態的對 store 和 next 參數賦值。
接著看上面 applyMiddleware 的代碼,定義了一個由 getState 和 dispatch 組成的閉包 midApi,中間件函數 middleware 第一次調用傳入 midApi 返回一個匿名函數,如下:
next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
緊接著對匿名函數再次調用,傳入 store.dispatch 作為參數 next,再次返回一個匿名函數,如下:
action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
通過對 middleware 的層層調用來生成一個新的 dispatch 方法,新的 dispatch
對 store 原有的 dispatch 方法進行了增強,最後返回一個對象,使用解構賦值將增強的 dispatch 覆蓋原有的 store.dispatch,成為一個新的 store。最終,在組件中發起 dispatch 的時候使用的就是新的 dispatch 方法。
到這裡,已經讓 redux 支持中間件的用法了,現在來使用一下,還是使用上面項目中的計數器案例,將 redux 和 react-redux 替換為自己編寫的文件,redux-thunk 不變,運行項目,依次點擊三個按鈕,達到預期效果。
編寫 redux-thunk 中間件#
上面已經讓我們的 redux 支持使用中間件,下面來嘗試自己編寫一個 thunk 中間件。在 src 下新建 thunk.js:
const thunk = ({dispatch, getState}) => next => action => {
return next(action)
}
export default thunk;
一個中間件的結構如上,一個三層箭頭函數,第一个參數傳入 midApi,這裡解構出 dispatch 和 getState 方法方便使用,第二個參數傳入下個中間件,即 store.dispatch,第三個參數傳入需要提交的 action。在中間件函數中,如果什麼都不做,直接返回 next(action),即直接 dispatch action。當然,這樣做並沒有什麼用,下面為它加上 thunk 的代碼:
const thunk = ({dispatch, getState}) => next => action => {
// 如果是函數,執行一下
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action)
}
export default thunk;
這就是 redux-thunk 的代碼,很簡單吧,首先判斷傳入的 action 是否為 function,如果是 function 就執行該 action,並傳入 dispatch 和 getState,如下,在 incrementAsync 的返回函數的參數中,可以接受 dispatch 和 getState 兩個參數:
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
下面來驗證一下實現的 thunk 中間件,在 src 下替換 redux-thunk 為 ./thunk。在瀏覽器依次點擊三個按鈕,結果如下,達到預期效果。