引子
在前面界面開發的過程中,為了增強在與後端交互過程中的用户體驗,通常會顯示 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 函數即可,做到了業務層與框架/工具的解耦。