Loading ... done

时间:2023-02-26 20:57:12

引子

在前面界面开发的过程中,为了增强在与后端交互过程中的用户体验,通常会显示 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​​ 函数即可,做到了业务层与框架/工具的解耦。