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