如何實現一個HTTP請求庫——axios原始碼閱讀與分析

黃Java發表於2018-05-05

概述

在前端開發過程中,我們經常會遇到需要傳送非同步請求的情況。而使用一個功能齊全,介面完善的HTTP請求庫,能夠在很大程度上減少我們的開發成本,提高我們的開發效率。

axios是一個在近些年來非常火的一個HTTP請求庫,目前在GitHub中已經擁有了超過40K的star,受到了各位大佬的推薦。

今天,我們就來看下,axios到底是如何設計的,其中又有哪些值得我們學習的地方。我在寫這邊文章時,axios的版本為0.18.0。我們就以這個版本的程式碼為例,來進行具體的原始碼閱讀和分析。當前axios所有原始碼檔案都在lib資料夾中,因此我們下文中提到的路徑均是指lib資料夾中的路徑。

本文的主要內容有:

  • 如何使用axios
  • axios的核心模組是如何設計與實現的(請求、攔截器、撤回)
  • axios的設計有什麼值得借鑑的地方

如何使用axios

想要了解axios的設計,我們首先需要來看下axios是如何使用的。我們通過一個簡單示例來介紹以下axios的API。

傳送請求

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

這是一個官方的API示例。從上面的程式碼中我們可以看到,axios的用法與jQuery的ajax很相似,都是通過返回一個Promise(也可以通過success的callback,不過建議使用Promise或者await)來繼續後面的操作。

這個程式碼示例很簡單,我就不過多贅述了,下面讓我們來看下如何新增一個過濾器函式。

增加攔截器(Interceptors)函式

// 增加一個請求攔截器,注意是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('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
複製程式碼

通過上面的示例我們可以看到,axios使用的是基於CancelToken的一個撤回提案。不過,目前該提案已經被撤回,具體詳情可以見此處。具體的撤回實現方法我們會在後面的章節原始碼分析的時候進行說明。

axios的核心模組是如何設計與實現的

通過上面的例子,我相信大家對axios的使用方法都有了一個大致的瞭解。下面,我們將按照模組來對axios的設計與實現進行分析。下圖是我們在這篇部落格中將會涉及到的相關的axios的檔案,如果讀者有興趣的話,可以通過clone相關程式碼結合部落格進行閱讀,這樣能夠加深對相關模組的理解。

如何實現一個HTTP請求庫——axios原始碼閱讀與分析

HTTP請求模組

作為核心模組,axios傳送請求相關的程式碼位於core/dispatchReqeust.js檔案中。由於篇幅有限,下面我選取部分重點的原始碼進行簡單的介紹:

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

    // 其他原始碼

    // default adapter是一個可以判斷當前環境來選擇使用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來得到傳送請求的模組的,我們自己也可以通過傳入符合規範的adapter函式來替換掉原生的模組(雖然一般不會這麼做,不過也算是一個鬆耦合擴充套件點)。

default.js檔案中,我們能夠看到相關的adapter選擇邏輯,即根據當前容器中特有的一些屬性和建構函式來進行判斷。

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檔案中。

攔截器模組

瞭解了dispatchRequest實現的HTTP請求傳送模組,我們來看下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. 在chain執行佇列中,插入了初始的傳送請求的函式dispatchReqeust和與之對應的undefined。後面需要增加一個undefined是因為在Promise中,需要一個success和一個fail的回撥函式,這個從程式碼promise = promise.then(chain.shift(), chain.shift());就能夠看出來。因此,dispatchReqeustundefined我們可以成為一對函式。
  3. 在chain執行佇列中,傳送請求的函式dispatchReqeust是處於中間的位置。它的前面是請求攔截器,通過unshift方法放入;它的後面是響應攔截器,通過push放入。要注意的是,這些函式都是成對的放入,也就是一次放入兩個。

通過上面的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) {
            // Cancellation has already been requested
            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
    };
};
複製程式碼

而在adapter/xhr.js檔案中,有與之相對應的取消請求的程式碼:

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

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

結合上面的取消HTTP請求的示例和這些程式碼,我們來簡單說下相關的實現邏輯:

  1. 在可能需要取消的請求中,我們初始化時呼叫了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函式時,沒有當做一個特殊的函式來對待,而是採用一視同仁的方法,將其放在佇列的中間位置,從而保證了佇列處理的一致性,提高了程式碼的可閱讀性。

Adapter的處理邏輯

在adapter的處理邏輯中,axios沒有把http和xhr兩個模組(一個用於Node.js傳送請求,另一個則用於瀏覽器端傳送請求)當成自身的模組直接在dispatchRequest中直接飲用,而是通過配置的方法在default.js檔案中進行預設引入。這樣既保證了兩個模組間的低耦合性,同時又能夠為今後使用者需要自定義請求傳送模組保留了餘地。

取消HTTP請求的處理邏輯

在取消HTTP請求的邏輯中,axios巧妙的使用了一個Promise來作為觸發器,將resolve函式通過callback中引數的形式傳遞到了外部。這樣既能夠保證內部邏輯的連貫性,也能夠保證在需要進行取消請求時,不需要直接進行相關類的示例資料改動,最大程度上避免了侵入其他的模組。

總結

本文對axios相關的使用方式、設計思路和實現方法進行了詳細的介紹。讀者能夠通過上述文章,瞭解axios的設計思想,同時能夠在axios的程式碼中,學習到關於模組封裝和互動等相關的經驗。

由於篇幅原因,本文僅針對axios的核心模組進行了分解和介紹,如果對其他程式碼有興趣的同學,可以去GitHub進行檢視。

如果有任何疑問或者觀點,歡迎隨時留言討論。

相關文章