axios 源碼閱讀(一)--探究基礎(chǔ)能力的實現(xiàn)

axios 是一個通用的寶藏請求庫,此次探究了 axios 中三個基礎(chǔ)能力的實現(xiàn),并將過程記錄于此.

零. 前置

一. 目標(biāo)

閱讀源碼肯定是帶著問題來學(xué)習(xí)的,所以以下是本次源碼閱讀準(zhǔn)備探究的問題:

  • Q1. 如何實現(xiàn)同時支持 axios(config)axios.get(config) 語法
  • Q2. 瀏覽器和 NodeJS 請求能力的兼容實現(xiàn)
  • Q3. 請求 & 響應(yīng)攔截器實現(xiàn)

二. 項目結(jié)構(gòu)

簡單提煉下項目中比較重要的部分:

三. 從 axios 對象開始分析

當(dāng)在項目中使用 axios 庫時,總是要通過 axios 對象調(diào)用相關(guān)能力,在項目的 test/typescipt/axios.ts 中有比較清晰的測試用例(以下為簡化掉 then 和 catch 后的代碼):

axios(config)
axios.get('/user?id=12345')
axios.get('/user', { params: { id: 12345 } })
axios.head('/user')
axios.options('/user')
axios.delete('/user')
axios.post('/user', { foo: 'bar' })
axios.post('/user', { foo: 'bar' }, { headers: { 'X-FOO': 'bar' } })
axios.put('/user', { foo: 'bar' })
axios.patch('/user', { foo: 'bar' })

從上面代碼可以看出 axios 庫 同時支持 axios(config)axios.get(config) 語法, 并且支持多種請求方法;

接下來逐個分析每個能力的實現(xiàn)過程~

3.1. 弄清 axios 對象

同時支持 axios(config)axios.get(config) 語法,說明 axios 是個函數(shù),同時也是一個具備方法屬性的對象. 所以接下來我們來分析一下 axios 庫暴露的 axios對象.

從項目根目錄可以找到入口文件 index.js,其指向 lib 目錄下的 axios.js, 這里做了三件事:

  • 1)使用工廠方法創(chuàng)建實例axios

    function createInstance(defaultConfig) {
      // 對 Axios 類傳入配置對象得到實例,并作為 Axios.prototype.request 方法的上下文
      // 作用:支持通過 axios('https://xxx/xx') 語法實現(xiàn)請求
      var context = new Axios(defaultConfig);
      var instance = bind(Axios.prototype.request, context); // 手?jǐn)]版本的 `Function.prototype.bind`
    
      // 為了實例具備 Axios 的能力,故將 Axios的原型 & Axios實例的屬性 復(fù)制給 instance
      utils.extend(instance, Axios.prototype, context);
      utils.extend(instance, context);
      return instance; // instance 的真面目是 request 方法
    }
    
    var axios = createInstance(defaults);
    
  • 2)給實例axios 掛載操作方法

    axios.Axios = Axios; // 可以通過實例訪問 Axios 類
    axios.create = function create(instanceConfig) {
      // 使用新配置對象,通過工廠類獲得一個新實例
      // 作用:如果項目中有固定的配置可以直接 cosnt newAxios = axios.create({xx: xx})
      //            然后使用 newAxios 按照 axios API 實現(xiàn)功能
      return createInstance(mergeConfig(axios.defaults, instanceConfig));
    };
    
    // 取消請求三件套
    axios.Cancel = require('./cancel/Cancel');
    axios.CancelToken = require('./cancel/CancelToken');
    axios.isCancel = require('./cancel/isCancel');
    
    axios.all = function all(promises) {
      return Promise.all(promises); // 相當(dāng)直接,其實就是 Promise.all
    };
    axios.spread = require('./helpers/spread'); // `Function.prototype.apply` 語法糖
    
    // 判定是否為 createError 創(chuàng)建的 Error 對象
    axios.isAxiosError = require('./helpers/isAxiosError');
    
  • 3)暴露實例axios

    module.exports = axios;
    module.exports.default = axios;
    

從以上分析可以得到結(jié)論,之所以能夠 "同時支持 axios(config)axios.get(config) 語法" ,是因為:

  • 暴露的 axios 實例本來就是 request 函數(shù),所以支持 axios(config) 語法
  • 工廠方法 createInstance 最終返回的對象,具備復(fù)制得的 Axios的原型 & Axios實例的屬性,所以也能像 Axios 實例一樣直接使用 axios.get(config) 語法

3.2. 支持多種請求方法

axios 支持 get | head | options | delete | post | put | patch 當(dāng)然并不是笨笨地逐個實現(xiàn)的,閱讀文件 lib/core/Axios.js 可以看到如下結(jié)構(gòu):

function function Axios(instanceConfig) {} // Axios 構(gòu)造函數(shù)

// 原型上只有兩個方法
Axios.prototype.request = function request(config) {};
Axios.prototype.getUri = function getUri(config) {};

// 為 request 方法提供別名
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config...)); // 這里省略了 mergeConfig 對入?yún)⒌恼?  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { 
  // 基本同上
});

module.exports = Axios;

其中核心實現(xiàn)自然是通用請求方法 Axios.prototype.request,其他函數(shù)通過調(diào)用 request 方法并傳入不同的配置參數(shù)來實現(xiàn)差異化.

四、封裝請求實現(xiàn)--"適配器"

總所周知,瀏覽器端請求一般是通過 XMLHttpRequest 或者 fetch 來實現(xiàn)請求,而 NodeJS 端則是通過內(nèi)置模塊 http 等實現(xiàn). 那么 axios 是如何實現(xiàn)封裝請求實現(xiàn)使得一套 API 可以在瀏覽器端和 NodeJS 端通用的呢?讓我們繼續(xù)看看 Axios.prototype.request 的實現(xiàn),簡化代碼結(jié)構(gòu)如下:

Axios.prototype.request = function request(config) {
  // 此處省略請求配置合并代碼,最終得到包含請求信息的 config 對象

  // chain 可以視為請求流程數(shù)組,當(dāng)不添加攔截器時 chain 如下
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 下方是攔截器部分,暫時忽略
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {});
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {});
  // 執(zhí)行請求
  while (chain.length) {
    // 請求流程數(shù)組前兩個出棧,當(dāng)前分別為 dispatchRequest 和 undefined
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

當(dāng)沒有添加任何攔截器的時候,請求流程數(shù)組 chain 中就只有 [dispatchRequest, undefined] ,此時在下方的 while 只運行一輪,dispatchRequest 作為 then 邏輯接收 config 參數(shù)并運行,繼續(xù)找到 dispatchRequest 的簡化實現(xiàn)(/lib/core/dispatchRequest.js):

module.exports = function dispatchRequest(config) {
  // 取消請求邏輯,稍后分析
  throwIfCancellationRequested(config);

  // 此處略去了對 config 的預(yù)處理

  // 嘗試獲取 config 中的適配器,如果沒有則使用默認(rèn)的適配器
  var adapter = config.adapter || defaults.adapter;

  // 將 config 傳入 adapte 執(zhí)行,得到一個 Promise 結(jié)果
    // 如果成功則將數(shù)據(jù)后放入返回對象的 data 屬性,失敗則放入返回結(jié)果的 response.data 屬性
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    response.data = transformData(...); // 此處省略入?yún)?    return response; // 等同于 Promise.resolve(response)
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      if (reason && reason.response) {
        reason.response.data = transformData(...); // 此處省略入?yún)?      }
    }
    return Promise.reject(reason);
  });
};

簡單過完一遍 dispatchRequest 看到重點在于 adapter(config) 之后發(fā)生了什么,于是找到默認(rèn)配置的實現(xiàn)(/lib/default.js):

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 提供給瀏覽器用的 XHR 適配器
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 提供給 NodeJS 用的內(nèi)置 HTTP 模塊適配器
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter(),
}

顯而易見,適配器通過判斷 XMLHttpRequest 和 process 對象來判斷當(dāng)前平臺并獲得對應(yīng)的實現(xiàn). 接下來繼續(xù)進入 /lib/adapters 目錄,里面的 xhr.js 和 http.js 分別對應(yīng)適配器的瀏覽器實現(xiàn)和NodeJS 實現(xiàn),而 README.md 介紹了實現(xiàn) adapter 的規(guī)范:

// /lib/adapters/README.md
module.exports = function myAdapter(config) {
  // 使用 config 參數(shù)構(gòu)建請求并實現(xiàn)派發(fā),獲得返回后則交給 settle 處理得到 Promise
  return new Promise(function(resolve, reject) {
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request
    };

    // 根據(jù) response 的狀態(tài),成功則執(zhí)行 resolve,否則執(zhí)行 reject并傳入一個 AxiosError
    settle(resolve, reject, response);
  });
}

// /lib/core/settle.js
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    // 省略參數(shù),createError 創(chuàng)建一個 Error 對象并為其添加 response 相關(guān)屬性方便讀取
    reject(createError(...));
  }
};

實現(xiàn)自定義適配器要先接收 config , 并基于 config 參數(shù)構(gòu)建請求并實現(xiàn)派發(fā),獲得結(jié)果后返回 Promise,接下來的邏輯控制權(quán)就交回給 /lib/core/dispatchRequest.js 繼續(xù)處理了.

五. 攔截器實現(xiàn)

5.1. 探究"請求|響應(yīng)攔截器"的實現(xiàn)

axios 的一個常用特性就是攔截器,只需要簡單的一行 axios.interceptors.request.use(config => return config)就能實現(xiàn)請求/響應(yīng)的攔截,在項目有鑒權(quán)需求或者返回值需要預(yù)處理時相當(dāng)常用. 現(xiàn)在就來看看這個特性是如何實現(xiàn)的,回到剛剛看過的 Axios.js:

// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // 此處省略請求配置合并代碼,最終得到包含請求信息的 config 對象

  // chain 可以視為請求流程數(shù)組,當(dāng)不添加攔截器時 chain 如下
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 攔截器實現(xiàn)
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 往 chain 數(shù)組頭部插入 then & catch邏輯
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected); // 往 chain 尾部插入處理邏輯
  });
  // 執(zhí)行請求
  while (chain.length) {
    // 請求流程數(shù)組前兩個出棧,當(dāng)前分別為 dispatchRequest 和 undefined
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

之前說過了項目中使用的 axios 其實就是 Axios.prototype.request,所以當(dāng) Axios.prototype.request 觸發(fā)時,會遍歷 axios.interceptors.requestaxios.interceptors.response 并將其中的攔截邏輯添加到"請求流程數(shù)組 chain" 中.

在 Axios.prototype.request 中并沒有 interceptors 屬性的實現(xiàn),于是回到 Axios 構(gòu)造函數(shù)中尋找對應(yīng)邏輯(之前說過,工廠函數(shù) createInstance 會將 Axios的原型 & Axios實例的屬性 復(fù)制給生成的 axios 對象):

// /lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 實例化 Axios 時也為其添加了 interceptors 對象,其攜帶了 request & response 兩個實例
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// /lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = []; // 數(shù)組,用于管理攔截邏輯
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {} // 添加攔截器
InterceptorManager.prototype.eject = function eject(id) {} // 刪除攔截器
InterceptorManager.prototype.forEach = function forEach(fn) {} // 遍歷攔截器

Axios 構(gòu)造函數(shù)在創(chuàng)建實例時會完成 interceptors 屬性的創(chuàng)建,實現(xiàn) axios.interceptors.requestaxios.interceptors.response 對于攔截邏輯的管理.

5.2. 實驗:同 axios 內(nèi)多個攔截器的執(zhí)行順序

由于 axios.interceptors.request 遍歷添加到 "請求流程數(shù)組 chain" 向數(shù)組頭插入 request 攔截器,所以越后 use 的 request 攔截器會越早執(zhí)行. 相反,越后 use 的 response 攔截器會越晚執(zhí)行.

現(xiàn)在假設(shè)為 axios 添加兩個請求攔截器和兩個響應(yīng)攔截器,那么 "請求流程數(shù)組 chain" 就會變成這樣(請求攔截器越后 use 的會越先執(zhí)行):

[
    request2_fulfilled, request2_rejected,
    request1_fulfilled, request1_rejected,
    dispatchRequest, undefined
    response3_fulfilled, response3_rejected,
  response4_fulfilled, response4_rejected,
]

為此編寫個單測用于打印驗證:

it('should add multiple request & response interceptors', function (done) {
  var response;

  axios.interceptors.request.use(function (data) {
    console.log('request1_fulfilled, request1_rejected')
    return data;
  });
  axios.interceptors.request.use(function (data) {
    console.log('request2_fulfilled, request2_rejected')
    return data;
  });
  axios.interceptors.response.use(function (data) {
    console.log('response3_fulfilled, response3_rejected')
    return data;
  });
  axios.interceptors.response.use(function (data) {
    console.log('response4_fulfilled, response4_rejected')
    return data;
  });

  axios('/foo').then(function (data) {
    response = data;
  });

  getAjaxRequest().then(function (request) {
    request.respondWith({
      status: 200,
      responseText: 'OK'
    });

    setTimeout(function () {
      expect(response.data).toBe('OK');
      done();
    }, 100);
  });
});

結(jié)果如下,攔截器的執(zhí)行打印與期望的結(jié)果一致:

5.3. 探究"取消攔截器"實現(xiàn)

使用以下與 setTimeout 和 clearTimeout 相似的語法,即可將一個定義好的攔截器給取消掉:

var intercept = axios.interceptors.response.use(function (data) { return data; }); // 定義攔截器
axios.interceptors.response.eject(intercept); // 取消攔截器

這里用到了 use 和 eject 兩個方法,所以到 InterceptorManager.js 中找找相關(guān)實現(xiàn):

// /lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = []; // 數(shù)組,用于管理攔截邏輯
}

// 添加攔截器
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  // 將攔截邏輯包裝為對象,推入管理數(shù)組 handles 中
  this.handlers.push({ fulfilled, rejected });
  return this.handlers.length - 1; // 返回當(dāng)前下標(biāo)
};

// 通過取消攔截器
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; // 根據(jù)下標(biāo)判斷,存在則置空掉
  }
};

這里邏輯就比較簡單了,由 InterceptorManager 創(chuàng)建一個內(nèi)置數(shù)組的實例來管理所有攔截器,use 推入一個數(shù)組并返回其數(shù)組下標(biāo),eject 時也用數(shù)組下標(biāo)來置空,這樣就起到了攔截器管理的效果了~

小結(jié)

至此文章就告一段落了,還記得開始提出的三個問題嗎?

  • Q1. 如何實現(xiàn)同時支持 axios(config)axios.get(config) 語法

  • A1. axios 庫暴露的 axios 對象本來就是一個具備 Axios 實例屬性的 Axios.prototype.request 函數(shù). 詳見"第三節(jié).從 axios 對象開始分析".

  • Q2. 瀏覽器和 NodeJS 請求能力的兼容實現(xiàn)

  • A2. 通過判斷平臺后選擇對應(yīng)平臺的適配器實現(xiàn). 詳見"第四節(jié). 封裝請求實現(xiàn)--'適配器'"

  • Q3. 請求 & 響應(yīng)攔截器實現(xiàn)

  • A3. 通過數(shù)組的形式管理, 將請求攔截器、請求、響應(yīng)攔截器都放在"請求流程數(shù)組 chain" 中,請求時依次執(zhí)行直到 "請求流程數(shù)組 chain" 為空. 詳見"第五節(jié). 攔截器實現(xiàn)".

歡迎拍磚,覺得還行也歡迎點贊收藏~
新開公號:「無夢的冒險譚」歡迎關(guān)注(搜索 Nodreame 也可以~)
旅程正在繼續(xù) ??ヽ(°▽°)ノ?

?著作權(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)容

  • 閱讀axios源碼,可解釋下列問題:1.為什么axios既可以像函數(shù)一樣調(diào)用,也可以使用別名,如axios.req...
    景陽岡大蟲在此閱讀 578評論 0 0
  • Axios是一個基于Promise的HTTP請求庫,可以用在瀏覽器和Node.js中。平時在Vue項目中,經(jīng)常使用...
    多啦斯基周閱讀 919評論 0 0
  • Vue -漸進式JavaScript框架 介紹 vue 中文網(wǎng) vue github Vue.js 是一套構(gòu)建用戶...
    桂_3d6b閱讀 947評論 0 0
  • axios 是一個基于 Promise 的http請求庫,可以用在瀏覽器和node.js中 備注: 每一小節(jié)都會從...
    Polaris_ecf9閱讀 699評論 0 1
  • Axios是近幾年非?;鸬腍TTP請求庫,官網(wǎng)上介紹Axios 是一個基于 promise 的 HTTP 庫,可以...
    milletmi閱讀 3,615評論 0 9

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