# 📁 lib
# |—— 📁 adapters // axios主要使用的請求方法
# |—— |—— 📃 http.js // axios中node端使用的請求函數
# |—— |—— 📃 xhr.js // axios中瀏覽器端使用的請求函數
# |—— 📁 cancel
# |—— |—— 📃 Cancel.js // 定義了,取消請求返回的信息結構
# |—— |—— 📃 CancelToken.js // 定義了用於取消請求的主要方法
# |—— |—— 📃 isCancel.js // 判斷是不是取消請求的信息
# |—— 📁 core
# |—— |—— 📃 Axios.js // Axios類
# |—— |—— 📃 dispatchRequest.js // 發起請求的地方
# |—— |—— 📃 InterceptorManager.js // InterceptorManager類,攔截器類
# |—— |—— 📃 mergeConfig.js // 合併配置項
# |—— |—— 📃 settle.js // 根據請求狀態,處理Promise
# |—— |—— 📃 createError.js // 生成指定的error
# |—— |—— 📃 enhanceError.js // 指定error對象的toJSON方法
# |—— |—— 📃 transformData.js // 使用default.js中transformRequest和transformResponse對響應以及請求進行格式化
# |—— 📁 helpers
# |—— |—— 📃 bind.js // 工具函數
# |—— |—— 📃 parseHeaders.js // 將getAllResponseHeaders返回的header信息轉化爲對象
# |—— |—— 📃 buildURL.js // 將params參數
# |—— |—— 📃 cookies.js // 封裝了讀取,寫入,刪除cookies的方法
# |—— |—— 📃 isURLSameOrigin.js // 檢測當前的url與請求的url是否同源
# |—— |—— 📃 normalizeHeaderName.js // 對對象屬性名的進行格式化,刪除,新建符合大小寫規範的屬性
# |—— |—— 📃 combineURLs.js // 組合baseurl
# |—— |—— 📃 isAbsoluteURL.js // 判斷是否爲絕對路徑(指的://或//開頭的爲絕對路徑)
# |—— 📃 axios.js
# |—— 📃 defaults.js // axios中默認配置
# |—— 📃 utils.js // 一些工具方法
# |—— |—— ⏹ isFormData // 判斷是不是formData對象
# |—— |—— ⏹ isStandardBrowserEnv // 判斷當前環境是否爲標準瀏覽器環境
# |—— |—— ⏹ isUndefined // 判斷是否爲undefined
# |—— |—— ⏹ merge
# |—— |—— ⏹ isURLSearchParams // 判斷是否爲URLSearchParams對象
複製代碼
本文主要關注axios中主流程的源碼,對於一些工具函數的實現會略過。還請見諒。若是文章中有錯誤的地方,還請及時指出。node
下面是axios源碼中發起一個請求時代碼大體的流程ios
CancelToken.js中定義了取消axios請求的相關行爲的代碼。但CancelToken.source返回的取消請求的cancel方法,使用的前提,是須要將CancelToken.source返回token的,結合到具體的請求的config中才能正常使用。ajax
我在看axios源碼以前,甚至並不知道axios能夠發出的請求,因此咱們先來了解下如何在axios取消一個請求。下面是一個例子🌰json
// axios用於取消請求的類
const CancelToken = axios.CancelToken
// source方法會返回一個對象,對象包含
// {
// token, 添加到請求的config,用於標識請求
// cancel, 調用cancel方法取消請求
// }
const source = CancelToken.source()
axios.get('/info', {
cancelToken: source.token
}).catch(function(error) {
if (axios.isCancel(error)) {
console.log('取消請求的錯誤')
} else {
// 其餘錯誤
}
})
// 調用source.cancel能夠取消axios.get('/info')的請求
source.cancel('取消請求')
複製代碼
var Cancel = require('./Cancel');
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
// 建立一個Promise
// 在調用cancel函數前該promise會一直處於pending狀態
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
// 判斷是否已經取消請求了
if (token.reason) {
return;
}
// 建立取消請求的信息,並將信息添加到實例的reason屬性上
token.reason = new Cancel(message);
// 結束this.promise的pending狀態
// 將this.promise狀態設置爲resolve
resolvePromise(token.reason);
});
}
// 判斷該請求是否已經被取消的方法
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
複製代碼
🤔 看到這裏,咱們仍是沒法瞭解axios是如何取消一個請求的。由於單獨使用CancelToken.source返回的cancel是沒法取消一個請求的,咱們須要結合xhr.js中的代碼來理解。axios
// /lib/adapters/xhr.js
request.open()
// ...省略
// 若是配置了cancelToken選項
if (config.cancelToken) {
// 對CancelToken中建立的Promise添加成功的回調
// 當調用CancelToken.source暴露的cancel函數時,回調會被觸發
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 取消xhr請求
request.abort();
// 將axios返回的promise,置爲reject態
reject(cancel);
request = null;
});
}
// ...省略
request.send()
複製代碼
想必你們看到這裏,對axios中如何請求有了一個大體的瞭解。咱們來總結一下,咱們經過CancelToken,建立了一個額外的PromiseA,並將PromiseA掛載到config中,同時將該PromiseA的resolve方法暴露出去。咱們在調用send方法前(發送請求前)添加對PromiseA的狀態進行監聽,當PromiseA的狀態被修改,咱們會在PromiseA的callback中取消請求,而且將axios返回的PromiseB的狀態置爲reject。從而達到取消請求的目的後端
xhr.js導出的xhrAdapter方法是axios在瀏覽器環境下使用的默認請求方法。咱們能夠在配置中使用adapter配置項對默認請求方法進行替換。跨域
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
// 判斷是不是FormData對象, 若是是, 刪除header的Content-Type字段,讓瀏覽器自動設置Content-Type字段
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type'];
}
// 建立xtr對象
var request = new XMLHttpRequest();
// 設置http請求頭中的Authorization字段
// 關於Authorization字段
// 更多內容參考https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Authorization
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password || '';
// 使用btoa方法base64編碼username和password
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// 初始化請求方法
// open(method: 請求的http方法, url: 請求的url地址, 是否支持異步)
request.open(
config.method.toUpperCase(),
buildURL(config.url, config.params, config.paramsSerializer),
true
);
// 設置超時時間
request.timeout = config.timeout;
// 監聽readyState狀態的變化,當readyState狀態爲4的時候,表示ajax請求成功
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// request.status響應的數字狀態碼,在完成請求前數字狀態碼等於0
// 若是request.status出錯返回的也是0,可是file協議除外,status等於0也是一個成功的請求
// 更多內容請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/status
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// getAllResponseHeaders方法會返回全部的響應頭
// 更多內容請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/getAllResponseHeaders
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
// 若是沒有設置數據響應類型(默認爲「json」)或者responseType設置爲text時,獲取request.responseText值不然是獲取request.response
// responseType是一個枚舉類型,手動設置返回數據的類型 更多請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseType
// responseText是所有後端的返回數據爲純文本的值 更多請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseText
// response爲正文,response的類型取決於responseType 更多請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/response
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
};
// status >= 200 && status < 300 resolve
// 不然reject
settle(resolve, reject, response);
request = null;
};
// ajax中斷時觸發
request.onabort = function handleAbort() {
if (!request) {
return;
}
// 拋出Request aborted錯誤
reject(createError('Request aborted', config, 'ECONNABORTED', request));
request = null;
};
// ajax失敗時觸發
request.onerror = function handleError() {
// 拋出Network Error錯誤
reject(createError('Network Error', config, null, request));
request = null;
};
// ajax請求超時時調用
request.ontimeout = function handleTimeout() {
// 拋出 timeout錯誤
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
request));
request = null;
};
// 判斷當前是爲標準瀏覽器環境,若是是,添加xsrf頭
// 什麼是xsrf header? xsrf header是用來防護CSRF攻擊
// 原理是服務端生成一個XSRF-TOKEN,並保存到瀏覽器的cookie中,在每次請求中ajax都會將XSRF-TOKEN設置到request header中
// 服務器會比較cookie中的XSRF-TOKEN與header中XSRF-TOKEN是否一致
// 根據同源策略,非同源的網站沒法讀取修改本源的網站cookie,避免了僞造cookie
if (utils.isStandardBrowserEnv()) {
var cookies = require('./../helpers/cookies');
// withCredentials設置跨域請求中是否應該使用cookie 更多請參考 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/withCredentials
// (設置了withCredentials爲true或者是同源請求)而且設置xsrfCookieName
var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
// 讀取cookie中XSRF-TOKEN
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
// 在request header中設置XSRF-TOKEN
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// setRequestHeader是用來設置請求頭部的方法
if ('setRequestHeader' in request) {
// 將config中配置的requestHeaders,循環設置到請求頭上
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
delete requestHeaders[key];
} else {
request.setRequestHeader(key, val);
}
});
}
// 設置xhr對象的withCredentials屬性,是否容許cookie進行跨域請求
if (config.withCredentials) {
request.withCredentials = true;
}
// 設置xhr對象的responseType屬性
if (config.responseType) {
try {
request.responseType = config.responseType;
} catch (e) {
if (config.responseType !== 'json') {
throw e;
}
}
}
// 下載進度
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// 上傳進度
// request.upload XMLHttpRequest.upload 屬性返回一個 XMLHttpRequestUpload對象,用來表示上傳的進度
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
if (config.cancelToken) {
// 取消請求,在介紹/lib/cancel/CancelToken.js中以及介紹,這裏不在贅述
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
if (requestData === undefined) {
requestData = null;
}
// 發送http請求
request.send(requestData);
});
};
複製代碼
dispatchRequest.js文件是axios源碼中實際調用請求的地方。數組
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
// 判斷請求是否已被取消,若是已經被取消,拋出已取消
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// 若是包含baseUrl, 而且不是config.url絕對路徑,組合baseUrl以及config.url
if (config.baseURL && !isAbsoluteURL(config.url)) {
// 組合baseURL與url造成完整的請求路徑
config.url = combineURLs(config.baseURL, config.url);
}
config.headers = config.headers || {};
// 使用/lib/defaults.js中的transformRequest方法,對config.headers和config.data進行格式化
// 好比將headers中的Accept,Content-Type統一處理成大寫
// 好比若是請求正文是一個Object會格式化爲JSON字符串,並添加application/json;charset=utf-8的Content-Type
// 等一系列操做
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 合併不一樣配置的headers,config.headers的配置優先級更高
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);
// 刪除headers中的method屬性
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
// 若是config配置了adapter,使用config中配置adapter的替代默認的請求方法
var adapter = config.adapter || defaults.adapter;
// 使用adapter方法發起請求(adapter根據瀏覽器環境或者Node環境會有不一樣)
return adapter(config).then(
// 請求正確返回的回調
function onAdapterResolution(response) {
// 判斷是否以及取消了請求,若是取消了請求拋出以取消
throwIfCancellationRequested(config);
// 使用/lib/defaults.js中的transformResponse方法,對服務器返回的數據進行格式化
// 例如,使用JSON.parse對響應正文進行解析
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
},
// 請求失敗的回調
function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
}
);
};
複製代碼
InterceptorManager.js文件中定義了axios攔截器類。包含了攔截器的添加,刪除,循環攔截器。不管是響應攔截器仍是請求攔截器,都是使用數組進行存儲的。promise
var utils = require('./../utils');
// 攔截器類
function InterceptorManager() {
// handlers數組用來存儲攔截器
this.handlers = [];
}
// 添加攔截器,use方法接收兩個參數,成功的回調以及失敗的回調
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
// 成功的回調
fulfilled: fulfilled,
// 失敗的回調
rejected: rejected
});
return this.handlers.length - 1;
};
// 根據id(索引),刪除實例handlers屬性中攔截器
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);
}
});
};
module.exports = InterceptorManager;
複製代碼
Axios.js文件中定義了Axios實例上的request,get,post,delete方法。get,post,delete等方法均是基於Axios.prototype.request的封裝📦。在Axios.prototype.request中會依次執行請求攔截器,dispatchRequest(實際發起),響應攔截器。總體的流程如👆上圖所示。瀏覽器
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
function Axios(instanceConfig) {
// Axios的配置
this.defaults = instanceConfig;
// 攔截器
this.interceptors = {
request: new InterceptorManager(), // 請求攔截器
response: new InterceptorManager() // 響應攔截器
};
}
Axios.prototype.request = function request(config) {
// 若是config是一個字符串,把字符串看成請求的url地址
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 合併配置
config = mergeConfig(this.defaults, config);
// 若是沒有指定請求方法,使用get方法
config.method = config.method ? config.method.toLowerCase() : 'get';
var promise = Promise.resolve(config);
// 將請求攔截器,和響應攔截器,以及實際的請求(dispatchRequest)的方法組合成數組,相似以下的結構
// [請求攔截器1success, 請求攔截器1error, 請求攔截器2success, 請求攔截器2error, dispatchRequest, undefined, 響應攔截器1success, 響應攔截器1error]
var chain = [dispatchRequest, 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);
});
// 開始執行整個請求流程(請求攔截器->dispatchRequest->響應攔截器)
// 流程能夠理解爲上圖⬆️
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// 基於Axios.prototype.request封裝其餘方法
// 將delete,get,head,options,post,put,patch添加到Axios.prototype的原型鏈上
// Axios.prototype.delete =
// Axios.prototype.get =
// Axios.prototype.head =
// Axios.prototype.options =
// ...
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
}));
};
});
module.exports = Axios;
複製代碼
defaults.js文件中配置了,axios默認的請求頭、不一樣的環境下axios默認使用的請求方法、格式化請求正文的方法,格式化響應正文方法等內容
var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
// 默認Content-Type
var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
};
// 設置ContentType,在沒有設置的狀況下
function setContentTypeIfUnset(headers, value) {
if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
headers['Content-Type'] = value;
}
}
// 根據當前環境,獲取默認的請求方法
function getDefaultAdapter() {
var adapter;
// 判斷當前環境是否存在process對象
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// node環境
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 瀏覽器環境
adapter = require('./adapters/xhr');
}
return adapter;
}
var defaults = {
// 默認的請求方法
adapter: getDefaultAdapter(),
// 格式化請求requestData,這會請求發送前使用
transformRequest: [
function transformRequest(data, headers) {
// 格式化header屬性名,將header中不標準的屬性名,格式化爲Accept屬性名
normalizeHeaderName(headers, 'Accept');
// 格式化header屬性名,將header中不標準的屬性名,格式化爲Content-Type屬性名
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;
}
// URLSearchParams提供了一些用來處理URL查詢字符串接口
// 若是是URLSearchParams對象
if (utils.isURLSearchParams(data)) {
// Content-Type設置爲application/x-www-form-urlencoded
// application/x-www-form-urlencoded,數據被編碼成以&分隔的鍵值對
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
// 若是是對象
if (utils.isObject(data)) {
// Content-Type設置爲application/json
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
// 將請求正文格式化爲JSON字符串,並返回
return JSON.stringify(data);
}
return data;
}
],
// 格式化響應resposeData,這會響應接受後使用
transformResponse: [
function transformResponse(data) {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}
],
// 默認超時時間
timeout: 0,
// xsrf設置的cookie的key
xsrfCookieName: 'XSRF-TOKEN',
// xsrf設置header的key
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
// 驗證請求的狀態
// 在處理請求的Promise會被使用
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
defaults.headers = {
// 通用的HTTP字段
// Accept告知客戶端能夠處理的類型
common: {
'Accept': 'application/json, text/plain, */*'
}
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});
// 爲post,put,patch請求設置默認的Content-Type
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;
複製代碼
axios.js文件是axios工具庫的入口方法,在axios.js
var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');
// 建立axios實例
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
// 更改Axios.prototype.request的this,執行context實例
// instance等於Axios.prototype.request方法
var instance = bind(Axios.prototype.request, context);
// 將Axios.prototype,context上的屬性合併到instance
// instance.get = Axios.prototype.get
// instance.defaults = context.defaults
// ...
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}
// axios會直接對使用者暴露一個axios.request的方法,因此咱們在使用axios的時候能夠這樣使用。不須要new一個axios的實例
// import axios from 'axios'
// axios.get('/info')
var axios = createInstance(defaults);
axios.Axios = Axios;
// axios.create能夠根據用戶自定義的config生成一個新的axios實例
axios.create = function create(instanceConfig) {
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);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
module.exports.default = axios;
複製代碼