先後分離模型之封裝 Api 調用

Ajax 和異步處理

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

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

固然更直觀的一點的處理方式是使用 ES2017 帶來的 async/await 方式,能夠用同步代碼的形式來寫異步代碼,固然也有一些坑在裏面。對於前端工程師來講,最大的坑就是有些瀏覽器不支持,須要進行轉譯,因此若是前端代碼沒有構建過程,通常仍是就用 ES5 的語法兼容性好一些(jQuery 的 Promise 是支持 ES5 的,可是標準 Promise 要 ES6 之後纔可使用)。ajax

關於 JavaScript 異步處理相關的內容能夠參考json

本身封裝工具函數

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

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

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

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 部分不一樣,因此進行一次封裝會大大減小代碼量,能夠這樣封裝promise

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 調用爲例,看似在講述異步調用。但實際想告訴你們的東西是:如何將一個經常使用的功能封裝起來,實現代碼重用和更簡潔的調用;以及在封裝的過程當中須要考慮的問題——向前和向後的兼容性,在作工具函數封裝的時候,應該儘可能避免和某個特定的工具特性綁定,向公共標準靠攏——不知你們是否有所體會。

相關文章
相關標籤/搜索