源码目录结构
# ???? 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 中主流程的源码,对于一些工具函数的实现会略过。还请见谅。如果文章中有错误的地方,还请及时指出。
请求流程概览
下面是 axios 源码中发起一个请求时代码大致的流程
源码逐行分析
/lib/cancel/CancelToken.js
CancelToken.js 中定义了取消 axios 请求的相关行为的代码。但 CancelToken.source 返回的取消请求的 cancel 方法,使用的前提,是需要将 CancelToken.source 返回 token 的,结合到具体的请求的 config 中才能正常使用。
如何在 axios 中使用取消请求的功能?
我在看 axios 源码之前,甚至并不知道 axios 可以发出的请求,所以我们先来了解下如何在 axios 取消一个请求。下面是一个例子????
// 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 中的代码来理解。
// /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。从而达到取消请求的目的
/lib/adapters/xhr.js
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);
});
};
/lib/core/dispatchRequest.js
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);
}
);
};
/lib/core/InterceptorManager.js
InterceptorManager.js 文件中定义了 axios 拦截器类。包含了拦截器的添加,删除,循环拦截器。无论是响应拦截器还是请求拦截器,都是使用数组进行存储的。
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;
/lib/core/Axios.js
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;
/lib/defaults.js
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;
/lib/axios.js
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;