前言
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js中,大部分人对axios的认识还在API层面,作为一个很优秀的开源项目,其实它的源码也并不复杂,本文将带大家一起阅读 axios 的源码, 解析当中的一些封装技巧、具体的功能实现、以及阅读源码的一些思路。
目录结构
先把源码axios clone一份下来到本地,目录如下:
- dist/ 编译输出文件夹
- example/ 官方的示例
- lib/ 源码目录
- /adapters/ 定义发送请求的适配器
- /cancel/ 定义取消功能
- /core/ 核心Axios
- helper/ 辅助方法
- axios.js 入口文件
- defaults.js axios的默认配置
- util 工具函数
- test 单元测试
- sandbox 沙箱模式
工具函数
源码中有几个出现频率较高的工具函数,先眼熟一下
bind
/** 绑定函数执行上下文 即this指向 */
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
forEach
/** 遍历数组 | 对象 */
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === "undefined") {
return;
}
// Force an array if not already something iterable
if (typeof obj !== "object") {
obj = [obj];
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
extend
/**
* 通过向对象a添加对象b的属性来扩展对象a。
* 如果存在同名属性,后面的对象会覆盖前者的属性
* thisArg可选参数,当属性是一个函数时绑定为该函数执行的上下文
* @return {Object} The resulting value of object a
*/
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === "function") {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
merge
/**
* 深度合并多个对象为一个对象
* 当多个对象包含同一个键时,后一个对象参数列表将优先,和extend不同的是这个不是覆盖策略,是深层合并,求同存异
*/
function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val);
} else if (isPlainObject(val)) {
result[key] = merge({}, val);
} else if (isArray(val)) {
result[key] = val.slice();
} else {
result[key] = val;
}
}
for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}
核心代码
axios.js
先进入入口文件axios.js
,代码不多,直接贴过来👍:
'use strict';
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);
// 指定函数执行的上下文
var instance = bind(Axios.prototype.request, context);
// 属性扩展
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}
// 使用默认配置创建一个axios实例,最终会被作为对象导出
var axios = createInstance(defaults);
// 对外暴露出构造函数
axios.Axios = Axios;
// 创建新实例的工厂函数,可以传入配置来覆盖默认的defaults配置
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');
// 绑定一个all方法 实质是调用Promise.all
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
// 用来判断是否是axios内部错误
axios.isAxiosError = require('./helpers/isAxiosError');
module.exports = axios;
// 允许使用Ts 中的 default import 语法
module.exports.default = axios;
总结起来这里其实做了三件事情:
- 调用
createInstance
方法创建实例 - 在实例上挂载一些方法与属性
- 对外暴露这个实例
分析一下createInstance
创建实例过程:
- 通过默认配置创建context
- 利用
bind
函数绑定Axios.prototype.request
上下文为 context,创建实例 instance - 扩展
Axios
的原型方法到 instance 实例,this 指向上下文 context - 扩展上下文 context 中的属性到 instance 实例
- 返回 instance 返回值instance是一个绑定了执行上下文的request函数,并且其上还挂载了许多属性和方法(方法的执行上下文已确定)
追本溯源,我们再来看看Axios、Axios.prototype.request的源码
Axios.js
'use strict';
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
var validator = require('../helpers/validator');
var validators = validator.validators;
/**
* Create a new instance of Axios
*/
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
/**
* 派发一个请求
* 请求的核心代码
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
// 允许另一种传参方式 url 在前
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 省略,后面单独讲
// ...
};
// 在原型上挂载一些方法
// 不同请求的传参方式不一样,所以分了两组,第二组有data参数,参考get post就行
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
module.exports = Axios;
总结一下代码做的事情:
- 在 Axios 原型注册了请求方法get post等等
- 定义请求和响应拦截器管理对象
- 定义发起请求的request方法
看到这里,我们就知道了为什么axios能提供多种使用方式:
axios(config)
axios(url, {...})
axios.get(url, {...})
axios.request(config)
答:因为axios上扩展了Axios的原型方法,这些方法本质都是调用request方法,同时request对参数做了一层处理(参数类型判断),支持不同的传参方式。
Axios.protoType.request 方法是请求开始的入口,分别处理了请求的 config,以及链式处理请求、响应拦截器,并返回 Proimse 的格式方便我们处理回调。让我们接着往下分析:
由于这部分涉及的东西比较多,我们先要理解拦截器
这个概念。
拦截器
拦截器起到的就是基于promise的中间件的作用,在请求或响应被 then 或 catch 处理前拦截它们。
回到初始化Axios的时候,interceptors对象上有两个属性request、response,这两个属性都是一个InterceptorManager实例,而这个InterceptorManager构造函数就是用来管理拦截器的。看看其定义:
function InterceptorManager() {
// 存放拦截器的数组
this.handlers = [];
}
// 注册拦截器,返回一个id,清除拦截器的时候有用
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);
}
});
};
代码看上去真的挺简单的,就是操作数组的一些方法而已,那么那么当我们通过axios.interceptors.request.use
添加拦截器后,又是怎么让这些拦截器能够在请求前、请求后拿到我们想要的数据的呢?
看看request是如何做到的,源码附带了详细的解析:
Axios.prototype.request = function request(config) {
// 省略部分代码
...
// 请求拦截的数组
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// 每次都往里面unshift都是成对的:[fulfilled,rejected,fulfilled,rejected,...]
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 响应拦截的数组 同理
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// synchronousRequestInterceptors为true表示是同步的请求拦截器,通常都是异步的
if (!synchronousRequestInterceptors) {
// 创造一个请求序列数组,第一位是发送请求的方法,第二位是空
var chain = [dispatchRequest, undefined];
// 把请求拦截器数组推入请求序列前端
// 得到 [请求fulfilled,请求rejected,dispatchRequest, undefined]
Array.prototype.unshift.apply(chain, requestInterceptorChain);
// 把响应拦截器数组推入请求序列后端
// 得到 [请求fulfilled,请求rejected,dispatchRequest, undefined,响应fulfilled,响应rejected]
chain.concat(responseInterceptorChain);
// 构造一个promise,传入config
promise = Promise.resolve(config);
// 给promise后面链式地加上then
// 等于 promise().then().then()....
while (chain.length) {
// 1. 每次循环,从chain数组里按序取出一对,并分别作为promise.then方法的第一个和第二个参数,
// 2. 对于请求拦截器,从拦截器数组按序读到后是通过unshift方法往chain数组数里添加的,又通过shift方法从chain数组里取出的,所以得出结论:对于请求拦截器,先添加的拦截器会后执行
// 3. 对于响应拦截器,从拦截器数组按序读到后是通过push方法往chain数组里添加的,又通过shift方法从chain数组里取出的,所以得出结论:对于响应拦截器,添加的拦截器先执行
// 4. 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,所以通过promise实现链式调用时,每个请求拦截器的fulfilled函数都会接收到一个config对象
// 5. 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是我们的请求方法)请求到的数据(也就是response对象),而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,所以通过promise实现链式调用时,每个响应拦截器的fulfilled函数都会接收到一个response对象
// 6. 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,所以dispatchRequest抛出的错误才会被响应拦截器接收到。
// 7. 因为axios是通过promise实现的链式调用,所以我们可以在拦截器里进行异步操作,而拦截器的执行顺序还是会按照我们上面说的顺序执行,也就是 dispatchRequest 方法一定会等待所有的请求拦截器执行完后再开始执行,响应拦截器一定会等待 dispatchRequest 执行完后再开始执行。
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
// 同步的情况 很简单
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
};
分析了以上代码,发现其实利用的就是promise的特性,值传递和错误冒泡,回调延迟绑定,最终的返回结果也是一个promise,使用者通过promise.then方法就拿到请求的结果,设计上真的非常巧妙。
到此,我们已经了解了拦截器是如何发挥作用的,那么我们的请求实际又是如何发送给服务器的呢?且看dispatchRequest方法。
派发请求 dispatchRequest
function dispatchRequest(config) {
// 取消请求相关 单独讲
throwIfCancellationRequested(config);
// 省略对请求数据的一系列标准化处理
// ....
var adapter = config.adapter || defaults.adapter;
// 使用适配器发送请求
// 对返回的数据对一层转换
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// 转换响应数据
response.data = transformData.call(
config,
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (reason && reason.response) {
reason.response.data = transformData.call(
config,
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
总结代码做了三件事情:
- 取消请求的处理和判断,这个后面会讲到
- 处理参数和默认参数
- 调用adapter发送请求,分成功失败两种情况对响应结果处理
当用户没有指定adapter的时候,默认使用defaults.adapter,因此我们在defaults.js中可以找到adapter的定义
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器环境下 且支持 xhr
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// node 环境下
adapter = require('./adapters/http');
}
return adapter;
}
axios 的本质分为两部分,一部分是浏览器环境中的封装好的 xhr 对象,一部分是 nodejs 环境中的 http方法,这里只分析浏览器环境下的处理:
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
...
var request = new XMLHttpRequest();
...
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
request.timeout = config.timeout;
function onloadend() {
...
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
// 请求回来的数据都是在data属性里
// axios(...).then(res=>{res.data})
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
// 根据xhr返回的status决定promise的状态
// status >= 200 && status < 300
settle(resolve, reject, response);
// 清空请求
request = null;
}
if ('onloadend' in request) {
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {...};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {...};
// Handle low level network errors
request.onerror = function handleError() {...};
// Handle timeout
request.ontimeout = function handleTimeout() {...};
// TODO: crsf防御 后面单独讲
// ....
// Add headers to the request
...
// Add withCredentials to request if needed
...
// Add responseType to request if needed
...
...
// TODO: 取消请求,单独讲
// ....
// send 发送请求
request.send(requestData);
});
};
得出结论:axios在浏览器端是通过一个promise封装XMLHttpRequest的请求过程实现的,总结一下代码做的事情:
- 使用浏览器xhr对象发送请求
- 请求过程封装成promise
- 监听xhr可能抛出的事件,决定promise的状态
- 取消请求,单独讲
- csrf防范,单独讲
到次我们基本上就分析完了axios请求发起,响应的整个流程,接下来介绍一下axios拥有的一些特性🎉
取消请求 - CancelToken
我们经常会遇到发送了某个 HTTP 请求,在等待接口响应的过程中突然不需要其结果的情形。比如快速地切换tab页,其内容依赖请求的某个接口的返回,由于接口耗时较长,用户在等待期间又切换了其他tab页,这时如果不取消原来的请求,就会显得非常混乱。
axios是如何帮助做到取消原来的请求的呢?我们知道其内部是promise实现的,那么问题变成了:如何取消一个promise?
实际上promise一旦执行是不可取消的,但是我们可以通过 Promise 的特性来实现类似取消 Promise 的功能,Promise 的状态一旦改变(从 pending 变为 fulfilled 或 rejected)就不可再次改变。
我们可以向外暴露一个取消函数,需要取消 Promise 时就调用该函数,函数被调用时会执行 Promise 的 resovle 或 reject 方法,这样接口得到响应时再执行 resolve 或 reject 就会被忽略,通过这样的方式来实现类似取消 Promise 的功能。
let cancel;
let p = new Promise((resolve, reject) => {
cancel = resolve;
setTimeout(() => {
console.log('xxx')
resolve("done");
}, 2000);
});
cancel('promise cancel')
p.then((r) => console.log(r)); // output: promise cancel
事实上settimeout代码仍然会执行,但是promise的状态已由cancel确定了,实际上axios正是利用了这个特性,我们在源码中可以找到答案。
function CancelToken(executor) {
// 省略了部分代码
...
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.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel,
};
};
module.exports = CancelToken;
- CancelToken构造函数在实例化时有一个promise属性,是一个Promise,对外暴露了他的resolve方法
- cancel函数负责改变这个promise的状态
只要能拿到这个cancel函数,我们就可以决定何时改变这个promise,官方为我们取消请求提供了两种方式,其内部原理是一样的:
- CancelToken的参数executor会作为一个函数执行,内部把cancel作为执行的参数,我们获取这个参数在外部就可以使用cancel函数
- 使用工厂方法source创建时,返回的cancel就是取消函数cancel,token就是CancelToken实例。 实际上工厂方法就是把实例化,取消函数赋值这一操作给封装起来了。
此时我们再回过头看xhrAdapter的代码,一切就非常清晰了,config.cancelToken
就是我们实例化的对象,当调用cancel函数时实例上的promise状态改变,then回调触发:
- 使用request.abort()中断了请求
- 调用reject使axios的promise的状态变为rejected,参数cancel就是token.reason
- 清除request对象
以上就是取消一个请求的全过程分析
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
// ...
}
}
CSRF 防范
跨站请求伪造(「CSRF」 或者 「XSRF」), 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。
作为一个请求库,axios如何帮助我们防范csrf攻击呢?
回到源码找答案,default 默认配置里面(后来合并到config 里面)存在两个变量:
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
可以作为前后端约定的字段帮助我们防御crsf攻击,在xhrAdapter里面可找到相关代码如下
// 标准浏览器环境
if (utils.isStandardBrowserEnv()) {
var cookies = require('./../helpers/cookies');
// withCredentials:跨域携带cookie
// isURLSameOrigin: 同源
var xsrfValue =
(config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName
? cookies.read(config.xsrfCookieName)
: undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = Value;
}
}
代码的逻辑很简单,其实就是如果 cookie 中包含 XSRF-TOKEN
这个字段,就把 header 中 X-XSRF-TOKEN
字段的值设为 XSRF-TOKEN
对应的值,这个值是服务端返回给我们的一个token值,一般保存在服务端的session中,并且有一个过期时间,在请求时会对比header的token和session的token,如果不匹配就认为是一次csrf攻击。
由于cookie不跨域,攻击者不能获取到这个token值,这样发起的请求里会虽然浏览器带上cookie,但是并不包含前端和后端约定好的 header 中的 X-XSRF-TOKEN
字段,所以请求会失败,这样便阻止了 csrf 攻击。
数据转换器
transformRequest
和transformResponse
用于转换请求与响应数据,默认的defaults配置项里已经自定义了一个请求转换器和一个响应转换器,自动转换 JSON 数据作为axios的亮点之一,其实就是通过转换器做到的,在默认情况下,axios将会自动的将传入的data对象序列化为JSON字符串,将响应数据中的JSON字符串转换为JavaScript对象。转换器数组里面的函数接受配置config的data和header作为参数,返回处理过的data。
// /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)) {
// 序列化data
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
transformResponse: [function transformResponse(data) {
if (typeof data === 'string') {
try {
// 尝试转为js对象
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
};
在dispatchRequest请求过程前后都用到了转换器
- 请求转换器的使用地方是http请求前,使用请求转换器对请求数据做处理,然后传给http请求适配器使用。
- 响应转换器的使用地方是在http请求完成后,根据http请求适配器的返回值做数据转换处理
// /lib/core/dispatchRequest.js
function dispatchRequest(config) {
// transformData: 遍历转换器数组,分别执行每一个转换器,根据data和headers参数,返回新的data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
return adapter(config).then(/* ... */);
};
总结
至此,我们对axios就有了一个比较完整的认识,个人看完之后感觉收获颇丰,里面的设计思路不乏值得借鉴的地方,例如请求前后的各种处理方法,参数合并,promise的链式调用等等,现在面试好像也经常问到这方面,这时就可以吹一吹自己知道它如何应用在axios实现中
如有疑问或者笔者理解不对的地方欢迎各位批评指正,共同进步。求点赞三连QAQ🔥🔥