忙里偷闲,终于干完了某阶段的活儿,正好最近在使用vue写项目,因为团队中的项目最近都被统一起来管理,结构和部署开始一致化,发现项目中封装好的api还挺方便,当然缺陷也是有的,比如错误处理这一块儿一直没有统一化,这是个坑(主要还是因为目前人员流动大,项目又杂乱,没有统一接口字段等等理由),总之这些都是题外话。发现项目中的api是经过了对axios的二次封装,说实话,我还没怎么用过axios,有时候遇到一些简单的问题也不是很清楚怎么去处理,正好用这空闲的晚上来阅读一下axios的源码,学习学习。
源码目录结构
首先奉上github源码地址:axios
可以选择clone一下这个项目到本地,先来看一下目录结构,入口文件index.js就做了一件事情,从lib文件夹中导出axios
module.exports = require('./lib/axios');
下面来看一下这个核心文件夹lib中的结构:
lib
├── adapters // 适配器
│ ├── http.js // node环境中使用http来请求接口
│ └── xhr.js // 浏览器环境中使用XMLHttpRequest来请求接口
├── cancel // 用于取消请求
│ ├── Cancel.js // 定义了取消请求返回的信息结构
│ ├── CancelToken.js // 定义用于取消请求的主要方法
│ └── isCancel.js
├── core // 核心文件夹
│ ├── Axios.js // 定义axios构造函数以及原型上的属性和方法
│ ├── buildFullPath.js // 根据相对路径和绝对路径使用buildFullPath构建完整url
│ ├── createError.js // 生成指定error
│ ├── dispatchRequest.js // 连接拦截器的中间件,发起请求的地方
│ ├── enhanceError.js
│ ├── InterceptorManager.js // 定义拦截器
│ ├── mergeConfig.js // 合并配置项
│ ├── settle.js // 根据请求状态,处理Promise
│ └── transformData.js // 格式化data
├── helpers
│ ├── bind.js
│ ├── buildURL.js // 将get请求中的参数格式化为&形式
│ ├── combineURLs.js
│ ├── cookies.js
│ ├── deprecatedMethod.js
│ ├── isAbsoluteURL.js
│ ├── isURLSameOrigin.js // 是否同源
│ ├── normalizeHeaderName.js // 对对象属性名的进行格式化
│ ├── parseHeaders.js // header信息转化为对象
│ └── spread.js // 扩展参数,类似于apply
├── axios.js
├── defaults.js // axios中默认的配置
└── utils.js // 一些工具方法
代码一窥
axios核心代码
其实最核心的axios代码部分不到五十行,先看一下这段源码是如何实现axios的功能设计的:
'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');
/**
* 定义createInstance方法,用来生成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实例,参数为default.js文件的默认配置
var axios = createInstance(defaults);
axios.Axios = Axios;
// 支持用户自定义配置
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 导出Cancel 和 CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// 导出all和spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
// 允许在TypeScript中使用axios
module.exports.default = axios;
可以看到这个文件已经是集合了很多功能最后封装导出的一部分,主要是通过Axios构造函数生成一个实例,在这个实例上添加Axios、create、Cancel、CancelToken、isCancel、all和spread等属性或方法,在实例上绑定Axios和create是为了让用户可自定义配置。
详细看一下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;
}
createInstance函数一共做了这几件事儿:
- 创建一个新Axios实例
- 更改Axios.prototype.request的this,执行context实例
- 绑定原型
- 绑定作用域
- 返回这个新实例instance
这个步骤是不是似曾相识?反正我是怎么瞅着怎么觉得像是new的过程:
在《JS高级程序设计(第三版)》书里有描述new的具体过程:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象;
我在这篇博客里记录了 关于new操作符,有具体的模拟实现new的方法:
function newObj() {
var o = {}; // 创建一个新对象
var Con = [].shift.call(arguments); // 取出第一个参数,即传入的构造函数,此时原arguments已经去掉了构造函数
Con.apply(o, arguments); // 将剩下的参数绑定到这个构造函数上
o.__proto__ = Con.prototype; // 绑定原型
// o.constructor = Con;
return o;
}
好,又扯远了。
回归正题,下面可以看到axios的构造函数是从core文件夹中引入的,打开Axios.js这个文件。
Axios构造函数
这个函数代码也不长,主要是定义了Axios构造函数,给自身加了默认配置defaults和拦截器interceptors的属性,在原型上绑定request、getUri方法,并为axios配置方法别名,也就是我们常用的delete、get、head、options、post、put和patch的请求方式。
比如get请求可以直接这么用:
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
这里主要定义了这些基本方法,至于all和spread在其他文件中定义,后面会分析到。
Axios.js源码:
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// Hook up interceptors middleware
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;
};
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// Provide aliases for supported request methods
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;
其中,下面这串代码是跟拦截器相关的,它把实际的请求初始化放在chain数组中,然后分别遍历interceptors的request请求,分别把成功回调和失败回调插入到数组头部;而interceptors的response响应则放在数组尾部,这个数组最后大概长成这样:
[请求拦截器success, 请求拦截器error, dispatchRequest, undefined, 响应拦截器success, 响应拦截器error]
接着while语句开始执行整个请求流程,顺序是 请求拦截器 -> dispatchRequest -> 响应拦截器
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);
});
// 最终将 chain 里的方法包成 promise,并返回
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
拦截器类
先来科普一下拦截器:
拦截器分为请求拦截器和响应拦截器,顾名思义: 请求拦截器(interceptors.request)是指可以拦截住每次或指定http请求,并可修改配置项 响应拦截器(interceptors.response)可以在每次http请求后拦截住每次或指定http请求,并可修改返回结果项。
接着看一下拦截器是怎么实现的?Axios.js文件中引入了拦截器,var InterceptorManager = require('./InterceptorManager');
定义拦截器的核心其实是一个数组,
function InterceptorManager() {
this.handlers = [];
}
// 增加一个请求拦截器,注意是2个函数,一个处理成功,一个处理失败
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);
}
});
};
module.exports = InterceptorManager;
我们可以把每一次请求想象成一条管道里的流过的水。当一个请求发出的时候,会先流过 interceptors 的 request 部分,接着请求会发出,当接受到响应时,会先流过 interceptors 的 response 部分,最后返回,这条管道大概如下:
interceptors.request -> request -> interceptors.response -> response
现在,我们再回看Axios中拦截器相关的代码,
// 最终将 chain 里的方法包成 promise,并返回
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
这时候返回的promise大概会是这样:
Promise.resolve(config)
.then(interceptor.request.fulfilled, interceptor.request.rejected)
.then(dispatchRequest, undefined)
.then(interceptor.response.fulfilled, interceptor.response.rejected)
还记得前面说过的chain数组吗?其中的dispatchRequest是真正发起请求的地方,它主要做了三件事:
- 拿到config对象,对config进行传给http请求适配器前的最后处理
- http请求适配器根据config配置,发起请求
- 请求完成后,如果成功则根据header、data、和数据转换后的response并返回
dispatchRequest主要代码:
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
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];
}
);
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
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);
});
};
如此一来,整个axios的请求流程就明朗了:
interceptor请求拦截器 -> dispatchRequest发请求 -> 格式化请求的数据 -> 请求适配器 -> 格式化响应的数据 -> interceptor响应拦截器
到现在已经介绍了拦截器和dispatchRequest,我们发现中间还有几步没介绍,一部分是数据的格式化transformData,和请求的适配。
数据的格式化
在上面讲过的dispatchRequest代码中,可以看到,请求前使用了transformData来转换config.data,请求后的成功和失败的回调中也都使用了transformData来转换响应数据。有使用过axios经验的应该都知道,默认情况下axios将会自动的将传入的data对象序列化为JSON字符串,将响应数据中的JSON字符串转换为JavaScript对象。这就是transformData的功劳。
transformData定义在core文件夹中,源码如下:
module.exports = function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};
很短,它只是将传入的回调函数遍历处理了而已,真正核心的代码其实是transformRequest和transformResponse,看一下dispatchRequest中是怎么使用transformData的?
// 响应前
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 响应后
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
找到transformRequest和transformResponse的定义:
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) {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
可以看到request主要处理了data为buffer、url的搜索参数、对象时的情况,response则将返回的字符串类型的响应转换为对象。
请求适配器
还是在dispatchRequest中使用到的,axios会根据当前的环境判断使用不同的adapter,如果是浏览器环境,使用XMLHttpRequest来请求,如果是node环境,使用http模块来请求数据。当然,axios也留给了用户通过config自行配置适配器的接口的。这个请求适配器实则是一个函数,如下:
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;
}
取消请求
以上主要介绍了axios的核心代码也是基础部分,axios还居于一些其他的特性,比如取消请求、处理并发请求等。
先看看取消请求是怎么做的?
其实在实际项目中,我还没用到过取消请求,也是在看了axios的介绍才知道还有这个操作,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('取消日志');
源码分析: cancelToken方法的定义在cancel文件夹中,
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);
});
}
这段代码的核心部分是
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
这里得到一个实例属性promise,在请求的适配器中继续给这个promise添加then方法
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
然后我们就可以通过传给CancelToken的参数executor(一个函数)来执行cancel方法,promise属性的状态变为rejected, 并执行request.abort()方法,并且返回 reason 信息。 执行的整个流程如下:
执行 cancel 方法 -> 生成 reason 信息 -> promise resolve -> request abort
客户端防止xsrf攻击
axios的另一个特性是可以防止xsrf攻击,继续科普一下:
XSRF跨站请求伪造(Cross-site request forgery),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
如何防范xsrf攻击?
当用户进行登录请求的时候,这时候后端应该把包含 xsrf 字段的 cookie 保存在 session 中并且返还给前端,前端需要获取到 cookie 中的值并且能放入 ajax 请求体或请求头中,后端把这个值与 session 中的相应值进行判断就可以了,根据跨域不可访问不同域的 cookie ,攻击者也很难猜测出 xsrf 的值,那么这样就防范了 xsrf 攻击
axios中是怎么做的? axios会在浏览器环境中判断,如果设置了跨域请求时需要凭证且请求的域名和页面的域名相同时,读取cookie中xsrf token 的值,并设置到承载 xsrf token 的值的 HTTP 头中。
if (utils.isStandardBrowserEnv()) {
var cookies = require('./../helpers/cookies');
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
支持并发
axios中有两个支持处理并发请求的助手函数axios.all和axios.spread。
其实这两个函数的定义也是非常的简单:
axios.all = function all(promises) {
return Promise.all(promises);
};
function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
axios.all实际上就是调用了 Promise.all的方法,而axios.spread其实类似于apply的作用,all的使用方式是axios.all([axios请求1, axios请求2, ....]),它仍然返回then,但是then方法中要传入 axios.spread(callback)。
示例:
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
//两个请求现已完成
}));
什么情况下会使用并发?
由于项目中本来一个接口获取到的数据,被拆分到了多个接口,但是还要能全部返回数据之后才能做页面显示,虽然发很多http请求会慢,但是因为是异步的,而且符合业务需求,所以可以使用promise All。(比如说上传表单中多张图片,每次只能上传一张时,需要等所有图片上传完成才可以发起提交的请求这时候可以使用axios.all操作)
最后
到此 axios 的大部分实现已经简单分析完成。由于本人才疏学浅,也是刚开始写源码分析类的文章,如有表述的不合理或者是错误的地方,欢迎小伙伴指正。