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