banner
他山之石

他山之石

手摸手撸一個簡單的Redux(三)

它提供了一個第三方擴展點,在發送一個 action 和它到達 reducer 的那一刻之間。 --- 中間件

image

本文完整代碼請查看 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 按鈕,達到預期效果。如下圖:

image

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 不變,運行項目,依次點擊三個按鈕,達到預期效果。

image

編寫 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。在瀏覽器依次點擊三個按鈕,結果如下,達到預期效果。

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。