前后分離模型之封裝 Api 調(diào)用

Ajax 和異步處理

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

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

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

關(guān)于 JavaScript 異步處理相關(guān)的內(nèi)容可以參考

自己封裝工具函數(shù)

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

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

那么用 jQuery 寫這么一個 Ajax 調(diào)用,大概是這樣

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("服務(wù)器錯誤");
    });

初步封裝

同一項目中,這樣的 Ajax 調(diào)用,基本上只有 data 部分和 .done 回調(diào)中的 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) {
                    // 如果服務(wù)器返回了消息,那么向用戶呈現(xiàn)消息
                    // resolve(null),表示不需要后續(xù)進行業(yè)務(wù)處理
                    alert(data.message);
                    deffered.resolve();
                } else {
                    // 如果服務(wù)器沒返回消息,那么把 data 丟給外面的業(yè)務(wù)處理
                    deferred.reject(data);
                }
            } else {
                // 正常返回數(shù)據(jù)的情況
                deffered.resolve(data);
            }
        })
        .fail(function() {
            // Ajax 調(diào)用失敗,向用戶呈現(xiàn)消息,同時不需要進行后續(xù)的業(yè)務(wù)處理
            alert("服務(wù)器錯誤");
            deffered.resolve();
        });

    return deferred.promise();
}

而業(yè)務(wù)層的調(diào)用就很簡單了

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

更換 API 調(diào)用接口

上面的封裝對調(diào)用接口和返回數(shù)據(jù)進行了統(tǒng)一處理,把大部分項目接口約定的內(nèi)容都處理掉了,剩下在每次調(diào)用時需要處理的就是純粹的業(yè)務(wù)。

現(xiàn)在項目組決定不用 jQuery 的 Ajax,而是采用 axios 來調(diào)用 API(axios 不見得就比 jQuery 好,這里只是舉例),那么只需要修改一下 appAjax() 的實現(xiàn)即可。所有業(yè)務(wù)調(diào)用都不需要修改。

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

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

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

    return deferred.promise();
}

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

項目中應(yīng)該用還是不用 jQuery?請閱讀為什么要用原生 JavaScript 代替 jQuery?

去除 jQuery

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

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

上面提到第 1 種方法中有一點需要特別注意,那就是 .then().done() 系列函數(shù)在處理方式上有所不同。.then() 是按 Promise 的特性設(shè)計的,它返回的是另一個 Promise 對象;而 .done() 系列函數(shù)是按事件機制實現(xiàn)的,返回的是原來的 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

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

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

使用 Promise 接口改善設(shè)計

我們的 appAjax() 接口部分也可以設(shè)計成 Promise 實現(xiàn),這是一個更通用的接口。既使用不用 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) {
            // 這里調(diào)整了判斷順序,會讓代碼看起來更簡潔
            if (!data.code) { return data; }
            if (!data.message) { throw data; }
            alert(data.message);
        }, function() {
            alert("服務(wù)器錯誤");
        });
}

不過現(xiàn)在前端有構(gòu)建工具,可以使用 ES2015+ 配置 Babel,也可以使用 TypeScript …… 總之,選擇很多,寫起來也很方便。那么在設(shè)計的時候就不用局限于 ES5 所支持的內(nèi)容了。所以可以考慮用 Promise + async/await 來實現(xiàn)

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

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

    alert(data.message);
}

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

當然業(yè)務(wù)層調(diào)用也可以使用 async/await(記得寫在 async 函數(shù)中):

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);

小結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,405評論 5 22
  • Promiese 簡單說就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果,語法上說,Pr...
    雨飛飛雨閱讀 3,491評論 0 19
  • 00、前言Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強大。它由社區(qū)...
    夜幕小草閱讀 2,227評論 0 12
  • 單線程與異步 Javascript是單線程運行、支持異步機制的語言。進入正題之前,我們有必要先理解這種運行方式。 ...
    貝聊科技閱讀 654評論 0 0
  • 被英語逼瘋的這幾天,痛定思痛,各種挖掘?qū)W英語的方法,網(wǎng)絡(luò)視頻書籍,各方各面在尋找學英語的資料、書籍,幾乎到了“窮兇...
    695e0145188a閱讀 893評論 0 5

友情鏈接更多精彩內(nèi)容