浅谈 Redux 应用如何处理 API 请求

本文创作于 2016 年 5 月 5 日

官方的例子

Redux 官方给的例子里面调用 API 请求的地方建议采用 redux-thunk Middleware 来处理。

官方给了几个例子,初级的、进阶的,我们这儿就此处的问题进行一些探讨

关注的点

  1. 统一的入口

  2. 异常处理,如:400、500、401、403 等

  3. 通用逻辑处理

  4. 转换数据格式

  5. 对测试友好,移除对后端的依赖

Note
ES7 草案中的 async/await 对代码的可读性、健壮性提升非常大,我们讨论的方案就是基于 async/await 语言特性展开的。

统一的入口

收敛 API 入口好处非常多,因为有很多工作每个 API 都需要做,常见的有认证、异常处理、API 性能统计、超时设置等等。在 jQuery 时代,这些都采用 Hooks 来实现的,通过 setupAjax 函数,将一些钩子插入到 ajax 的流程中。

可是,如果我们不再采用 jQuery 而是使用 fetch API 呢?在工程里面抽象出一个 apis 层也是非常有必要的,它是连接后端数据与前端 Model 的通道,如果工程里本身就有一个统一的调用入口就可以不再需要依赖诸如第三方库的 Hooks 等来实现类似的功能。

一种方案就是在 fetch API 的基础上封装一个 fetch 函数,在这个函数里面来做一些统一的事情。

封装过的 fetch API
const globalSettings = {};
async function newFetch(url, options, settings) {
    if (globalSettings.hooks) {
        // process global hooks
    }

    try {
        return await fetch(url, options);
    } catch (e) {
        // process exceptions
    }
}

异常处理

API 的请求过程会出现很多问题,譬如超时、认证失败、服务器错误等,这些错误对每一个 API 都可能会出现,通常处理逻辑如下示例:

封装过的 fetch API
const globalSettings = {};
async function newFetch(url, options, settings) {
    if (globalSettings.hooks) {
        // process global hooks
    }
    if (settings.hooks) {
        // process custom hooks
    }

    try {
        const response = await fetch(url, options);

    } catch (e) {
        // process exceptions
        return null;
    }

    if (response.status < 400) {
        return response;
    }

    // 错误处理
    switch (response.status) {
        case 401:
            // 提示认证问题,提示登录
            break;
        case 400:
            // 输入错误
        case 500:
            // 服务器内部错误
        default:
            return null;
    }
}

如果每个 API 都需要这样搞一下,那真是太悲剧了,违反了 DIY 不说,也及其不方便。得益于统一的 API 入口,我们不再需要每个 API 都『拷贝』一下类似的代码。

但是类似的实现也并不是最优的,我们希望错误会中断正常的业务逻辑,否则就需要不断地在业务层对接口的返回值进行判断,if (error) { return error; } 类似的代码在 nodejs 里并不罕见,每一层都需要处理这些返回值。

Javascript 是支持异常的语言,ES7 草案中的 async/await 又解决了回调的异常问题,现在这个问题完全可以非常优雅地解决。在异常处理里面,通用的处理逻辑通常需要在最外层,这样才可以将整个过程中的异常捕获住,并中断正常业务的执行过程,也就是说,我们需要将 try/catch 放到非常外层的地方。

Redux 中做这种事情的最合适地方是 middleware,因为每个 action 都会经过 middleware,它可以处理 action 的执行方式,处理 action 的结果,甚至可以忽略 action。

为了实现类似的效果,我们还需要将错误转换为异常。

class HTTPException extends Exception {
    constractor(response) {
        super();
        this.response = response;
    }
}

基于上面的 HTTPException,我们的 middileware 示例如下:

middleware 的示例
const process_exceptions = (store) => (next) => async (action) => {
    try {
        async action(store.dispatch, store.getState);
    } catch (exc) {
        // do some logging jobs
        // such as sentry

        if (exc instanceof HTTPException) {
            // process HTTPExceptions
        }
    }
}

fetch API 将改造为:

封装过的 fetch API
const globalSettings = {};
async function newFetch(url, options, settings) {
    // 预处理

    // 不再需要 catch 掉异常了,最外层统一处理
    const response = await fetch(url, options);

    if (response.status >= 400) {
        // 将错误转换为异常
        throw new HTTPException(response);
    }

    return await response.json();
}

通用逻辑处理

通用逻辑处理也是 API 请求过程中经常会遇到的问题,诸如:处理 HTTP HEADERS、OAuth2.0 token、使用代理等。在我们的 API 设计中也有一些通用的约定,API 返回值里面约定了几个通用的字段,entitiesrelationshipsviews,前者按照 Identity Map 格式来组织领域实体,并将它们传递给 WebApp,因此这种逻辑可以统一处理,将 API 返回数据转化为 WebApp 需要的 Immutable Model,并存储到 store 的 state 里面。views 是每个 API 特别的域。

统一处理 entities
const globalSettings = {};
async function newFetch(url, options, settings, dispatch) {
    // 预处理

    // 不再需要 catch 掉异常了,最外层统一处理
    const response = await fetch(url, options);

    if (response.status >= 400) {
        // 将错误转换为异常
        throw new HTTPException(response);
    }

    const json = await response.json();
    const { entities } = json;
    if (entities) {
        dispatch(receiveEntities(entities));
    }
    return json.views;
}

// reducer
function entities(state = new IdentityMap(), action) {
    switch (action.type) {
        case RECEIVE_ENTIEIS:
            return state.updateWith(action.entities);
        default:
            return state;
    }
}

// action
function receiveEntities(entities) {
    return { type: RECEIVE_ENTIEIS, entities };
}

这样改造完的 fetch API 就需要每次都传入 dispatch 函数,使用非常不方便,好在 dispatch 对于每个 store 是唯一的。

通过IoC 解决 dispatch 问题
const globalSettings = {};
let $dispatch = null;
function setDispatch(dispatch) {
    $dispatch = dispatch;
}

async function newFetch(url, options, settings) {
    // 预处理

    // 不再需要 catch 掉异常了,最外层统一处理
    const response = await fetch(url, options);

    if (response.status >= 400) {
        // 将错误转换为异常
        throw new HTTPException(response);
    }

    const json = await response.json();
    const { entities } = json;
    if (entities) {
        $dispatch(receiveEntities(entities));
    }
    return json.views;
}

虽然几个简单的函数能解决一些类似的问题,但不太优雅,扩展性也差,考虑后面我们需要分离测试,封装一个访问类是一个比较好的方案。

class APIManager {
    constractur(dispatch, settings) {
        this.dispatch = dispatch;
        this.settings = settings;
    }
    fetch(url, options) {
        //
    }
}
Note
reducer entities 可以将实体全部都转换成 Immutable 的 Model,可以解决多种数据结构不对应的问题,诸如:API 是普通的 Javascript 对象,而 state 里又是 Immutable 对象。

对测试友好

『依赖依赖 UI 设计和后端提供 API』这可能是前端研发抱怨比较多的两个问题,前者不在本文讨论的范畴,后者倒是可以在引入 API 层后得到解决。 使用依赖注入,让 API 层逻辑更方便地根据环境的不同而改变,在生产环境使用真实的 API 请求逻辑,而在开发、测试环境下使用的是 fake 的 API 层逻辑。

通过IoC 解决 dispatch 问题
let $apiMgr = null;
function setAPIManager(apiMgr) {
    $apiMgr = apiMgr;
}
function getAPIManager() {
    return $apiMgr;
}

if (process.env['NODE_ENV'] === 'production') {
    setAPIManager(new APIManager());
} else {
    setAPIManager(new FakeAPIManager());
}
Note
可以更好地做单元测试,持续集成。

results matching ""

    No results matching ""