前後分離模型之封裝 Api 調用 原

Ajax 和異步處理

調用 API 訪問數據採用的 Ajax 方式,這是一個異步過程,異步過程最基本的處理方式是事件或回調,其實這兩種處理方式實現原理差不多,都需要在調用異步過程的時候傳入一個在異步過程結束的時候調用的接口。比如 jQuery Ajax 的 success 就是典型的回調參數。不過使用 jQuery 處理異步推薦使用 Promise 處理方式。

Promise 處理方式也是通過註冊回調函數來完成的。jQuery 的 Promise 和 ES6 的標準 Promise 有點不一樣,但在 then 上可以兼容,通常稱爲 thenable。jQuery 的 Promise 沒有提供 .catch() 接口,但它自己定義的 .done().fail().always() 三個註冊回調的方式也很有特色,用起來很方便,它是在事件的方式來註冊的(即,可以註冊多個同類型的處理函數,在該觸發的時候都會觸發)。

當然更直觀的一點的處理方式是使用 ES2017 帶來的 async/await 方式,可以用同步代碼的形式來寫異步代碼,當然也有一些坑在裏面。對於前端工程師來說,最大的坑就是有些瀏覽器不支持,需要進行轉譯,所以如果前端代碼沒有構建過程,一般還是就用 ES5 的語法兼容性好一些(jQuery 的 Promise 是支持 ES5 的,但是標準 Promise 要 ES6 以後纔可以使用)。

關於 JavaScript 異步處理相關的內容可以參考

自己封裝工具函數

在處理 Ajax 的過程中,雖然有現成的庫(比如 jQuery.ajax,axios 等),它畢竟是爲了通用目的設計的,在使用的時候仍然不免繁瑣。而在項目中,對 Api 進行調用的過程幾乎都大同小異。如果設計得當,就連錯誤處理的方式都會是一樣的。因此,在項目內的 Ajax 調用其實可以進行進一步的封裝,使之在項目內使用起來更方便。如果接口方式發生變化,修改起來也更容易。

比如,當前接口要求使用 POST 方法調用(暫不考慮 RESTful),參數必須包括 action,返回的數據以 JSON 方式提供,如果出錯,只要不是服務器異常都會返回特定的 JSON 數據,包括一個不等於 0 的 code 和可選的 message 屬性。

那麼用 jQuery 寫這麼一個 Ajax 調用,大概是這樣

const apiUrl = "http://api.some.com/";

jQuery
    .ajax(url, {
        type: "post",
        dataType: "json",
        data: {
            action: "login",
            username: "uname",
            password: "passwd"
        }
    })
    .done(function(data) {
        if (data.code) {
            alert(data.message || "登錄失敗!");
        } else {
            window.location.assign("home");
        }
    })
    .fail(function() {
        alert("服務器錯誤");
    });

初步封裝

同一項目中,這樣的 Ajax 調用,基本上只有 data 部分和 .done 回調中的 else 部分不同,所以進行一次封裝會大大減少代碼量,可以這樣封裝

function appAjax(action, params) {
    var deffered = $.Deferred();

    jQuery
        .ajax(apiUrl, {
            type: "post",
            dataType: "json",
            data: $.extend({
                action: action
            }, params)
        })
        .done(function(data) {
            // 當 code 爲 0 或省略時,表示沒有錯誤,
            // 其它值表示錯誤代碼
            if (data.code) {
                if (data.message) {
                    // 如果服務器返回了消息,那麼向用戶呈現消息
                    // resolve(null),表示不需要後續進行業務處理
                    alert(data.message);
                    deffered.resolve();
                } else {
                    // 如果服務器沒返回消息,那麼把 data 丟給外面的業務處理
                    deferred.reject(data);
                }
            } else {
                // 正常返回數據的情況
                deffered.resolve(data);
            }
        })
        .fail(function() {
            // Ajax 調用失敗,向用戶呈現消息,同時不需要進行後續的業務處理
            alert("服務器錯誤");
            deffered.resolve();
        });

    return deferred.promise();
}

而業務層的調用就很簡單了

appAjax("login", {
    username: "uname",
    password: "passwd"
}).done(function(data) {
    if (data) {
        window.location.assign("home");
    }
}).fail(function() {
    alert("登錄失敗");
});

更換 API 調用接口

上面的封裝對調用接口和返回數據進行了統一處理,把大部分項目接口約定的內容都處理掉了,剩下在每次調用時需要處理的就是純粹的業務。

現在項目組決定不用 jQuery 的 Ajax,而是採用 axios 來調用 API(axios 不見得就比 jQuery 好,這裏只是舉例),那麼只需要修改一下 appAjax() 的實現即可。所有業務調用都不需要修改。

假設現在的目標環境仍然是 ES5,那麼需要第三方 Promise 提供,這裏擬用 Bluebird,兼容原生 Promise 接口(在 HTML 中引入,未直接出現在 JS 代碼中)。

function appAjax(action, params) {
    var deffered = $.Deferred();

    axios
        .post(apiUrl, {
            data: $.extend({
                action: action
            }, params)
        })
        .then(function(data) { ... }, function() { ... });

    return deferred.promise();
}

這次的封裝採用了 axios 來實現 Web Api 調用。但是爲了保持原來的接口(jQuery Promise 對象有提供 .done().fail().always() 事件處理),appAjax 仍然不得不返回 jQuery Promise。這樣,即使所有地方都不再需要使用 jQuery,這裏仍然得用。

項目中應該用還是不用 jQuery?請閱讀爲什麼要用原生 JavaScript 代替 jQuery?

去除 jQuery

就只在這裏使用 jQuery 總讓人感覺如芒在背,想把它去掉。有兩個辦法

  1. 修改所有業務中的調用,去掉 .done().fail().always(),改成 .then()。這一步工作量較大,但基本無痛,因爲 jQuery Promise 本身支持 .then()。但是有一點需要特別注意,這一點稍後說明
  2. 自己寫個適配器,兼容 jQuery Promise 的接口,工作量也不小,但關鍵是要充分測試,避免差錯。

上面提到第 1 種方法中有一點需要特別注意,那就是 .then().done() 系列函數在處理方式上有所不同。.then() 是按 Promise 的特性設計的,它返回的是另一個 Promise 對象;而 .done() 系列函數是按事件機制實現的,返回的是原來的 Promise 對象。所以像下面這樣的代碼在修改時就要注意了

appAjax(url, params)
    .done(function(data) { console.log("第 1 處處理", data) })
    .done(function(data) { console.log("第 2 處處理", data) });
// 第 1 處處理 {}
// 第 2 處處理 {}

簡單的把 .done() 改成 .then() 之後(注意不需要使用 Bluebird,因爲 jQuery Promise 支持 .then()

appAjax(url, params)
    .then(function(data) { console.log("第 1 處處理", data); })
    .then(function(data) { console.log("第 2 處處理", data); });
// 第 1 處處理 {}
// 第 2 處處理 undefined

原因上面已經講了,這裏正確的處理方式是合併多個 done 的代碼,或者在 .then() 處理函數中返回 data

appAjax(url, params)
    .then(function(data) {
        console.log("第 1 處處理", data);
        return data;
    })
    .then(function(data) {
        console.log("第 2 處處理", data);
    });

使用 Promise 接口改善設計

我們的 appAjax() 接口部分也可以設計成 Promise 實現,這是一個更通用的接口。既使用不用 ES2015+ 特性,也可以使用像 jQuery Promise 或 Bluebird 這樣的三方庫提供的 Promise。

function appAjax(action, params) {
    // axios 依賴於 Promise,ES5 中可以使用 Bluebird 提供的 Promise
    return axios
        .post(apiUrl, {
            data: $.extend({
                action: action
            }, params)
        })
        .then(function(data) {
            // 這裏調整了判斷順序,會讓代碼看起來更簡潔
            if (!data.code) { return data; }
            if (!data.message) { throw data; }
            alert(data.message);
        }, function() {
            alert("服務器錯誤");
        });
}

不過現在前端有構建工具,可以使用 ES2015+ 配置 Babel,也可以使用 TypeScript …… 總之,選擇很多,寫起來也很方便。那麼在設計的時候就不用侷限於 ES5 所支持的內容了。所以可以考慮用 Promise + async/await 來實現

async function appAjax(action, params) {
    // axios 依賴於 Promise,ES5 中可以使用 Bluebird 提供的 Promise
    const data = await axios
        .post(apiUrl, {
            data: $.extend({
                action: action
            }, params)
        })
        // 這裏模擬一個包含錯誤消息的結果,以便後面統一處理錯誤
        // 這樣就不需要用 try ... catch 了
        .catch(() => ({ code: -1, message: "服務器錯誤" }));

    if (!data.code) { return data; }
    if (!data.message) { throw data; }

    alert(data.message);
}

上面代碼中使用 .catch() 來避免 try ... catch ... 的技巧在從不用 try-catch 實現的 async/await 語法說錯誤處理中提到過。

當然業務層調用也可以使用 async/await(記得寫在 async 函數中):

const data = await appAjax("login", {
    username: "uname",
    password: "passwd"
}).catch(() => {
    alert("登錄失敗");
});

if (data) {
    window.location.assign("home");
}

對於多次 .done() 的改造:

const data = await appAjax(url, params);
console.log("第 1 處處理", data);
console.log("第 2 處處理", data);

小結

本文以封裝 Ajax 調用爲例,看似在講述異步調用。但實際想告訴大家的東西是:如何將一個常用的功能封裝起來,實現代碼重用和更簡潔的調用;以及在封裝的過程中需要考慮的問題——向前和向後的兼容性,在做工具函數封裝的時候,應該儘量避免和某個特定的工具特性綁定,向公共標準靠攏——不知大家是否有所體會。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章