banner
他山之石

他山之石

手摸手撸一个简单的Redux(三)

It provides a third-party extension point between dispatching an
action, and the moment it reaches the 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}>
        Increment async
        </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">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </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 返回的是一个 function,处理这种情况就需要使用 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 是否为 fuction,如果是 function 就执行该 action,并传入 dispatch 和 getState, 如下,在 incrementAsync 的返回函数的参数中,可以接受 dispatch 和 getState 两个参数:

function incrementAsync() {
  return (dispatch, getState) => {
    setTimeout(() => {
      dispatch(onIncrement());
    }, 2000)
  }
}

下面来验证一下实现的 thunk 中间件,在 src 下替换 redux-thunk 为./thunk。在浏览器依次点击三个按钮,结果如下,达到预期效果。

image

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。