Loading ... done

邊城發表於2023-02-27

引子

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

相關文章