redux 中间件与异步操作

1,093 阅读4分钟

前言

经过上一篇文章(juejin.cn/post/684490…) 的学习,我们已经初步掌握了 Redux 的基本用法,并通过在原生 JavaScript 中的一个简单的例子加深了印象。但是还有个关键的问题没有解决:异步操作怎么办? Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。这就要用到新的工具:中间件(middleware)

对于有服务端编程经验的开发者来说,对于 中间件(middleware)这个名词并不陌生。顾名思义,中间件作为中间设备、中间桥梁,它可以连接两种事务或服务。比如对于服务端来说,中间件经常被嵌入在从框架接收请求到产生相应的过程中。因此在这个环节,我们可以利用中间件完成扩展以适应不同的业务需求。

中间件的概念

Redux 提供的是位于 action 被派发之后,到达 reducer 之前的扩展点,因此我们可以利用 Redux 中间件来完成日志记录、调用异步接口或者路由等。

在 Redux 架构中可以接入多个中间件,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联,派发给 redux store 的 action 对象,会被 store 上的多个中间件依次处理,middleware 在 Redux 的数据流中所处的位置如下图所示

了解了中间件的概念之后,我们来认识一些常用的中间件,并加以应用

如何接入中间件

Redux 本身提供了 applyMiddleware 方法来接入中间件,首先来看下创建 store 实例的代码:

const store = createStore(reducer, preloadedState, enhancer);
  • reducer: 必传参数,为开发者编写的 reducer 函数
  • preloadedState: 可选参数,页面的初始状态数据树
  • enhancer: 可选参数,增强器

我们可以在 enhancer 参数的位置接入中间件,createStore 方法可以接收 applyMiddleware(...middlewares) 作为参数,创建一个应用了中间件之后的 store 。如下代码所示:

const store = createStore(
    reducer,
    preloadState,
    applyMiddleware(middleware)
);

其中 preloadState 可以省略,也可以如下方式:

const store = createStore(
    reducer,
    applyMiddleware(middleware)
);

redux-logger 中间件

利用 redux-logger 中间件来进行日志记录。每一次派发前后的页面数据状态,以及当前所派发的 action,都能在浏览器开发面板中打印出来,方便开发者调试。

1. 引入 redux-logger 中间件

import createLogger from 'redux-logger';

2. 接入 redux-logger 中间件

import { applyMiddleware, createStore } from 'redux';
const logger = createLogger();

const store = {
    reducer,
    applyMiddleware(logger)
};

3. 在控制台查看

每次派发 action 后都可以在控制台看到改变前后的状态

redux-thunk 中间件

假如有一个异步需求,比如需要派发一个网络请求 action,在网络请求返回之后,再派发一个 action 用来根据返回的数据渲染页面。

如果先派发一个action,再派发一个处理请求返回结果的 action,这样显然是不行的,因为它们是同步进行的。设想一下:如果 dispatch 可以接受一个函数为参数,在函数体内进行异步操作,并在异步完成之后再派发相应的 action,那么便能解决问题了。这就是 redux-thunk 中间件所解决的问题。

1. 引入 redux-thunk 中间件

import thunk from 'redux-thunk';
const store = {
     reducer,
     applyMiddleware(thunk)
 };

thunk 的使用

// 编写 reducer 函数
function reducer(state = initialState, action) {
    switch(action.type) {
        case 'GET_INFO_START':
            return {info: '开始加载'}
        case 'GET_INFO_SUCCESS':
            return {info: action.data}
        default:
            return {info: '未加载'}
    }
}

function getInfo() {
    return dispatch => {
        dispatch({type: 'GET_INFO_START', data: ''});
        fetch('https://jsonplaceholder.typicode.com/posts/1').then((response) => {
            dispatch({type: 'GET_INFO_SUCCESS', data: response.url});
        })
    }
}

// 调用
store.dispatch(getInfo())

不难发现,redux-thunk 对于异步处理的关键在于:使 dispatch 能够接收异步函数,之后变得灵活起来,我们可以控制 dispatch 相应 action 的时机。

redux-promise 中间件的使用

另一种异步操作的解决方案,就是让 dispatch 接收一个 Promise 对象,这就需要使用redux-promise中间件。

1. 引入 redux-promise 中间件

import promiseMiddleware from 'redux-promise';
const store = {
     reducer,
     applyMiddleware(promiseMiddleware)
 };

2. redux-promise 的应用

// 编写 reducer 函数
function reducer(state = initialState, action) {
    switch(action.type) {
        case 'GET_INFO_START':
            return {info: '开始加载'}
        case 'GET_INFO_SUCCESS':
            return {info: action.payload}
        default:
            return {info: '未加载'}
    }
}

const fetchData = () => fetch('https://jsonplaceholder.typicode.com/posts/1');
async function getWeather() {
    const result = await fetchData()
    if (result.error) {
        return {
            type: 'GET_INFO_ERROR', payload: result.error,
        }
    }
    return {
        type: 'GET_INFO_SUCCESS', payload: result.url,
    }
}

// 触发
store.dispatch(getWeather())

3. redux-promise源码

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}