初識Redux Middleware

前言

原先改變store是經過dispatch(action) = > reducer;那Redux的Middleware是什麼呢?就是dispatch(action) = > reducer過程當中搞點事情,既不更改原代碼,還能擴展原有功能,這就是Redux的中間件。html

至於Redux的Middleware是怎麼演變來的,推薦去看看Redux的官網文檔,講得很不錯,諸位必定要多看幾遍。若是你發現仍是很差理解,那請你花點時間,細心看看這篇文章。文章內容比較多,但願你跟着我一步一步敲着代碼學習,這樣收穫更多,要是有什麼疑惑或者不對的地方,請指出!🤝🤝🤝node

  • 基礎環境

這裏使用create-react-app搭建環境,方便快速。注意請先自行安裝node。react

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,其他不變。ios

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中。npm

更改index.js。redux

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。axios

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,結果以下:app

不錯,如今是時候瞭解下redux的中間件了。首先,我想實現一個日誌記錄的中間件,在dispatch(action) => reducer的過程當中能打印派發的action和更改後的store。在沒有中間件時,咱們來更改下。dom

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內部實現。ide

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中間件有一個比較初步的認識。

相關文章
相關標籤/搜索