axios源碼分析 - XHR篇javascript
axios 是一個基於 Promise 的http請求庫,能夠用在瀏覽器和node.js中,目前在github上有 42K 的star數java
├── /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/
下進行查找node
攔截器 interceptorsios
(若是你熟悉中間件,那麼就很好理解了,由於它起到的就是基於promise的中間件的做用)git
攔截器分爲請求攔截器和響應攔截器,顧名思義: 請求攔截器(interceptors.request
)是指能夠攔截住每次或指定http請求,並可修改配置項 響應攔截器(interceptors.response
)能夠在每次http請求後攔截住每次或指定http請求,並可修改返回結果項。程序員
這裏先簡單說明,後面會作詳細的介紹如何攔截請求響應並修改請求參數修改響應數據。github
數據轉換器 (其實就是對數據進行轉換,好比將對象轉換爲JSON字符串)json
數據轉換器分爲請求轉換器和響應轉換器,顧名思義: 請求轉換器(transformRequest
)是指在請求前對數據進行轉換, 響應轉換器(transformResponse
)主要對請求響應後的響應體作數據轉換。axios
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
的參數,再如xhrAdapter
http請求適配器方法的參數。
config在axios項目裏的是很是重要的一條鏈,是用戶跟axios項目內部「通訊」的主要橋樑。
(注:本節可先跳過,後面用到了再過來查看)
有一些方法在項目中多處使用,簡單介紹下這些方法
bind(fn, context);
複製代碼
實現效果同Function.prototype.bind
方法: fn.bind(context)
var utils = require('./utils');
var forEach = utils.forEach;
// 數組
utils.forEach([], (value, index, array) => {})
// 對象
utils.forEach({}, (value, key, object) => {})
複製代碼
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
}
}
複製代碼
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包引進來
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
,指的是貫穿整個項目的配置項對象, 經過這個對象,能夠設置:
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.defaults
、request
請求的參數config
進行了合併。
由此得出,多處配置的優先級由低到高是: —> 默認配置對象defaults
(/lib/defaults.js
)
—> { method: 'get' }
—> Axios實例屬性this.defaults
—> request
請求的參數config
留給你們思考一個問題: defaults
和 this.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
這裏面的代碼比較複雜,一些方法須要追根溯源才能搞清楚, 因此只需對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);
複製代碼
axios.interceptors.request.use(config => config, error => {
// 是否能夠直接 return error ?
return Promise.reject(error);
});
複製代碼
new People('whr').sleep(3000).eat('apple').sleep(5000).eat('durian');
// 打印結果
// (等待3s)--> 'whr eat apple' -(等待5s)--> 'whr eat durian'
複製代碼
關於攔截器,名詞解釋一節已經作過簡單說明。
每一個axios實例都有一個interceptors
實例屬性, interceptors
對象上有兩個屬性request
、response
。
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主要作了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進行異步處理的?
import axios from 'axios'
axios.get(/**/)
.then(data => {
// 此處能夠拿到向服務端請求回的數據
})
.catch(error => {
// 此處能夠拿到請求失敗或取消或其餘處理失敗的錯誤對象
})
複製代碼
先來一個圖簡單的瞭解下axios項目裏,http請求完成後到達用戶的順序流:
經過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發揮什麼功效呢?
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;
}];
複製代碼
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數據"做爲一個獨立的亮點來介紹的,那麼數據轉換器是如何完成這個功能的呢? 其實很是簡單,咱們一塊兒看下吧。
在默認狀況下,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還帶給了咱們哪些好用的技能點吧。
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種寫法太隱蔽了,不如第一種直觀好理解。
/lib/adapters/xhr.js文件中,onCanceled方法的參數不該該叫message麼,爲何叫cancel?
/lib/adapters/xhr.js文件中,onCanceled方法裏,reject裏應該將config信息也傳出來
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().catch(error => {
const { message } = error;
if (message.indexOf('timeout') > -1){
// 超時處理
}
})
複製代碼
自定義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,實力毫不是蓋的,值得好好交交心!