初識Redux的Middleware
前言
原先改變store是通過dispatch(action) = > reducer;那Redux的Middleware是什麼呢?就是dispatch(action) = > reducer過程中搞點事情,既不更改原始碼,還能擴充套件原有功能,這就是Redux的中介軟體。
至於Redux的Middleware是怎麼演變來的,推薦去看看 Redux的官網文件 ,講得很不錯,諸位一定要多看幾遍。如果你發現還是不好理解,那請你花點時間,細心看看這篇文章。文章內容比較多,希望你跟著我一步一步敲著程式碼學習,這樣收穫更多,要是有什麼疑惑或者不對的地方,請指出!
- 基礎環境
這裡使用create-react-app搭建環境,方便快速。注意請先自行安裝node。
sudo npm i -g create-react-app (如果已經安裝過,請忽略;期間根據提示輸入密碼即可。window使用者執行npm i -g create-react-app) create-react-app redux-middleware cd redux-middleware yarn add redux react-redux mockjs axios(create-react-app預設使用yarn作為包管理,這裡就照著用) // 下面是版本號 "dependencies": { "axios": "^0.18.0", "mockjs": "^1.0.1-beta3", "react": "^16.7.0", "react-dom": "^16.7.0", "react-redux": "^6.0.0", "react-scripts": "2.1.3", "redux": "^4.0.1" }
- 一個小例子
為了方便,使用mock攔截,模仿後臺介面,根據輸入內容,返回模擬資料。現在改寫App.js,其餘不變。
import React, { Component } from 'react'; import axios from 'axios'; import Mock from 'mockjs'; Mock.mock('http://test.com/search', { 'list|0-5': [{ 'id|+1': 1, name: '@character("lower")', 'version': '@float(1, 10, 2, 2)', publisher: '@cname' }] }); class App extends Component { state = { data: [], searchValue: '' }; handleSearch = e => { e.preventDefault(); if (this.state.searchValue) { axios.get(`http://test.com/search`).then(result => { if (result.status === 200) { this.setState({ data: result.data.list.map(item => ({...item, name: `${this.state.searchValue}${item.name}`})) }); } }) } }; changeValue = e => { this.setState({ searchValue: e.target.value }); }; render() { return ( <div style={{ textAlign: 'center', margin: '40px' }}> <form onSubmit={this.handleSearch}> <input type="text" value={this.state.searchValue} onChange={this.changeValue} /> <button type="submit">搜尋</button> </form> <ul> {this.state.data.map(item => ( <li key={item.id} style={{ listStyle: 'none' }}> <p>{item.name}</p> <p> {item.publisher} publish {item.version} </p> </li> ))} </ul> </div> ); } } export default App;
- 開始redux中介軟體之旅
現在將App元件與redux關聯起來,資料存入state中。
更改index.js。
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import App from './App'; import * as serviceWorker from './serviceWorker'; function listReducer(state = { list: [] }, action) { switch (action.type) { case 'receive': return { list: action.data }; default: return state; } } const store = createStore(listReducer); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')); serviceWorker.unregister();
更改App.js。
import React, { Component } from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; import Mock from 'mockjs'; Mock.mock('http://test.com/search', { 'list|0-5': [{ 'id|+1': 1, name: '@character("lower")', 'version': '@float(1, 10, 2, 2)', publisher: '@cname' }] }); class App extends Component { state = { searchValue: '' }; handleSearch = e => { e.preventDefault(); if (this.state.searchValue) { axios.get(`http://test.com/search`).then(result => { if (result.status === 200) { this.props.changeList(result.data.list.map(item => ({...item, name: `${this.state.searchValue}${item.name}`}))); } }) } }; changeValue = e => { this.setState({ searchValue: e.target.value }); }; render() { return ( <div style={{ textAlign: 'center', margin: '40px' }}> <form onSubmit={this.handleSearch}> <input type="text" value={this.state.searchValue} onChange={this.changeValue} /> <button type="submit">搜尋</button> </form> <ul> {this.props.list.map(item => ( <li key={item.id} style={{ listStyle: 'none' }}> <p>{item.name}</p> <p> {item.publisher} publish {item.version} </p> </li> ))} </ul> </div> ); } } function mapStateToProps(state) { return { list: state.list } } function mapDispatchToProps(dispatch) { return { changeList: function (data) { dispatch({ type: 'receive', data }); } } } export default connect(mapStateToProps, mapDispatchToProps)(App);
測試一下,我們搜尋rxjs,結果如下:
不錯,現在是時候瞭解下redux的中介軟體了。首先,我想實現一個日誌記錄的中介軟體,在dispatch(action) => reducer的過程中能列印派發的action和更改後的store。在沒有中介軟體時,我們來更改下。
function MapStateToProps(state) { console.log('nextState: ', state); return { list: state.list } } function mapDispatchToProps(dispatch) { return { changeList: function (data) { const action = { type: 'receive', data }; console.log('dispatch: ', action); dispatch(action); } } }
很可惜,雖然實現了,但在元件初始化時,卻列印了初始化的state。原因在於mapStateToProps方法無法判斷是初始化返回的資料還是dispatch(action) => reducer引發的state更改。當然這裡你可以用一個變數去儲存是不是初始化元件(即第一次呼叫mapStateToProps),但是加入了額外的開銷不說,還手動更改了mapStateToProps和mapDispatchToProps方法,程式碼一下子不好看了。這該怎麼辦呢?先回溯到更改state過程:dispatch(action) => reducer => state。由於reducer是一個純函式,只要函式引數唯一,返回結果必定唯一。通過reducer來實現日誌當然可以,但是感覺reducer不純了。你本來只負責生成新state,不能有自己的小心思。那隻好把目光放到dispatch上,先來看看dispatch內部實現。
function dispatch(action) { ...// 資料校驗 try { isDispatching = true currentState = currentReducer(currentState, action) // 哈哈,新state在此 } finally { isDispatching = false } const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }
dispatch是store提供的一個方法,要訪問只能在呼叫dispatch時做些文章。那我試試替換dispatch方法呢?
function logMiddleware(action) { console.log('dispatch: ', action); dispatch(action); console.log('nextState: ', currentState); } return { dispatch: logMiddleware, subscribe, getState, replaceReducer, [$$observable]: observable }
這樣明目張膽改了原始碼不好,換一種方式。更改index.js。
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import App from './App'; import * as serviceWorker from './serviceWorker'; function listReducer(state = { list: [] }, action) { switch (action.type) { case 'receive': return { list: action.data }; default: return state; } } const store = createStore(listReducer); let next = store.dispatch; store.dispatch = function logMiddleware(action) { console.log('dispatch: ', action); next(action); console.log('nextState: ', store.getState()); }; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')); serviceWorker.unregister();
刪除App.js中我們加入的日誌記錄程式碼。再次搜尋如下:
這樣乍一看好像可行,如果再加一箇中間件呢?再次更改index.js。
... let next = store.dispatch; store.dispatch = function logMiddleware(action) { console.log('dispatch: ', action); next(action); console.log('nextState: ', store.getState()); }; let next2 = store.dispatch; // 這裡的store.dispatch是logMiddleware store.dispatch = function logMiddleware2(action) { console.log('logMiddleware2 start'); next2(action); console.log('logMiddleware2 end'); }; ...
糟糕,每次加入一箇中間件都得用個變數去存嗎?這樣的程式碼太滑稽了,那該如何優雅的獲取上一個中介軟體呢?換個角度思考下,如果將中介軟體作為引數傳遞,那效果是不是不一樣呢?更改後的index.js如下:
... function logMiddleware(dispatch, action) { console.log('dispatch: ', action); let next = dispatch(action); console.log('nextState: ', store.getState()); return next; } function logMiddleware2(dispatch, action) { console.log('logMiddleware2 start'); let next = dispatch(action); console.log('logMiddleware2 end'); return next; } let dispatch = logMiddleware2(logMiddleware(store.dispatch, action), action); store.dispatch = dispatch; ...
這裡action這樣傳遞是有問題的,得琢磨琢磨。既然是dispatch(action),那中介軟體返回一個函式,函式的引數就是action呢?修改index.js如下:
... function logMiddleware(dispatch) { return function (action) { console.log('dispatch: ', action); let next = dispatch(action); console.log('nextState: ', store.getState()); return next; } } function logMiddleware2(dispatch) { return function (action) { console.log('logMiddleware2 start'); let next = dispatch(action); console.log('logMiddleware2 end'); return next; } } let dispatch = logMiddleware2(logMiddleware(store.dispatch)); store.dispatch = dispatch; ...
Yes,we can!!!但是細看程式碼還是不完美,logMiddleware中store是直接獲取的,嚴重耦合在一起。如果將logMiddleware單獨放入一個模組檔案中,程式碼就不能正常運行了。那還不簡單,將store匯出,再匯入到logMiddleware模組中不就完了。可是這樣還是嚴重耦合,只是換了一種方式而已(你寫的中介軟體應該是其他小夥伴拿來即用的,不應該有其他騷操作)。騷年,還得再想想辦法。index.js,不要抱怨,還得再改改你。
... function logMiddleware(store) { return function (dispatch) { return function (action) { console.log('dispatch: ', action); let next = dispatch(action); console.log('nextState: ', store.getState()); return next; } } } function logMiddleware2(store) { return function (dispatch) { return function (action) { console.log('logMiddleware2 start'); let next = dispatch(action); console.log('logMiddleware2 end'); return next; } } } let dispatch = logMiddleware2(store)(logMiddleware(store)(store.dispatch)); store.dispatch = dispatch; ...
我們可以發現logMiddleware中dispatch是store.dispatch,logMiddleware2中的dispatch是logMiddleware中介軟體。既然如此,那換個名稱,以免誤會。這裡統一改成next。最後let dispatch = ...只是為了讓大家看懂過程,現在也改一下。
... function logMiddleware(store) { return function (next) { return function (action) { console.log('dispatch: ', action); let result = next(action); console.log('nextState: ', store.getState()); return result; } } } function logMiddleware2(store) { return function (next) { return function (action) { console.log('logMiddleware2 start'); let result = next(action); console.log('logMiddleware2 end'); return result; } } } const middlewares = [logMiddleware2, logMiddleware]; const chain = middlewares.map(middleware => middleware(store)); const chains = chain.reduce((a, b) => (...args) => a(b(...args))); let dispatch = chains(store.dispatch); store.dispatch = dispatch; ...
講到這裡,基礎也差不多講完了,希望你能對redux中介軟體有一個比較初步的認識。