[譯]axios 是如何封裝 HTTP 請求的

原載於 TutorialDocs 網站的文章《How to Implement an HTTP Request Library with Axios》javascript

概述

前端開發中,常常會遇到發送異步請求的場景。一個功能齊全的 HTTP 請求庫能夠大大下降咱們的開發成本,提升開發效率。html

axios 就是這樣一個 HTTP 請求庫,近年來很是熱門。目前,它在 GitHub 上擁有超過 40,000 的 Star,許多權威人士都推薦使用它。前端

所以,咱們有必要了解下 axios 是如何設計,以及如何實現 HTTP 請求庫封裝的。撰寫本文時,axios 當前版本爲 0.18.0,咱們以該版本爲例,來閱讀和分析部分核心源代碼。axios 的全部源文件都位於 lib 文件夾中,下文中提到的路徑都是相對於 lib 來講的。java

本文咱們主要討論:ios

  • 怎樣使用 axios。git

  • axios 的核心模塊(請求、攔截器、撤銷)是如何設計和實現的?github

  • axios 的設計優勢是什麼?ajax

如何使用 axios

要理解 axios 的設計,首先須要看一下如何使用 axios。咱們舉一個簡單的例子來講明下 axios API 的使用。axios

發送請求

axios({
  method:'get',
  url:'http://bit.ly/2mTM3nY',
  responseType:'stream'
})
  .then(function(response) {
  response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
複製代碼

這是一個官方示例。從上面的代碼中能夠看到,axios 的用法與 jQuery 的 ajax 方法很是相似,二者都返回一個 Promise 對象(在這裏也可使用成功回調函數,但仍是更推薦使用 Promiseawait),而後再進行後續操做。promise

這個實例很簡單,不須要我解釋了。咱們再來看看如何添加一個攔截器函數。

添加攔截器函數

// 添加一個請求攔截器。注意,這裏面有 2 個函數——分別是成功和失敗時的回調函數,這樣設計的緣由會在以後介紹
axios.interceptors.request.use(function (config) {
    // 發起請求前執行一些處理任務
    return config; // 返回配置信息
  }, function (error) {
    // 請求錯誤時的處理
    return Promise.reject(error);
  });

// 添加一個響應攔截器
axios.interceptors.response.use(function (response) {
    // 處理響應數據
    return response; // 返回響應數據
  }, function (error) {
    // 響應出錯後所作的處理工做
    return Promise.reject(error);
  });
複製代碼

從上面的代碼,咱們能夠知道:發送請求以前,咱們能夠對請求的配置參數(config)作處理;在請求獲得響應以後,咱們能夠對返回數據作處理。當請求或響應失敗時,咱們還能指定對應的錯誤處理函數。

撤銷 HTTP 請求

在開發與搜索相關的模塊時,咱們常常要頻繁地發送數據查詢請求。通常來講,當咱們發送下一個請求時,須要撤銷上個請求。所以,能撤銷相關請求功能很是有用。axios 撤銷請求的示例代碼以下:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 例子一
axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('請求撤銷了', thrown.message);
  } else {
    // 處理錯誤
  }
});

// 例子二
axios.post('/user/12345', {
  name: '新名字'
}, {
  cancelToken: source.token
}).

// 撤銷請求 (信息參數是可選的)
source.cancel('用戶撤銷了請求');
複製代碼

從上例中能夠看到,在 axios 中,使用基於 CancelToken 的撤銷請求方案。然而,該提案現已撤回,詳情如 點這裏。具體的撤銷請求的實現方法,將在後面的源代碼分析的中解釋。

axios 核心模塊的設計和實現

經過上面的例子,我相信每一個人都對 axios 的使用有一個大體的瞭解了。下面,咱們將根據模塊分析 axios 的設計和實現。下面的圖片,是我在本文中會介紹到的源代碼文件。若是您感興趣,最好在閱讀時克隆相關的代碼,這能加深你對相關模塊的理解。

HTTP 請求模塊

請求模塊的代碼放在了 core/dispatchRequest.js 文件中,這裏我只展現了一些關鍵代碼來簡單說明:

module.exports = function dispatchRequest(config) {
    throwIfCancellationRequested(config);

    // 其餘源碼

    // 默認適配器是一個模塊,能夠根據當前環境選擇使用 Node 或者 XHR 發送請求。
    var adapter = config.adapter || defaults.adapter; 

    return adapter(config).then(function onAdapterResolution(response) {
        throwIfCancellationRequested(config);

        // 其餘源碼

        return response;
    }, function onAdapterRejection(reason) {
        if (!isCancel(reason)) {
            throwIfCancellationRequested(config);

            // 其餘源碼

            return Promise.reject(reason);
        });
};
複製代碼

上面的代碼中,咱們可以知道 dispatchRequest 方法是經過 config.adapter ,得到發送請求模塊的。咱們還能夠經過傳遞,符合規範的適配器函數來替代原來的模塊(通常來講,咱們不會這樣作,但它是一個鬆散耦合的擴展點)。

defaults.js 文件中,咱們能夠看到相關適配器的選擇邏輯——根據當前容器的一些獨特屬性和構造函數,來肯定使用哪一個適配器。

function getDefaultAdapter() {
    var adapter;
    // 只有在 Node.js 中包含 process 類型對象時,才使用它的請求模塊
    if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
        // Node.js 請求模塊
        adapter = require('./adapters/http');
    } else if (typeof XMLHttpRequest !== 'undefined') {
        // 瀏覽器請求模塊
        adapter = require('./adapters/xhr');
    }
    return adapter;
}
複製代碼

axios 中的 XHR 模塊相對簡單,它是對 XMLHTTPRequest 對象的封裝,這裏我就再也不解釋了。有興趣的同窗,能夠本身閱讀源源碼看看,源碼位於 adapters/xhr.js 文件中。

攔截器模塊

如今讓咱們看看 axios 是如何處理,請求和響應攔截器函數的。這就涉及到了 axios 中的統一接口 ——request 函數。

Axios.prototype.request = function request(config) {

    // 其餘源碼

    var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);

    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
        chain.unshift(interceptor.fulfilled, interceptor.rejected);
    });

    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
        chain.push(interceptor.fulfilled, interceptor.rejected);
    });

    while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
};
複製代碼

這個函數是 axios 發送請求的接口。由於函數實現代碼至關長,這裏我會簡單地討論相關設計思想:

  1. chain 是一個執行隊列。隊列的初始值是一個攜帶配置(config)參數的 Promise 對象。

  2. 在執行隊列中,初始函數 dispatchRequest 用來發送請求,爲了與 dispatchRequest對應,咱們添加了一個 undefined。添加 undefined 的緣由是須要給 Promise 提供成功和失敗的回調函數,從下面代碼裏的 promise = promise.then(chain.shift(), chain.shift()); 咱們就能看出來。所以,函數 dispatchRequestundefiend 能夠當作是一對函數。

  3. 在執行隊列 chain 中,發送請求的 dispatchReqeust 函數處於中間位置。它前面是請求攔截器,使用 unshift 方法插入;它後面是響應攔截器,使用 push 方法插入,在 dispatchRequest 以後。須要注意的是,這些函數都是成對的,也就是一次會插入兩個。

瀏覽上面的 request 函數代碼,咱們大體知道了怎樣使用攔截器。下一步,來看看怎樣撤銷一個 HTTP 請求。

撤銷請求模塊

與撤銷請求相關的模塊位於 Cancel/ 文件夾下,如今咱們來看下相關核心代碼。

首先,咱們來看下基礎 Cancel 類。它是一個用來記錄撤銷狀態的類,具體代碼以下:

function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;
複製代碼

使用 CancelToken 類時,須要向它傳遞一個 Promise 方法,用來實現 HTTP 請求的撤銷,具體代碼以下:

function CancelToken(executor) {
    if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
    }

    var resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve;
    });

    var token = this;
    executor(function cancel(message) {
        if (token.reason) {
            // 已經被撤銷了
            return;
        }

        token.reason = new Cancel(message);
        resolvePromise(token.reason);
    });
}

CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
        cancel = c;
    });
    return {
        token: token,
        cancel: cancel
    };
};
複製代碼

adapters/xhr.js 文件中,撤銷請求的地方是這樣寫的:

if (config.cancelToken) {
    // 等待撤銷
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        request.abort();
        reject(cancel);
        // 重置請求
        request = null;
    });
}
複製代碼

經過上面的撤銷  HTTP請求的例子,讓咱們簡要地討論一下相關的實現邏輯:

  1. 在須要撤銷的請求中,調用 CancelToken 類的 source 方法類進行初始化,會獲得一個包含 CancelToken 類實例 A 和 cancel 方法的對象。

  2. 當 source 方法正在返回實例 A 的時候,一個處於 pending 狀態的 promise 對象初始化完成。在將實例 A 傳遞給 axios 以後,promise 就能夠做爲撤銷請求的觸發器使用了。

  3. 當調用經過 source 方法返回的 cancel 方法後,實例 A 中 promise 狀態從 pending 變成 fulfilled,而後當即觸發 then 回調函數。因而 axios 的撤銷方法——request.abort() 被觸發了。

axios 這樣設計的好處是什麼?

發送請求函數的處理邏輯

如前幾章所述,axios 不將用來發送請求的 dispatchRequest 函數看作一個特殊函數。實際上,dispatchRequest 會被放在隊列的中間位置,以便保證隊列處理的一致性和代碼的可讀性。

適配器的處理邏輯

在適配器的處理邏輯上,httpxhr 模塊(一個是在 Node.js 中用來發送請求的,一個是在瀏覽器裏用來發送請求的)並無在 dispatchRequest 函數中使用,而是各自做爲單獨的模塊,默認經過 defaults.js 文件中的配置方法引入的。所以,它不只確保了兩個模塊之間的低耦合,並且還爲未來的用戶提供了定製請求發送模塊的空間。

撤銷 HTTP 請求的邏輯

在撤銷 HTTP 請求的邏輯中,axios 設計使用 Promise 來做爲觸發器,將 resolve 函數暴露在外面,並在回調函數裏使用。它不只確保了內部邏輯的一致性,並且還確保了在須要撤銷請求時,不須要直接更改相關類的樣例數據,以免在很大程度上入侵其餘模塊。

總結

本文詳細介紹了 axios 的用法、設計思想和實現方法。在閱讀以後,您能夠了解 axios 的設計,並瞭解模塊的封裝和交互。

本文只介紹了 axios 的核心模塊,若是你對其餘模塊代碼感興趣,能夠到 GitHub 上查看。

(完)

相關文章
相關標籤/搜索