Axios原始碼深度剖析 – AJAX新王者

小賊先生_ronffy發表於2019-03-03

axios原始碼分析 – XHR篇

文章原始碼託管在github上,歡迎fork指正!

axios 是一個基於 Promise 的http請求庫,可以用在瀏覽器和node.js中,目前在github上有 42K 的star數

備註:

  1. 每一小節都會從兩個方面介紹:如何使用 -> 原始碼分析
  2. [工具方法簡單介紹]一節可先跳過,後面用到了再過來檢視
  3. axios最核心的技術點是如何攔截請求響應並修改請求引數修改響應資料axios是如何用promise搭起基於xhr的非同步橋樑的

axios專案目錄結構


├── /dist/                     # 專案輸出目錄
├── /lib/                      # 專案原始碼目錄
│ ├── /cancel/                 # 定義取消功能
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios的核心主類
│ │ ├── dispatchRequest.js     # 用來呼叫http請求介面卡方法傳送請求
│ │ ├── InterceptorManager.js  # 攔截器建構函式
│ │ └── settle.js              # 根據http響應狀態,改變Promise的狀態
│ ├── /helpers/                # 一些輔助方法
│ ├── /adapters/               # 定義請求的介面卡 xhr、http
│ │ ├── http.js                # 實現http介面卡
│ │ └── xhr.js                 # 實現xhr介面卡
│ ├── axios.js                 # 對外暴露介面
│ ├── defaults.js              # 預設配置 
│ └── utils.js                 # 公用工具
├── package.json               # 專案資訊
├── index.d.ts                 # 配置TypeScript的宣告檔案
└── index.js                   # 入口檔案

複製程式碼

注:因為我們需要要看的程式碼都是/lib/目錄下的檔案,所以以下所有涉及到檔案路徑的地方,
我們都會在/lib/下進行查詢

名詞解釋

  • 攔截器 interceptors

    (如果你熟悉中介軟體,那麼就很好理解了,因為它起到的就是基於promise的中介軟體的作用)

    攔截器分為請求攔截器和響應攔截器,顧名思義:
    請求攔截器(interceptors.request)是指可以攔截住每次或指定http請求,並可修改配置項
    響應攔截器(interceptors.response)可以在每次http請求後攔截住每次或指定http請求,並可修改返回結果項。

    這裡先簡單說明,後面會做詳細的介紹如何攔截請求響應並修改請求引數修改響應資料

  • 資料轉換器 (其實就是對資料進行轉換,比如將物件轉換為JSON字串)

    資料轉換器分為請求轉換器和響應轉換器,顧名思義:
    請求轉換器(transformRequest)是指在請求前對資料進行轉換,
    響應轉換器(transformResponse)主要對請求響應後的響應體做資料轉換。

  • http請求介面卡(其實就是一個方法)

    在axios專案裡,http請求介面卡主要指兩種:XHR、http。
    XHR的核心是瀏覽器端的XMLHttpRequest物件,
    http核心是node的http[s].request方法

    當然,axios也留給了使用者通過config自行配置介面卡的介面的,
    不過,一般情況下,這兩種介面卡就能夠滿足從瀏覽器端向服務端發請求或者從node的http客戶端向服務端發請求的需求。

    本次分享主要圍繞XHR。

  • config配置項 (其實就是一個物件)

    此處我們說的config,在專案內不是真的都叫config這個變數名,這個名字是我根據它的用途起的一個名字,方便大家理解。

    在axios專案中的,設定讀取config時,
    有的地方叫它defaults(/lib/defaults.js),這兒是預設配置項,
    有的地方叫它config,如Axios.prototype.request的引數,再如xhrAdapterhttp請求介面卡方法的引數。

    config在axios專案裡的是非常重要的一條鏈,是使用者跟axios專案內部“通訊”的主要橋樑。

axios內部的運作流程圖

Axios原始碼深度剖析 – AJAX新王者

工具方法簡單介紹

(注:本節可先跳過,後面用到了再過來檢視)

有一些方法在專案中多處使用,簡單介紹下這些方法

  1. bind: 給某個函式指定上下文,也就是this指向

bind(fn, context); 

複製程式碼

實現效果同Function.prototype.bind方法: fn.bind(context)

  1. forEach:遍歷陣列或物件

var utils = require(`./utils`);
var forEach = utils.forEach;

// 陣列
utils.forEach([], (value, index, array) => {})

// 物件
utils.forEach({}, (value, key, object) => {})

複製程式碼
  1. merge:深度合併多個物件為一個物件

var utils = require(`./utils`);
var merge = utils.merge;

var obj1 = {
  a: 1,
  b: {
    bb: 11,
    bbb: 111,
  }
};
var obj2 = {
  a: 2,
  b: {
    bb: 22,
  }
};
var mergedObj = merge(obj1, obj2); 

複製程式碼

mergedObj物件是:


{ 
  a: 2, 
  b: { 
    bb: 22, 
    bbb: 111 
  } 
}

複製程式碼
  1. extend:將一個物件的方法和屬性擴充套件到另外一個物件上,並指定上下文

var utils = require(`./utils`);
var extend = utils.extend;

var context = {
  a: 4,
};
var target = {
  k: `k1`,
  fn(){
    console.log(this.a + 1)
  }
};
var source = {
  k: `k2`,
  fn(){
    console.log(this.a - 1)
  }
};
let extendObj = extend(target, source, context);

複製程式碼

extendObj物件是:


{
  k: `k2`,
  fn: source.fn.bind(context),
}

複製程式碼

執行extendObj.fn();, 列印3

axios為何會有多種使用方式

如何使用


// 首先將axios包引進來
import axios from `axios`

複製程式碼

第1種使用方式:axios(option)


axios({
  url,
  method,
  headers,
})

複製程式碼

第2種使用方式:axios(url[, option])


axios(url, {
  method,
  headers,
})

複製程式碼

第3種使用方式(對於get、delete等方法):axios[method](url[, option])


axios.get(url, {
  headers,
})

複製程式碼

第4種使用方式(對於post、put等方法):axios[method](url[, data[, option]])


axios.post(url, data, {
  headers,
})

複製程式碼

第5種使用方式:axios.request(option)


axios.request({
  url,
  method,
  headers,
})

複製程式碼

原始碼分析

作為axios專案的入口檔案,我們先來看下axios.js的原始碼
能夠實現axios的多種使用方式的核心是createInstance方法:


// /lib/axios.js
function createInstance(defaultConfig) {
  // 建立一個Axios例項
  var context = new Axios(defaultConfig);

  // 以下程式碼也可以這樣實現:var instance = Axios.prototype.request.bind(context);
  // 這樣instance就指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式呼叫 
  // Axios.prototype.request 內對第一個引數的資料型別判斷,使我們能夠以 instance(url, option) 方式呼叫
  var instance = bind(Axios.prototype.request, context);

  // 把Axios.prototype上的方法擴充套件到instance物件上,
  // 這樣 instance 就有了 get、post、put等方法
  // 並指定上下文為context,這樣執行Axios原型鏈上的方法時,this會指向context
  utils.extend(instance, Axios.prototype, context);

  // 把context物件上的自身屬性和方法擴充套件到instance上
  // 注:因為extend內部使用的forEach方法對物件做for in 遍歷時,只遍歷物件本身的屬性,而不會遍歷原型鏈上的屬性
  // 這樣,instance 就有了  defaults、interceptors 屬性。(這兩個屬性後面我們會介紹)
  utils.extend(instance, context);

  return instance;
}

// 接收預設配置項作為引數(後面會介紹配置項),建立一個Axios例項,最終會被作為物件匯出
var axios = createInstance(defaults);

複製程式碼

以上程式碼看上去很繞,其實createInstance最終是希望拿到一個Function,這個Function指向Axios.prototype.request,這個Function還會有Axios.prototype上的每個方法作為靜態方法,且這些方法的上下文都是指向同一個物件。

那麼在來看看Axios、Axios.prototype.request的原始碼是怎樣的?

Axios是axios包的核心,一個Axios例項就是一個axios應用,其他方法都是對Axios內容的擴充套件
Axios建構函式的核心方法是request方法,各種axios的呼叫方式最終都是通過request方法發請求的


// /lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  // ...省略程式碼
};

// 為支援的請求方法提供別名
utils.forEach([`delete`, `get`, `head`, `options`], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach([`post`, `put`, `patch`], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

複製程式碼

通過以上程式碼,我們就可以以多種方式發起http請求了: axios()、axios.get()、axios.post()

一般情況,專案使用預設匯出的axios例項就可以滿足需求了,
如果不滿足需求需要建立新的axios例項,axios包也預留了介面,
看下面的程式碼:


// /lib/axios.js  -  31行
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstance(utils.merge(defaults, instanceConfig));
};

複製程式碼

說完axios為什麼會有這麼多種使用方式,可能你心中會有一個疑問:
使用axios時,無論get方法還是post方法,最終都是呼叫的Axios.prototype.request方法,那麼這個方法是怎麼根據我們的config配置發請求的呢?

在開始說Axios.prototype.request之前,我們先來捋一捋在axios專案中,使用者配置的config是怎麼起作用的?

使用者配置的config是怎麼起作用的

這裡說的config,指的是貫穿整個專案的配置項物件,
通過這個物件,可以設定:

http請求介面卡、請求地址、請求方法、請求頭header、 請求資料、請求或響應資料的轉換、請求進度、http狀態碼驗證規則、超時、取消請求等

可以發現,幾乎axios所有的功能都是通過這個物件進行配置和傳遞的,
既是axios專案內部的溝通橋樑,也是使用者跟axios進行溝通的橋樑。

首先我們看看,使用者能以什麼方式定義配置項:


import axios from `axios`

// 第1種:直接修改Axios例項上defaults屬性,主要用來設定通用配置
axios.defaults[configName] = value;

// 第2種:發起請求時最終會呼叫Axios.prototype.request方法,然後傳入配置項,主要用來設定“個例”配置
axios({
  url,
  method,
  headers,
})

// 第3種:新建一個Axios例項,傳入配置項,此處設定的是通用配置
let newAxiosInstance = axios.create({
  [configName]: value,
})

複製程式碼

看下 Axios.prototype.request 方法裡的一行程式碼: (/lib/core/Axios.js – 第35行)


config = utils.merge(defaults, {method: `get`}, this.defaults, config);

複製程式碼

可以發現此處將預設配置物件defaults/lib/defaults.js)、Axios例項屬性this.defaultsrequest請求的引數config進行了合併。

由此得出,多處配置的優先順序由低到高是:
—> 預設配置物件defaults/lib/defaults.js)
—> { method: `get` }
—> Axios例項屬性this.defaults
—> request請求的引數config

留給大家思考一個問題: defaultsthis.defaults 什麼時候配置是相同的,什麼時候是不同的?

至此,我們已經得到了將多處merge後的config物件,那麼這個物件在專案中又是怎樣傳遞的呢?


Axios.prototype.request = function request(config) {
  // ...
  config = utils.merge(defaults, {method: `get`}, this.defaults, config);

  var chain = [dispatchRequest, undefined];
  // 將config物件當作引數傳給Primise.resolve方法
  var promise = Promise.resolve(config);

  // ...省略程式碼
  
  while (chain.length) {
    // config會按序通過 請求攔截器 - dispatchRequest方法 - 響應攔截器
    // 關於攔截器 和 dispatchRequest方法,下面會作為一個專門的小節來介紹。
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

複製程式碼

至此,config走完了它傳奇的一生 -_-
下一節就要說到重頭戲了: Axios.prototype.request

axios.prototype.request

這裡面的程式碼比較複雜,一些方法需要追根溯源才能搞清楚,
所以只需對chain陣列有個簡單的瞭解就好,涉及到的攔截器、[dispatchRequest]後面都會詳細介紹

chain陣列是用來盛放攔截器方法和dispatchRequest方法的,
通過promise從chain陣列裡按序取出回撥函式逐一執行,最後將處理後的新的promise在Axios.prototype.request方法裡返回出去,
並將response或error傳送出去,這就是Axios.prototype.request的使命了。

檢視原始碼:


// /lib/core/Axios.js
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;
};

複製程式碼

此時,你一定對攔截器充滿了好奇,這個攔截器到底是個什麼傢伙,下一節就讓我們一探究竟吧

如何攔截請求響應並修改請求引數修改響應資料

如何使用


// 新增請求攔截器
const myRequestInterceptor = axios.interceptors.request.use(config => {
    // 在傳送http請求之前做些什麼
    return config; // 有且必須有一個config物件被返回
}, error => {
    // 對請求錯誤做些什麼
    return Promise.reject(error);
});

// 新增響應攔截器
axios.interceptors.response.use(response => {
  // 對響應資料做點什麼
  return response; // 有且必須有一個response物件被返回
}, error => {
  // 對響應錯誤做點什麼
  return Promise.reject(error);
});

// 移除某次攔截器
axios.interceptors.request.eject(myRequestInterceptor);

複製程式碼

思考

  1. 是否可以直接 return error?

axios.interceptors.request.use(config => config, error => {
  // 是否可以直接 return error ?
  return Promise.reject(error); 
});

複製程式碼
  1. 如何實現promise的鏈式呼叫

new People(`whr`).sleep(3000).eat(`apple`).sleep(5000).eat(`durian`);

// 列印結果
// (等待3s)--> `whr eat apple` -(等待5s)--> `whr eat durian`

複製程式碼

原始碼分析

關於攔截器,名詞解釋一節已經做過簡單說明。

每個axios例項都有一個interceptors例項屬性,
interceptors物件上有兩個屬性requestresponse


function Axios(instanceConfig) {
  // ...
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

複製程式碼

這兩個屬性都是一個InterceptorManager例項,而這個InterceptorManager建構函式就是用來管理攔截器的。

我們先來看看InterceptorManager建構函式:

InterceptorManager建構函式就是用來實現攔截器的,這個建構函式原型上有3個方法:use、eject、forEach。
關於原始碼,其實是比較簡單的,都是用來操作該建構函式的handlers例項屬性的。


// /lib/core/InterceptorManager.js

function InterceptorManager() {
  this.handlers = []; // 存放攔截器方法,陣列內每一項都是有兩個屬性的物件,兩個屬性分別對應成功和失敗後執行的函式。
}

// 往攔截器裡新增攔截方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// 用來登出指定的攔截器
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 遍歷this.handlers,並將this.handlers裡的每一項作為引數傳給fn執行
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};


複製程式碼

那麼當我們通過axios.interceptors.request.use新增攔截器後,
axios內部又是怎麼讓這些攔截器能夠在請求前、請求後拿到我們想要的資料的呢?

先看下程式碼:


// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];

  // 初始化一個promise物件,狀態微resolved,接收到的引數微config物件
  var promise = Promise.resolve(config);

  // 注意:interceptor.fulfilled 或 interceptor.rejected 是可能為undefined
  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);
  });

  // 新增了攔截器後的chain陣列大概會是這樣的:
  // [
  //   requestFulfilledFn, requestRejectedFn, ..., 
  //   dispatchRequest, undefined,
  //   responseFulfilledFn, responseRejectedFn, ....,
  // ]

  // 只要chain陣列長度不為0,就一直執行while迴圈
  while (chain.length) {
    // 陣列的 shift() 方法用於把陣列的第一個元素從其中刪除,並返回第一個元素的值。
    // 每次執行while迴圈,從chain陣列裡按序取出兩項,並分別作為promise.then方法的第一個和第二個引數

    // 按照我們使用InterceptorManager.prototype.use新增攔截器的規則,正好每次新增的就是我們通過InterceptorManager.prototype.use方法新增的成功和失敗回撥

    // 通過InterceptorManager.prototype.use往攔截器陣列裡新增攔截器時使用的陣列的push方法,
    // 對於請求攔截器,從攔截器陣列按序讀到後是通過unshift方法往chain陣列數裡新增的,又通過shift方法從chain陣列裡取出的,所以得出結論:對於請求攔截器,先新增的攔截器會後執行
    // 對於響應攔截器,從攔截器陣列按序讀到後是通過push方法往chain陣列裡新增的,又通過shift方法從chain陣列裡取出的,所以得出結論:對於響應攔截器,新增的攔截器先執行

    // 第一個請求攔截器的fulfilled函式會接收到promise物件初始化時傳入的config物件,而請求攔截器又規定使用者寫的fulfilled函式必須返回一個config物件,所以通過promise實現鏈式呼叫時,每個請求攔截器的fulfilled函式都會接收到一個config物件

    // 第一個響應攔截器的fulfilled函式會接受到dispatchRequest(也就是我們的請求方法)請求到的資料(也就是response物件),而響應攔截器又規定使用者寫的fulfilled函式必須返回一個response物件,所以通過promise實現鏈式呼叫時,每個響應攔截器的fulfilled函式都會接收到一個response物件

    // 任何一個攔截器的丟擲的錯誤,都會被下一個攔截器的rejected函式收到,所以dispatchRequest丟擲的錯誤才會被響應攔截器接收到。

    // 因為axios是通過promise實現的鏈式呼叫,所以我們可以在攔截器裡進行非同步操作,而攔截器的執行順序還是會按照我們上面說的順序執行,也就是 dispatchRequest 方法一定會等待所有的請求攔截器執行完後再開始執行,響應攔截器一定會等待 dispatchRequest 執行完後再開始執行。

    promise = promise.then(chain.shift(), chain.shift());

  }

  return promise;
};

複製程式碼

現在,你應該已經清楚了攔截器是怎麼回事,以及攔截器是如何在Axios.prototype.request方法裡發揮作用的了,
那麼處於”中游位置”的dispatchRequest是如何傳送http請求的呢?

dispatchrequest都做了哪些事

dispatchRequest主要做了3件事:
1,拿到config物件,對config進行傳給http請求介面卡前的最後處理;
2,http請求介面卡根據config配置,發起請求
3,http請求介面卡請求完成後,如果成功則根據header、data、和config.transformResponse(關於transformResponse,下面的資料轉換器會進行講解)拿到資料轉換後的response,並return。


// /lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Support baseURL config
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // Ensure headers exist
  config.headers = config.headers || {};

  // 對請求data進行轉換
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 對header進行合併處理
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 刪除header屬性裡無用的屬性
  utils.forEach(
    [`delete`, `get`, `head`, `post`, `put`, `patch`, `common`],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // http請求介面卡會優先使用config上自定義的介面卡,沒有配置時才會使用預設的XHR或http介面卡,不過大部分時候,axios提供的預設介面卡是能夠滿足我們的
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(/**/);
};

複製程式碼

好了,看到這裡,我們是時候梳理一下:axios是如何用promise搭起基於xhr的非同步橋樑的?

axios是如何用promise搭起基於xhr的非同步橋樑的

axios是如何通過Promise進行非同步處理的?

如何使用


import axios from `axios`

axios.get(/**/)
.then(data => {
  // 此處可以拿到向服務端請求回的資料
})
.catch(error => {
  // 此處可以拿到請求失敗或取消或其他處理失敗的錯誤物件
})

複製程式碼

原始碼分析

先來一個圖簡單的瞭解下axios專案裡,http請求完成後到達使用者的順序流:

Axios原始碼深度剖析 – AJAX新王者

通過axios為何會有多種使用方式我們知道,
使用者無論以什麼方式呼叫axios,最終都是呼叫的Axios.prototype.request方法,
這個方法最終返回的是一個Promise物件。


Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];
  // 將config物件當作引數傳給Primise.resolve方法
  var promise = Promise.resolve(config);

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

  return promise;
};

複製程式碼

Axios.prototype.request方法會呼叫dispatchRequest方法,而dispatchRequest方法會呼叫xhrAdapter方法,xhrAdapter方法返回的是還一個Promise物件


// /lib/adapters/xhr.js
function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ... 省略程式碼
  });
};

複製程式碼

xhrAdapter內的XHR傳送請求成功後會執行這個Promise物件的resolve方法,並將請求的資料傳出去,
反之則執行reject方法,並將錯誤資訊作為引數傳出去。


// /lib/adapters/xhr.js
var request = new XMLHttpRequest();
var loadEvent = `onreadystatechange`;

request[loadEvent] = function handleLoad() {
  // ...
  // 往下走有settle的原始碼
  settle(resolve, reject, response);
  // ...
};
request.onerror = function handleError() {
  reject(/**/);
  request = null;
};
request.ontimeout = function handleTimeout() {
  reject(/**/);
  request = null;
};

複製程式碼

驗證服務端的返回結果是否通過驗證:


// /lib/core/settle.js
function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(/**/);
  }
};

複製程式碼

回到dispatchRequest方法內,首先得到xhrAdapter方法返回的Promise物件,
然後通過.then方法,對xhrAdapter返回的Promise物件的成功或失敗結果再次加工,
成功的話,則將處理後的response返回,
失敗的話,則返回一個狀態為rejected的Promise物件,


  return adapter(config).then(function onAdapterResolution(response) {
    // ...
    return response;
  }, function onAdapterRejection(reason) {
    // ...
    return Promise.reject(reason);
  });
};

複製程式碼

那麼至此,使用者呼叫axios()方法時,就可以直接呼叫Promise的.then.catch進行業務處理了。

回過頭來,我們在介紹dispatchRequest一節時說到的資料轉換,而axios官方也將資料轉換專門作為一個亮點來介紹的,那麼資料轉換到底能在使用axios發揮什麼功效呢?

資料轉換器-轉換請求與響應資料

如何使用

  1. 修改全域性的轉換器

import axios from `axios`

// 往現有的請求轉換器裡增加轉換方法
axios.defaults.transformRequest.push((data, headers) => {
  // ...處理data
  return data;
});

// 重寫請求轉換器
axios.defaults.transformRequest = [(data, headers) => {
  // ...處理data
  return data;
}];

// 往現有的響應轉換器裡增加轉換方法
axios.defaults.transformResponse.push((data, headers) => {
  // ...處理data
  return data;
});

// 重寫響應轉換器
axios.defaults.transformResponse = [(data, headers) => {
  // ...處理data
  return data;
}];

複製程式碼
  1. 修改某次axios請求的轉換器

import axios from `axios`

// 往已經存在的轉換器裡增加轉換方法
axios.get(url, {
  // ...
  transformRequest: [
    ...axios.defaults.transformRequest, // 去掉這行程式碼就等於重寫請求轉換器了
    (data, headers) => {
      // ...處理data
      return data;
    }
  ],
  transformResponse: [
    ...axios.defaults.transformResponse, // 去掉這行程式碼就等於重寫響應轉換器了
    (data, headers) => {
      // ...處理data
      return data;
    }
  ],
})

複製程式碼

原始碼分析

預設的defaults配置項裡已經自定義了一個請求轉換器和一個響應轉換器,
看下原始碼:


// /lib/defaults.js
var defaults = {

  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, `Content-Type`);
    // ...
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, `application/x-www-form-urlencoded;charset=utf-8`);
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, `application/json;charset=utf-8`);
      return JSON.stringify(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    if (typeof data === `string`) {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],
  
};

複製程式碼

那麼在axios專案裡,是在什麼地方使用了轉換器呢?

請求轉換器的使用地方是http請求前,使用請求轉換器對請求資料做處理,
然後傳給http請求介面卡使用。


// /lib/core/dispatchRequest.js
function dispatchRequest(config) {
  
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  return adapter(config).then(/* ... */);
};

複製程式碼

看下transformData方法的程式碼,
主要遍歷轉換器陣列,分別執行每一個轉換器,根據data和headers引數,返回新的data。


// /lib/core/transformData.js
function transformData(data, headers, fns) {
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });
  return data;
};

複製程式碼

響應轉換器的使用地方是在http請求完成後,根據http請求介面卡的返回值做資料轉換處理:


// /lib/core/dispatchRequest.js
return adapter(config).then(function onAdapterResolution(response) {
    // ...
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      // ...
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });

複製程式碼

轉換器和攔截器的關係?

攔截器同樣可以實現轉換請求和響應資料的需求,但根據作者的設計和綜合程式碼可以看出,
在請求時,攔截器主要負責修改config配置項,資料轉換器主要負責轉換請求體,比如轉換物件為字串
在請求響應後,攔截器可以拿到response,資料轉換器主要負責處理響應體,比如轉換字串為物件。

axios官方是將”自動轉換為JSON資料”作為一個獨立的亮點來介紹的,那麼資料轉換器是如何完成這個功能的呢?
其實非常簡單,我們一起看下吧。

自動轉換json資料

在預設情況下,axios將會自動的將傳入的data物件序列化為JSON字串,將響應資料中的JSON字串轉換為JavaScript物件

原始碼分析


// 請求時,將data資料轉換為JSON 字串
// /lib/defaults.js 
transformRequest: [function transformRequest(data, headers) {
  // ...
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, `application/json;charset=utf-8`);
      return JSON.stringify(data);
    }
    return data;
}]

// 得到響應後,將請求到的資料轉換為JSON物件
// /lib/defaults.js
transformResponse: [function transformResponse(data) {
    if (typeof data === `string`) {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
}]

複製程式碼

至此,axios專案的運作流程已經介紹完畢,是不是已經打通了任督二脈了呢
接下來我們一起看下axios還帶給了我們哪些好用的技能點吧。

header設定

如何使用


import axios from `axios`

// 設定通用header
axios.defaults.headers.common[`X-Requested-With`] = `XMLHttpRequest`; // xhr標識

// 設定某種請求的header
axios.defaults.headers.post[`Content-Type`] = `application/x-www-form-urlencoded;charset=utf-8`;

// 設定某次請求的header
axios.get(url, {
  headers: {
    `Authorization`: `whr1`,
  },
})

複製程式碼

原始碼分析


// /lib/core/dispatchRequest.js  -  44行

  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

複製程式碼

如何取消已經傳送的請求

如何使用


import axios from `axios`

// 第一種取消方法
axios.get(url, {
  cancelToken: new axios.CancelToken(cancel => {
    if (/* 取消條件 */) {
      cancel(`取消日誌`);
    }
  })
});

// 第二種取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
  cancelToken: source.token
});
source.cancel(`取消日誌`);

複製程式碼

原始碼分析


// /cancel/CancelToken.js  -  11行
function CancelToken(executor) {
 
  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);
  });
}

// /lib/adapters/xhr.js  -  159行
if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        request.abort();
        reject(cancel);
        request = null;
    });
}

複製程式碼

取消功能的核心是通過CancelToken內的this.promise = new Promise(resolve => resolvePromise = resolve)
得到例項屬性promise,此時該promise的狀態為pending
通過這個屬性,在/lib/adapters/xhr.js檔案中繼續給這個promise例項新增.then方法
xhr.js檔案的159行config.cancelToken.promise.then(message => request.abort()));

CancelToken外界,通過executor引數拿到對cancel方法的控制權,
這樣當執行cancel方法時就可以改變例項的promise屬性的狀態為rejected
從而執行request.abort()方法達到取消請求的目的。

上面第二種寫法可以看作是對第一種寫法的完善,
因為很多是時候我們取消請求的方法是用在本次請求方法外,
例如,傳送A、B兩個請求,當B請求成功後,取消A請求。


// 第1種寫法:
let source;
axios.get(Aurl, {
  cancelToken: new axios.CancelToken(cancel => {
    source = cancel;
  })
});
axios.get(Burl)
.then(() => source(`B請求成功了`));

// 第2種寫法:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(Aurl, {
  cancelToken: source.token
});
axios.get(Burl)
.then(() => source.cancel(`B請求成功了`));

複製程式碼

相對來說,我更推崇第1種寫法,因為第2種寫法太隱蔽了,不如第一種直觀好理解。

發現的問題
  1. /lib/adapters/xhr.js檔案中,onCanceled方法的引數不應該叫message麼,為什麼叫cancel?

  2. /lib/adapters/xhr.js檔案中,onCanceled方法裡,reject裡應該將config資訊也傳出來

跨域攜帶cookie

如何使用


import axios from `axios`

axios.defaults.withCredentials = true;

複製程式碼

原始碼分析

我們在使用者配置的config是怎麼起作用的一節已經介紹了config在axios專案裡的傳遞過程,
由此得出,我們通過axios.defaults.withCredentials = true做的配置,
/lib/adapters/xhr.js裡是可以取到的,然後通過以下程式碼配置到xhr物件項。


var request = new XMLHttpRequest();

// /lib/adapters/xhr.js
if (config.withCredentials) {
  request.withCredentials = true;
}

複製程式碼

超時配置及處理

如何使用


import axios from `axios`

axios.defaults.timeout = 3000;

複製程式碼

原始碼分析


// /adapters/xhr.js
request.timeout = config.timeout;

// /adapters/xhr.js
// 通過createError方法,將錯誤資訊合為一個字串
request.ontimeout = function handleTimeout() {
  reject(createError(`timeout of ` + config.timeout + `ms exceeded`, 
    config, `ECONNABORTED`, request));
};

複製程式碼
  • axios庫外如何新增超時後的處理

axios().catch(error => {
  const { message } = error;
  if (message.indexOf(`timeout`) > -1){
    // 超時處理
  }
})

複製程式碼

改寫驗證成功或失敗的規則validatestatus

自定義http狀態碼的成功、失敗範圍

如何使用


import axios from `axios`

axios.defaults.validateStatus = status => status >= 200 && status < 300;

複製程式碼

原始碼分析

在預設配置中,定義了預設的http狀態碼驗證規則,
所以自定義validateStatus其實是對此處方法的重寫


// `/lib/defaults.js`
var defaults = {
  // ...
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },
  // ...
}

複製程式碼

axios是何時開始驗證http狀態碼的?


// /lib/adapters/xhr.js
var request = new XMLHttpRequest();
var loadEvent = `onreadystatechange`;

// /lib/adapters/xhr.js
// 每當 readyState 改變時,就會觸發 onreadystatechange 事件
request[loadEvent] = function handleLoad() {
  if (!request || (request.readyState !== 4 && !xDomain)) {
    return;
  }
  // ...省略程式碼
  var response = {
      // ...
      // IE sends 1223 instead of 204 (https://github.com/axios/axios/issues/201)
      status: request.status === 1223 ? 204 : request.status,
      config: config,
  };
  settle(resolve, reject, response);
  // ...省略程式碼
}

複製程式碼

// /lib/core/settle.js
function settle(resolve, reject, response) {
  // 如果我們往上搗一搗就會發現,config物件的validateStatus就是我們自定義的validateStatus方法或預設的validateStatus方法
  var validateStatus = response.config.validateStatus;
  // validateStatus驗證通過,就會觸發resolve方法
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      `Request failed with status code ` + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

複製程式碼

總結

axios這個專案裡,有很多對JS使用很巧妙的地方,比如對promise的串聯操作(當然你也可以說這塊是借鑑很多非同步中介軟體的處理方式),讓我們可以很方便對請求前後的各種處理方法的流程進行控制;很多實用的小優化,比如請求前後的資料處理,省了程式設計師一遍一遍去寫JSON.xxx了;同時支援了瀏覽器和node兩種環境,對使用node的專案來說無疑是極好的。

總之,這個能夠在github斬獲42K+(截止2018.05.27)的star,實力絕不是蓋的,值得好好交交心!

相關文章