16

使用Redux访问服务器,同样要要解决异步问题。

Redux单向数据流,由action对象开始驱动,每个action对象被派发到Store之后,被分配给reducer函数,reducer完成数据操作后立刻返回,reducer返回的结果又被拿去更新Store上的状态数据,更新状态数据的操作立刻会被同步给监听Store状态改变的函数,从而引发React视图组件的更新过程。

Redux单向数据流

整个过程都是马不停蹄一路同步执行,根本没有异步操作的机会,那应该在 哪里插入访问服务器的异步操作呢?

redux-thunk中间件

redux-thunk中间件就是解决redux异步操作的标准方式。

npm install redux-thunk --save

异步actoin对象

Redux单向数据流的驱动起点是action对象,Redux异步操作也避免不了从派发一个action对象开始。但是这个action对象比较特殊,我们叫它“异步action对象”。

与普通action对象(包含若干字段,其中type必不可少)不同的是,“异步action对象”不是一个普通的JavaScript对象,而是一个函数。

这样一个函数类型的action对象派发出去,由于没有type字段,就没有下一步的reducer什么事了。但reducer又不得不按redux数据流的步骤自动介入进来。所以中间件在此时机站出来,认定这件事非他管不可的话,reducer就得一边凉快去。

所以,redux-thunk的工作就是检查action对象是不是函数,如果不是就撤退。而如果是的话,就执行这个函数,并把Store的dispatch函数和getState函数作为参数传递进去。

寥寥几行的redux-thunk源代码:

function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
        if(typeof action == 'function') {
            return action(dispatch, getState, extraArgument);
        }
    };
}

可以很清楚地看到,当actoin为函数时,并没有调用next或dispatch方法,而是返回action函数的调用。

了解到redux-thunk的原理后,我们模拟一个天气的异步请求。action creator通常可以这么写:

function getWeather(url, params) {
    return (dispatch, getState) => { // 由中间件负责调用,dispatch和getState也由中间件负责传入
        fetch(url, params)
            .then(result => {
                dispatch({
                    type: 'GET_WEATHER_SUCCESS',
                    payload: result
                });
            })
            .catch(err => {
                dispatch({
                    type: 'GET_WEATHER_ERROR',
                    error: err
                });
            });
    };
}

异步action函数的代码基本都是这样的套路:

export const sampleAsyncAction = () => {
    return (dispatch, getState) => {
        // 在这个函数里可以调用异步函数, 自行决定再合适的时机通过dispatch参数派发新的action对象
    }
};

这就是异步action的工作机理,异步action最终还是要产生同步actoin的派发,才能触达视图的响应。redux-thunk要做的工作也就不过如此,但因为引入了一次函数执行,而这个函数还能访问到dispatch和getState,就给异步操作带来了可能。

异步action函数中,可以通过ajax发起对服务器的异步请求,当得到结果之后,通过参数dispatch,把成功或失败的结果当做actoin对象再派发出去。这一次派发的是普通action对象,就不会被redux-thunk截获,直接到达reducer,最终驱动Store上状态的改变。

redux-promise中间件

我们发现,异步请求其实都是利用promise来完成的,那么为什么不直接通过抽象promise来解决异步流问题呢?

npm install redux-promise --save

通过源码分析一下它是怎么做的:

import { isFSA } from 'flux-standard-action';

function isPromise(val) {
    return val && typeof val.then === 'function';
}

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);
    };
}

redux-promise兼容了FSA标准,也就是说将返回的结果保存在payload中。实现过程非常容易理解,即判断action或action.payload是否为promise,如果是,就执行then,返回的结果再发送一次dispatch。

我们利用ES7的async和await语法,可以简化上述获取天气的异步过程:

const fetchData = (url, params) => fetch(url, params);

async function getWeather(url, params) {
    const result = await fetchData(url, params);
    
    if(result.error) {
        return {
            type: 'GET_WEATHER_ERROR',
            error: result.error
        };
    }
    
    return {
        type: 'GET_WEATHER_SUCCESS',
        payload: result
    };
}

redux-composable-fetch

在实际中,我们还需要加上loading状态。结合上述讨论的两个开源middleware,我们完全可以自己实现一个贴合工程需要的middleware,这里将其命名为redux-composable-fetch。

在理想的情况下,我们不希望通过复杂的方法去请求数据,而希望通过如下形式一并完成在异步请求过程中的不同状态:

{
    url: '/api/weather.json',
    params: {
        city: encodeURI(city)
    },
    types: ['GET_WEATHER', 'GET_WEATHER_SUCCESS', 'GET_WEATHER_ERROR']
}

可以看到,异步请求的action格式有别于FSA。它并没有使用type属性,而使用了types属性。types其实是三个普通action type的集合,分别代表请求中、请求成功和请求失败。

在请求middleware中,会对action进行格式检查,若存在url和types属性,则说明这个action是一个用于发送异步请求的action。此外,并不是所有请求都能携带参数,因此params是可选的。

当请求middleware识别到这是一个用于发送请求的action后,首先会分发一个新的action,这个action的type就是原action里types数组中的第一个元素,即请求中。分发这个新action的目的在于让store能够同步当前请求的状态,如将loading状态置为true,这样在对应的界面上可以展示一个友好的加载中动画。

然后请求middleware会根据action中的url、params、method等参数发送一个异步请求,并在请求响应后根据结果的成功或失败分别分发请求成功和请求失败的新action。

请求middleware的简化实现如下,我们可以根据具体的场景对此进行改造:

const fetchMiddleware = store => next => action => {
    if(!action.url || !Array.isArray(action.types)) {
        return next(action);
    }
    
    const [LOADING, SUCCESS, ERROR] = action.types;
    
    next({
        type: LOADING,
        loading: true,
        ...action
    });
    
    fetch(action.url, { params: action.params })
        .then(result => {
            next({
                type: SUCCESS,
                loading: false,
                payload: result
            });
        })
        .catch(err => {
            next({
                type: ERROR,
                loading: false,
                error: err
            });
        });
};

这样我们一步就完成了异步请求的action。

redux-observable

在Redux中,处理异步action的方法非常多,最标准的做法是使用redux-thunk中间件,经过thunk中间件的处理,一个action被dispatch后可以返回一个函数,这个函数可以用来做其他的事:发起异步请求和dispatch另外更多的action。使用redux-promise比redux-thunk更加易用,复杂度也不高,创建的异步action对象符合FSA标准。

在Redux社区中,负有盛名的还有redux-sage、redux-observable等。

redux-observable,是通过创建epics中间件,为每一个dispatch添加相应的附加效果。


zhutianxiang
1.5k 声望327 粉丝