目录:
一、axios与其他请求库的区别
二、axios的实现思路(干货)
三、你不知道的axios
四、思路借鉴
内容:
先贴上axios源码的地址,便于大家down下来阅读:https://github.com/axios/axios.git
一、axios与其他方法请求库的区别
一般而言用的比较多的是jQuery的ajax、fetch和axios这几个用于请求的库。
1、早期没有vue、react的时候我们都是使用的jQuery的ajax库,它的优缺点如下:
1)基于原生xhr,贴近底层,支持jsonp
2)为了使用ajax而引入jQuery库过于庞大
3)回调地狱问题
4)不太适用于现在比较流行的Vue、React等框架
2、fetch并不是基于原生xhr的,是ES6新的一个API
1)基于promise,解决了回调地狱的问题,写起来也更加简洁
2)也是偏底层,需要我们手动去封装一些东西,比如请求的返回,状态码的处理等
3)默认不带cookie,需要自己添加
4)兼容性还不是很好
3、request
1)仅作为服务端发起请求的工具
4、axios
axios的官方文档对它是这么介绍的
- 从浏览器中创建 XMLHttpRequest
- 从 node.js 发出 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防止CSRF/XSRF
可以看到,axios的功能还是非常强大的,具备且不限于之前所有请求库的功能,下面我们就来具体讲讲axios是如何实现这些功能的吧!
二、axios的实现思路
axios作为一个小而精的请求库,写的非常清晰明了,大致实现思路如图所示,对照这张图我们再进行分析。
我们以一个最简单的请求为例,来看它是怎么走过这一生的。
import axios form 'axios'
axios.get('/api/getsomething')
.then((response)=>{
console.log('response', response)
})
.catch((error)=>{
console.log('error', error)
})
首先axios实例是从axios.js文件中导出的,那我们就先来看看这个入口文件吧。
// /lib/axios.js
// 重点方法 是用来生成axios实例的
function createInstance(defaultConfig) {
// 传入default默认参数来创建一个Axios实例
var context = new Axios(defaultConfig);
// 使instance指向request方法,且上下文指向context
// 这个request方法是核心方法,一会会讲到,基本上都是用它来发请求的
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
// 这个是把Axios.prototype上的方法扩展到instance对象上,使得我们可以使用axios.get等方法
utils.extend(instance, Axios.prototype, context);
// 把context对象上的自身属性和方法扩展到instance上
// 这样instance 就有了 defaults、interceptors 属性。
// Copy context to instance
utils.extend(instance, context);
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// ...这里省略了一些代码。是用来给axios增加额外的方法的,比如取消请求、合并请求等,在第三部分会讲到
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
核心在于Axios.prototype.request这个方法,那接下来我们就进入Axios.js这个核心文件来看看Axios到底是个什么东西吧。
// /lib/core/Axios.js
// Axios构造函数,有默认defaults设置以及两个拦截器,请求拦截器和响应拦截器,分别用来对请求和响应做一些处理。
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
// ...重点请求方法,后面单独讲
};
// 为支持的请求方法提供别名,这样我们就可以用axios.get等等方法来调用了
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
}));
};
});
到这里我们就明白了,axios那么多调用方式,其实最终都是调用的Axios.prototype.request这个核心方法,接下来我们就进入这个方法细细品味。
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
// 如果用户传入的第一个参数是string类型的,即url,config就取第二个参数,第一个参数就是url,否则参数就是第一个参数,这里再回顾一下我们会给他传什么,axios.get('url',config).then(),是不是确实第一个参数是url?第二个参数可能就是data、header之类的配置项了
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 首先把默认配置和用户传入的config合并,当然,用户自身的配置优先级更高。然后改写config中的请求方法,如果有,改成小写的,没有就加个默认get
config = mergeConfig(this.defaults, config);
config.method = config.method ? config.method.toLowerCase() : 'get';
接下去就是整个axios我觉得比较复杂的部分了,也是它最精华核心的部分了,这里重点要理解一下chain数组存放的东西,它是用来盛放拦截器方法和dispatchRequest方法的,这两个东西很重要,一个是用来拦截请求和响应的,一个用来发送请求的, 通过promise从chain数组里按序取出回调函数逐一执行,最后将处理后的新的promise在Axios.prototype.request方法里返回出去, 并将response或error传送出去。最终我们才得到了想要的数据。(这里我们暂时先不管拦截器,后面在第三节我们单独来讲)
// Hook up interceptors middleware
// 这里就是存放拦截器与发送请求的chain数组
var chain = [dispatchRequest, undefined];
// 先把promise的状态设为resolved,参数是处理过的config
var promise = Promise.resolve(config);
while (chain.length) {
// 数组的 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
// 每次执行while循环,从chain数组里按序取出两项,并分别作为promise.then方法的第一个和第二个参数
// 这里先假设没有拦截器,我们就是直接调用了dispatchRequest这个方法
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
那么这个dispatchRequest方法做了什么呢?和它的名字一样,它所做的事情就是发请求,具体怎么做的还是看代码。
// /lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Support baseURL config
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}
// Ensure headers exist
config.headers = config.headers || {};
// 转换请求数据
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);
// 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);
});
};
其实很简单,把数据处理一下给adapter适配器进行请求,完了再对请求回来的数据进行一个转换后返回。
最后我们再进适配器看看,它是怎么处理我们的请求的。这里就看针对浏览器端的请求吧
// /lib/adapters/xhr.js
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
});
};
它用的是底层xhr发送的请求。返回的也是个promise,所以前面才可以拿到它的resolve和reject,xhrAdapter内的XHR发送请求成功后会执行这个Promise对象的resolve方法,并将请求的数据传出去, 反之则执行reject方法,并将错误信息作为参数传出去。
到现在整个axios的运行流程应该很清楚了,下一节我们再进行深入
三、你不知道的axios
上面的篇幅简单介绍了一个axios请求是如何一步步被执行并返回的全流程,下面更深层次介绍axios的一些方法,比如请求响应拦截、数据转换器、取消请求等
axios的亮点很多,我觉得写的很优美的一个还是要数拦截器的实现了。
先看看这个图,大概有个印象。
我们先来回顾一下,拦截器在哪里首次出现的呢?是在Axios构造函数中。
// /lib/core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// 拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
那我们进InterceptorManager这个构造函数中去看看。
// /lib/core/InterceptorManager.js
function InterceptorManager() {
this.handlers = [];// 用来存放拦截器方法,数组内每一项都有两个属性,一个成功的一个失败的
}
// 我们使用的就是这个use方法,往handlers数组中加一个对象(成功+失败)
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);
}
});
};
这个构造函数其实就三个方法,use、eject、forEach,都是操作拦截器里的handler这个数组的。
让我们看看拦截器的使用,就是上面说的use方法,可以看到确实是传了两个参数,一个成功的一个失败的
axios.interceptors.request.use(config => {
// 在发送http请求之前做些什么
return config; // 有且必须有一个config对象被返回
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
});
后面在axios.prototype.request这个请求方法中拦截器再次出现并发挥了作用,我们重新来过一遍这个方法(这次是加上了拦截器的chain,看看有什么不一样)
// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
// ...
var chain = [dispatchRequest, undefined];
// 初始化一个参数为config的promise对象,状态为resolved
var promise = Promise.resolve(config);
// 将拦截器放进chain数组中
// 这里注意一下请求拦截器是unshift方式入栈的,响应拦截器是push进去的,一个在头一个在尾
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) {
// 每次从chain数组里按序取出两项,作为promise.then方法的第一个和第二个参数
// 这两个参数就是我们使用InterceptorManager.prototype.use方法添加的成功和失败回调
// 注意一下请求拦截器是unshift进数组的,shift出来,所以请求拦截器是先添加的后执行
// 而相应拦截器是push进数组的,shift出来,所以相应拦截器是先添加先执行
// 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,
// 而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,
// 所以通过promise实现链式调用时,每个请求拦截器的fulfilled函数都会接收到一个config对象
// 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是我们的请求方法)请求到的数据(也就是response对象),
// 而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,
// 所以通过promise实现链式调用时,每个响应拦截器的fulfilled函数都会接收到一个response对象
// 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,
// 所以dispatchRequest抛出的错误才会被响应拦截器接收到。
// 因为axios是通过promise实现的链式调用,所以我们可以在拦截器里进行异步操作,
// 而拦截器的执行顺序还是会按照我们上面说的顺序执行,
// 也就是 dispatchRequest 方法一定会等待所有的请求拦截器执行完后再开始执行,
// 响应拦截器一定会等待 dispatchRequest 执行完后再开始执行。
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
现在看看这个图是不是就很清晰了?
接下来我们再讲一讲数据转换器
使用:
1、全局转换器添加
// 往现有的请求转换器里增加转换方法
axios.defaults.transformRequest.push((data, headers) => {
// ...处理数据
return data;
});
// 重写请求转换器
axios.defaults.transformRequest = [(data, headers) => {
// ...处理数据
return data;
}];
2、 修改某次axios请求的转换器
axios.get(url, {
// ...
transformRequest: [
...axios.defaults.transformRequest,
(data, headers) => {
// ...处理数据
return data;
}
]
})
数据转换器在default文件中有定义,这个default之前我们讲过,是和用户的config合并在一起传给axios的参数,也就是axios默认的参数
// /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)) {
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;
}],
};
可以看到转换器的作用其实就是转换数据,根据请求和响应传过来的参数进行转换,返给用户。
下面再讲讲取消请求
其实axios底层使用的是xhr,这就给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('取消请求');
如何实现的
// /cancel/CancelToken.js - 11行
function CancelToken(executor) {
var resolvePromise;
// 设置一个状态为pending的promise
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
// 将promise的状态设为resolve
resolvePromise(token.reason);
});
}
// /lib/adapters/xhr.js - 159行
if (config.cancelToken) {
// 这里将promise设为reject,取消后面的请求
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
看到这应该知道其实两种方法最后调用的都是cancelToken里面的executor函数,将canceltoken的promise设为resolved,传进最后调用的xhr中,将整个promise链设为reject,取消请求。
四、思路借鉴
读完了整个axios的源码之后,对我们平时写代码有什么可以借鉴之处呢?
1、拦截器的思路实现
将请求拦截器和响应拦截器分别放在chain数组的两端,中间是发送请求的方法,一步一步成对执行,将这么多promise进行串联,非常巧妙。
2、Adapter的处理逻辑
适配器是在自身的配置中默认引用,并根据环境自动选择,还可根据用户自行配置,降低耦合,且给用户留了口子,很人性化。
3、请求响应转换器
自动根据数据类型转换数据,不用用户手动转换,非常棒,用户自行也可以设置,人性化。
4、取消HTTP请求的处理逻辑
在取消HTTP请求的逻辑中,axios巧妙的使用了一个Promise来作为触发器,将resolve函数通过callback中参数的形式传递到了外部。这样既能够保证内部逻辑的连贯性,也能够保证在需要进行取消请求时,不需要直接进行相关类的示例数据改动,最大程度上避免了侵入其他的模块。
参考文章: