5
头图

引子

在前面界面开发的过程中,为了增强在与后端交互过程中的用户体验,通常会显示 Loading 动画。Loading 动画会在与后端交互结束的时候关闭。这是一个很常规的需求,技术实现也不复杂。

showLoading();
axios.request(...)
    .then(...)
    .finally(() => hideLoading());

Node.js 和大部分浏览器都在 2018 年实现了对 Promise.prototype.finally() 的支持。Deno 在 2020 年发布的 1.0 中也已经支持 finally() 了。即使不支持,使用 await 也很容易处理。

showLoading()
try {
    await axios.request(...);
}
finally {
    hideLoading();
}

而在更早的时候,jQuery 在 jqXHR 中就已经通过 always() 提供了支持。

showLoading();
$.ajax(...)
    .done(...)
    .always(() => hideLoading());

拦截器中的 Loading ... done 逻辑

接下来,为了所有接口调用的行为一致,也为了在一个地方处理相同的事情以达到复用的目的,Loading ... done 的逻辑开始被写在一些拦截器中。这对单个远程接口调用来说,没有问题。但如果有这样一个业务逻辑会怎么样:

function async doSomething() {
    const token = await fetchToken();
    const auth = await remoteAuth(token);
    const result = await fetchBusiness(auth);
}

假设上面的每个调用都使用了 Axios,而 Axios 在拦截器中注入了 showLoading()hideLoading() 的逻辑。那么这段代码会依次弹出三个 Loading 动画。一个业务弹多个 Loading 动画确实是个不太好的体验。

给 Loading 记数

其实这个问题我们可以在 showLoading()hideLoading() 中去想办法。我们把这两个方法放入一个闭包环境,然后用一个变量来记录调用次数:

const { showLoading, hideLoading } = (() => {
    let count = 0;
    function showLoading() {
        count++;
        if (count > 1) { return; }
        // TODO show loading view
    }
    function hideLoading() {
        count--;
        if (count > 1) { return; }
        // TODO hide loading view
    }
})();

包装业务逻辑代替拦截器方案

作者观点

我个人并不赞同在拦截器里去处理界面上的事情。拦截器中应该处理与请求本身强相关的事情,比如对参数的预处理,对响应的后处理等。

我不太赞同在拦截器中去处理界面上的东西。像这种情况,可以设计一个 wrap 函数来处理 Loading 的呈现并调用通过参数传入的业务逻辑。这个 wrap 函数可以这样写:

async function wrapLoading(fn) {
    showLoading();
    try {
        return await fn();
    }
    finally {
        hideLoading();
    }
}

在使用的时候可以这样用:

// 单个远程调用,不带参数
await wrapLoading(fetchSomething);

// 单个远程调用,带参数
await wrapLoading(() => fetchSomething(arg1, arg2, arg3));

// 多个调用的组合逻辑
const result = await wrapLoading(() => {
    const token = await fetchToken();
    const auth = await remoteAuth(token);
    return await fetchBusiness(auth);
});

下沉包装函数降低业务处理复杂度

为了应用内更自由地统一化处理,建议对底层 Ajax 框架进行一次封装。业务远程调用时使用封装的接口,避免直接使用 Ajax 库接口。比如对 Axios request 进行一层封装。

async function request(url, config) {
    config.url = url;
    return await axios.request(config);
}

如果需要显示 Loading,可以扩展 config,加一个 withLoading 选项:

async function request(url, config) {
    const { withLoading, ...cfg } = config;
    cfg.url = url;
    
    if (!withLoading) { return await axios.request(cfg); }

    try {
        showLoading();
        return await axios.request(cfg);
    }
    finally {
        hideLoading();
    }
}

如果扩展的业务参数比较多,可以考虑封装成一个对象,比如 config.options,也可以给封装的 request 多加一个参数:request(url, config, options),这些实现都不难,就不细说了。

有了这层封装之后,如果以后想更换 Ajax 框架也相对容易,只需要修改封装的 request 函数即可,做到了业务层与框架/工具的解耦。


边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!