這是 源碼拾遺系列 的第一篇文章,閱讀完本文,下面的問題會迎刃而解,html
全文約兩千字,閱讀完大約須要 6 分鐘,文中 Axios 版本爲 0.21.1node
咱們以特性做爲入口,解答上述問題的同時一塊兒感覺下 Axios 源碼極簡封裝的藝術。react
前兩個特性解釋了爲何 Axios 能夠同時用於瀏覽器和 Node.js 的緣由,簡單來講就是經過判斷是服務器仍是瀏覽器環境,來決定使用 XMLHttpRequest
仍是 Node.js 的 HTTP 來建立請求,這個兼容的邏輯被叫作適配器,對應的源碼在 lib/defaults.js
中,ios
// defaults.js
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
複製代碼
以上是適配器的判斷邏輯,經過偵測當前環境的一些全局變量,決定使用哪一個 adapter。 其中對於 Node 環境的判斷邏輯在咱們作 ssr
服務端渲染的時候,也能夠複用。接下來咱們來看一下 Axios 對於適配器的封裝。git
定位到源碼文件 lib/adapters/xhr.js
,先來看下總體結構,github
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
})
}
複製代碼
導出了一個函數,接受一個配置參數,返回一個 Promise。咱們把關鍵的部分提取出來,web
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var request = new XMLHttpRequest();
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
request.onreadystatechange = function handleLoad() {}
request.onabort = function handleAbort() {}
request.onerror = function handleError() {}
request.ontimeout = function handleTimeout() {}
request.send(requestData);
});
};
複製代碼
是否是感受很熟悉?沒錯,這就是 XMLHttpRequest
的使用姿式呀,先建立了一個 xhr 而後 open 啓動請求,監聽 xhr 狀態,而後 send 發送請求。咱們來展開看一下 Axios 對於 onreadystatechange 的處理,spring
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(resolve, reject, response);
// Clean up request
request = null;
};
複製代碼
首先對狀態進行過濾,只有當請求完成時(readyState === 4)才往下處理。 須要注意的是,若是 XMLHttpRequest 請求出錯,大部分的狀況下咱們能夠經過監聽 onerror
進行處理,可是也有一個例外:當請求使用文件協議(file://
)時,儘管請求成功了可是大部分瀏覽器也會返回 0
的狀態碼。json
Axios 針對這個例外狀況也作了處理。axios
請求完成後,就要處理響應了。這裏將響應包裝成一個標準格式的對象,做爲第三個參數傳遞給了 settle 方法,settle 在 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(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
};
複製代碼
settle 對 Promise 的回調進行了簡單的封裝,確保調用按必定的格式返回。
以上就是 xhrAdapter 的主要邏輯,剩下的是對請求頭,支持的一些配置項以及超時,出錯,取消請求等回調的簡單處理,其中對於 XSRF 攻擊的防範是經過請求頭實現的。
咱們先來簡單回顧下什麼是 XSRF (也叫 CSRF,跨站請求僞造)。
背景:用戶登陸後,須要存儲登陸憑證保持登陸態,而不用每次請求都發送帳號密碼。
怎麼樣保持登陸態呢?
目前比較常見的方式是,服務器在收到 HTTP請求後,在響應頭裏添加 Set-Cookie 選項,將憑證存儲在 Cookie 中,瀏覽器接受到響應後會存儲 Cookie,根據瀏覽器的同源策略,下次向服務器發起請求時,會自動攜帶 Cookie 配合服務端驗證從而保持用戶的登陸態。
因此若是咱們沒有判斷請求來源的合法性,在登陸後經過其餘網站向服務器發送了僞造的請求,這時攜帶登陸憑證的 Cookie 就會隨着僞造請求發送給服務器,致使安全漏洞,這就是咱們說的 CSRF,跨站請求僞造。
因此防範僞造請求的關鍵就是檢查請求來源,refferer 字段雖然能夠標識當前站點,可是不夠可靠,如今業界比較通用的解決方案仍是在每一個請求上附帶一個 anti-CSRF token,這個的原理是攻擊者沒法拿到 Cookie,因此咱們能夠經過對 Cookie 進行加密(好比對 sid 進行加密),而後配合服務端作一些簡單的驗證,就能夠判斷當前請求是否是僞造的。
Axios 簡單地實現了對特殊 csrf token 的支持,
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
複製代碼
攔截器是 Axios 的一個特點 Feature,咱們先簡單回顧下使用方式,
// 攔截器能夠攔截請求或響應
// 攔截器的回調將在請求或響應的 then 或 catch 回調前被調用
var instance = axios.create(options);
var requestInterceptor = axios.interceptors.request.use(
(config) => {
// do something before request is sent
return config;
},
(err) => {
// do somthing with request error
return Promise.reject(err);
}
);
// 移除已設置的攔截器
axios.interceptors.request.eject(requestInterceptor)
複製代碼
那麼攔截器是怎麼實現的呢?
定位到源碼 lib/core/Axios.js
第 14 行,
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
複製代碼
經過 Axios 的構造函數能夠看到,攔截器 interceptors 中的 request 和 response 二者都是一個叫作 InterceptorManager 的實例,這個 InterceptorManager 是什麼?
定位到源碼 lib/core/InterceptorManager.js
,
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
複製代碼
InterceptorManager 是一個簡單的事件管理器,實現了對攔截器的管理,
經過 handlers 存儲攔截器,而後提供了添加,移除,遍歷執行攔截器的實例方法,存儲的每個攔截器對象都包含了做爲 Promise 中 resolve 和 reject 的回調以及兩個配置項。
值得一提的是,移除方法是經過直接將攔截器對象設置爲 null 實現的,而不是 splice 剪切數組,遍歷方法中也增長了相應的 null 值處理。這樣作一方面使得每一項ID保持爲項的數組索引不變,另外一方面也避免了從新剪切拼接數組的性能損失。
攔截器的回調會在請求或響應的 then 或 catch 回調前被調用,這是怎麼實現的呢?
回到源碼 lib/core/Axios.js
中第 27 行,Axios 實例對象的 request 方法,
咱們提取其中的關鍵邏輯以下,
Axios.prototype.request = function request(config) {
// Get merged config
// Set config.method
// ...
var requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
複製代碼
能夠看到,當執行 request 時,實際的請求(dispatchRequest)和攔截器是經過一個叫 chain 的隊列來管理的。整個請求的邏輯以下,
這裏的實際請求是對適配器的封裝,請求和響應數據的轉換都在這裏完成。
那麼數據轉換是如何實現的呢?
定位到源碼 lib/core/dispatchRequest.js
,
function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
複製代碼
這裏的 throwIfCancellationRequested 方法用於取消請求,關於取消請求稍後咱們再討論,能夠看到發送請求是經過調用適配器實現的,在調用前和調用後會對請求和響應數據進行轉換。
轉換經過 transformData 函數實現,它會遍歷調用設置的轉換函數,轉換函數將 headers 做爲第二個參數,因此咱們能夠根據 headers 中的信息來執行一些不一樣的轉換操做,
// 源碼 core/transformData.js
function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};
複製代碼
Axios 也提供了兩個默認的轉換函數,用於對請求和響應數據進行轉換。默認狀況下,
Axios 會對請求傳入的 data 作一些處理,好比請求數據若是是對象,會序列化爲 JSON 字符串,響應數據若是是 JSON 字符串,會嘗試轉換爲 JavaScript 對象,這些都是很是實用的功能,
對應的轉換器源碼能夠在 lib/default.js
的第 31 行找到,
var defaults = {
// Line 31
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
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) {
var result = data;
if (utils.isString(result) && result.length) {
try {
result = JSON.parse(result);
} catch (e) { /* Ignore */ }
}
return result;
}],
}
複製代碼
咱們說 Axios 是支持取消請求的,怎麼個取消法呢?
其實不論是瀏覽器端的 xhr 或 Node.js 裏 http 模塊的 request 對象,都提供了 abort 方法用於取消請求,因此咱們只須要在合適的時機調用 abort 就能夠實現取消請求了。
那麼,什麼是合適的時機呢?控制權交給用戶就合適了。因此這個合適的時機應該由用戶決定,也就是說咱們須要將取消請求的方法暴露出去,Axios 經過 CancelToken 實現取消請求,咱們來一塊兒看下它的姿式。
首先 Axios 提供了兩種方式建立 cancel token,
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 方式一,使用 CancelToken 實例提供的靜態屬性 source
axios.post("/user/12345", { name: "monch" }, { cancelToken: source.token });
source.cancel();
// 方式二,使用 CancelToken 構造函數本身實例化
let cancel;
axios.post(
"/user/12345",
{ name: "monch" },
{
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
}
);
cancel();
複製代碼
到底什麼是 CancelToken?定位到源碼 lib/cancel/CancelToken.js
第 11 行,
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 就是一個由 promise 控制的極簡的狀態機,實例化時會在實例上掛載一個 promise,這個 promise 的 resolve 回調暴露給了外部方法 executor,這樣一來,咱們從外部調用這個 executor方法後就會獲得一個狀態變爲 fulfilled 的 promise,那有了這個 promise 後咱們如何取消請求呢?
是否是隻要在請求時拿到這個 promise 實例,而後在 then 回調裏取消請求就能夠了?
定位到適配器的源碼 lib/adapters/xhr.js
第 158 行,
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
複製代碼
以及源碼 lib/adaptors/http.js
第 291 行,
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (req.aborted) return;
req.abort();
reject(cancel);
});
}
複製代碼
果真如此,在適配器裏 CancelToken 實例的 promise 的 then 回調裏調用了 xhr 或 http.request 的 abort 方法。試想一下,若是咱們沒有從外部調用取消 CancelToken 的方法,是否是意味着 resolve 回調不會執行,適配器裏的 promise 的 then 回調也不會執行,就不會調用 abort 取消請求了。
Axios 經過適配器的封裝,使得它能夠在保持同一套接口規範的前提下,同時用在瀏覽器和 node.js 中。源碼中大量使用 Promise 和閉包等特性,實現了一系列的狀態控制,其中對於攔截器,取消請求的實現體現了其極簡的封裝藝術,值得學習和借鑑。
本文首發於個人 博客,才疏學淺,不免有錯誤,文章有誤之處還望不吝指正!
若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤
若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵
(完)