Axios源碼分析 - XHR篇
文章源碼托管在github上,歡迎fork指正!
axios 是一個基於 Promise 的http請求庫,可以用在瀏覽器和node.js中,目前在github上有 42K 的star數
備注:
- 每一小節都會從兩個方面介紹:如何使用 -> 源碼分析
- [工具方法簡單介紹]一節可先跳過,后面用到了再過來查看
- 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
的參數,再如xhrAdapter
http請求適配器方法的參數。config在axios項目里的是非常重要的一條鏈,是用戶跟axios項目內部“通信”的主要橋梁。
axios內部的運作流程圖
工具方法簡單介紹
(注:本節可先跳過,后面用到了再過來查看)
有一些方法在項目中多處使用,簡單介紹下這些方法
1.bind: 給某個函數指定上下文,也就是this指向
bind(fn, context);
實現效果同Function.prototype.bind
方法: fn.bind(context)
2.forEach:遍歷數組或對象
var utils = require('./utils');
var forEach = utils.forEach;
// 數組
utils.forEach([], (value, index, array) => {})
// 對象
utils.forEach({}, (value, key, object) => {})
3.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
}
}
4.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.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
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);
思考
- 是否可以直接 return error?
axios.interceptors.request.use(config => config, error => {
// 是否可以直接 return error ?
return Promise.reject(error);
});
- 如何實現promise的鏈式調用
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都做了哪些事
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為何會有多種使用方式我們知道,
用戶無論以什么方式調用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;
}];
- 修改某次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種寫法太隱蔽了,不如第一種直觀好理解。
發現的問題
-
/lib/adapters/xhr.js文件中,onCanceled方法的參數不應該叫message么,為什么叫cancel?
-
/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,實力絕不是蓋的,值得好好交交心!