中间件与异步操作

中间件

中间件(Middleware)的概念常用于 Web 服务器框架中,例如 Node.js 的 Web 框架 Express,代表处理请求的通用逻辑代码,一个请求在经历中间件的处理后,才能到达业务逻辑代码层。多个中间件可以串联起来使用,前一个中间件的输出是下一个中间件的输入,整个处理过程就如同 “管道” 一般,如图8-1所示。

image 2024 04 24 15 44 04 699
Figure 1. 图8-1

Redux 的中间件概念与此类似,Redux 的 action 可类比 Web 框架收到的请求,reducer 可类比 Web 框架的业务逻辑层,因此,Redux 的中间件代表 action 在到达 reducer 前经过的处理程序。实际上,一个 Redux 中间件就是一个函数。Redux 中间件增强了 store 的功能,我们可以利用中间件为 action 添加一些通用功能,例如日志输出、异常捕获等。我们可以通过改造 store.dispatch 增加日志输出的功能:

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
}

上面的代码重新定义了 store.dispatch,在发送 action 前后都添加了日志输出,这就是中间件的雏形,对 store.dispatch 方法进行了改造,在发出 action 和执行 reducer 这两步之间添加其他功能。注意,实际的中间件实现方式要远比上面的示例复杂,本书不涉及这块内容。实际项目中,往往是直接使用别人写好的中间件。例如,上面介绍的日志输出功能就可以使用专门的日志中间件 redux-logger(https://github.com/evgenyrodionov/redux-logger)。为 store 添加中间件支持的代码如下:

import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import reducer from './reducers';

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

上面的代码先从 redux-logger 中引入日志中间件 logger,然后将它放入 applyMiddleware 方法中并传给 createStore,完成 store.dispatch 功能的加强。下面来看一下 applyMiddleware 这个函数做了些什么,代码如下:

import compose from './compose'

export default function applyMiddleware(...middlewares) {
    return (createStore) => (...args) => {
        const store = createStore(...args)
        let dispatch = store.dispatch
        let chain = []

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    }
}

要想完全理解这段源码并不容易,建议读者先把握主线逻辑:applyMiddleware 把接收到的中间件放入数组 chain 中,然后通过 compose(…​chain)(store.dispatch) 定义加强版的 dispatch 方法,compose 是一个工具函数,compose(f, g, h) 等价于 (…​args)⇒ f(g(h(args)))。另外需要注意,每一个中间件都接收一个包含 getState 和 dispatch 的参数对象,在利用中间件执行异步操作时,将会使用到这两个方法。

异步操作

异步操作在 Web 应用中是不可缺少的,其中最常见的异步操作是向服务器请求数据。目前,我们介绍的 Redux 的工作流是:发送 action,reducer 立即处理收到的 action,reducer 返回新的 state。这个流程并不涉及异步操作,Redux 中处理异步操作必须借助中间件的帮助。

redux-thunk(https://github.com/gaearon/redux-thunk)是处理异步操作最常用的中间件。使用 redux-thunk 的代码如下:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducer from './reducers'

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

现在定义一个异步 action 模拟向服务器请求数据:

// 异步 action
function getData(url) {
    return function (dispatch) {
        return fetch(url).then(
            response => response.json(),
            error => console.log('An error occured.', error)
        ).then(json => dispatch({type: 'RECEIVE_DATA', data: json}))
    }
}

发送这个 action:

sotre.dispatch(getData("http://xxx"));

不使用 redux-thunk 中间件时,上面的代码会报错,因为 store.dispatch 只能接收普通 JavaScript 对象代表的 action,现在使用 redux-thunk,store.dispatch 就能接收函数作为参数了。异步 action 会先经过 redux-thunk 的处理,当请求返回后,再次发送一个 action:dispatch({type:'RECEIVE_DATA',json}),把返回的数据发送出去,这时的 action 就是一个普通的 JavaScript 对象了,处理流程也和不使用中间件的流程一样。

在实际项目中,处理一个网络请求往往会使用三个 action,分别表示请求开始、请求成功和请求失败,例如:

{type: 'FETCH_DATA_REQUEST' }
{type: 'FETCH_DATA_SUCCESS', data: { ... }}
{type: 'FETCH_DATA_FAILTURE', error: 'Oops'}

使用这三个 action 改写上面的代码:

// 异步action
function getData(url) {
    return function (dispatch) {
        dispatch({type:'FETCH_DATA_REQUEST'});
        return fetch(url).then(
            response => response.json(),
            error => {
                console.log('An error occured.', error);
                dispatch({type:'FETCH_DATA_FAILURE', error});
            }
        ).then(json =>
            dispatch({type: 'FETCH_DATA_SUCCESS', data: json});
        )
    }
}

这样,应用就可以根据请求所处的阶段显示不同的 UI,例如控制 Loading 效果。除了 redux-thunk 外,常用于处理异步操作的中间件还有 redux-promise(https://github.com/acdlite redux-promise)、redux-saga(https://github.com/redux-saga/redux-saga)等,本书不再展开介绍。