关于axios请求原理分析
什么是axios
axios是基于Promise封装的一个前端请求库,可以用在node.js和浏览器中。
axios的特点:
- 请求返回Promise,可以很方便的进行链式调用
- 可以附加拦截器:请求拦截器、响应拦截器
- 可以随时取消未完成的请求
- 客户端支持防御 XSRF(跨站请求伪造)
- 会帮你转换请求数据和响应数据
- 在node.js中发送请求使用http,在浏览器使用XMLHttpRequest
安装
$ npm install axios
简单使用:
get请求
axios.get('/user', {
params: {
ID: 12345
}
})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
post请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
因为是基于 promise 封装的,所以 axios 也支持 promise.all 的写法,同时 axios.spread 会 依次帮你展开请求相应结果
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));
深入学习axios
在学完axios的用法,以及上手使用了axios之后,我心里就有些疑问:
- axios是怎么附加的拦截器呢?
- axios 的取消请求具体又是怎么实现的?
- axios和我们自己调用create方法产生的实例有什么区别呢?
首先要搞清axios发送请求的整个流程。我们稍微分析一下就能知道,请求拦截器肯定是在发送请求之前被调用的,而响应拦截器肯定是在请求结束,拿到返回结果的时候,再联想到官方文档介绍 axios 是基于promise 的, 那这整个流程肯定就是基于promise的链式调用了嘛。
去 github 拿到 axios 的源码 ,然后我们来捋一捋:
拿到源码的之后,我发现了两个axios文件,一个首字母大写,一个小写!我们日常写代码,不都是用的小写axios,或者创建的instance吗?
然后我去看了下入口文件
入口文件不是小写的 axios.js 嘛?所以这个大写的Axios是个啥?
带着这个疑问,我们先去看下axios.js,默认导出了axios,axios又是createInstance方法的返回值,而且后面一句axios.create,这不就是axios创建实例的方法嘛?同样是调用了createInstance方法,区别是传入的config配置不同。
再往后面看,发现在var 了 axios后,又往axios上加了取消请求的相关方法,和 all 等等一些方法。而我们自己再代码中调用create方法返回的实例并没有这些!这就解决了我的第三个疑问!
axios和我们自己调用create方法产生的实例有什么区别
看完以上代码我们可以总结下,先说相同点
- 都能发任意请求
- 都有默认配置和拦截器属性
不同点
- 默认匹配的值可能不同,由于使用create方法创建的instance也支持传入配置项,这里的配置项在合并时会覆盖掉默认的axios.default
- instance原型对象上没有取消请求相关,以及axios.all,axios.spead等方法
然后我们去看下createInstance 方法里,再给个特写
这里定义的context 是 new 的 大写Axios,后面用于return 的 instance调用了bind,里面传入的是Axios原型对象上的request,和大写Axios的实例,再到后面两句代码都跟Axios有关。也就是说axios从功能上来说,完全就是Axios的实例啊,然后去 Axios.js文件里看下:
拦截器是怎么附加的
第一段代码 定义了defaults 默认配置,以及拦截器。然后我好奇的点开 InterceptorManager 看下
InterceptorManager 里定义了 一个 handlers 数组,再往下看 一个 use 函数被添加到它的原型对象上。从 use 函数 的 fulfilled 和 rejected 两个参数的命名也可以猜出来, 这两个参数就是我们添加拦截器时传入的promise,看下下面添加拦截器的代码就懂了。
use 函数的作用也很明显 把这两个函数添加到 handlers 数组中,所以 axios 的拦截器是可以添加多个的。
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
最后 use 函数返回了 handlers 数组的长度减1,刚开始我是没看懂的,后来看到关于返回值的注释 An ID used to remove interceptor later 翻译过来就是 用于以后删除拦截器的ID ,也就是被加入到handlers 数组中对应的索引呐。
联想到 axios 是可以删除 拦截器的,下面 eject 函数的作用也就明显了。
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
至于 InterceptorManager 原型对象上的 foreach 函数,看起来像是循环 handles 数组,执行传入的 fn 函数 。这个暂且放下。
回到Axios .js文件中,下面代码往Axios原型对象上添加了一个 request 方法
这个request方法暂且也放下,再往下看
这里调用了工具类中的foreach方法, 为Axios的原型对象上添加对应的请求方法,如get,post等,我们调用的Axios.get,post等方法,就来源于这些代码。
继续往下看,Axios原型对象上的这些方法内,实际调用了this.request ,这里的this.request 就是我们上面跳过的那个 request方法,也就是说我们调用axios发起请求后,所有的操作都走了这个 request 方法,来看下下面这个request方法(英文注释都被我换成了中文)
request方法接收一个 config 参数,功能如其名,明显这就是个配置项。下面的第一段代码,意为 根据config 类型做相应的处理,然后合并传入的 config 与 defaults.config,这段跳过。
第二段意为 设置请求方法,默认为 get请求 ,这段跳过。
第三段重点来了,先定义一个数组,数组中有两个默认值,一个是请求分发的函数,从 dispatchRequest.js 引入,这里先跳过,第二个是 undefined ,为什么是个undefined 呢 ?
先往后看
后面定义一个变量,存储promise成功的方法,这个promise里存的就是上面合并的config,随后循环数组,嗯????…这个foreach有点不对啊,这好像不是Array对象原型上的循环方法啊, f12进去发现,这丫不是 InterceptorManager 原型对象上的方法嘛。
不过也确实是循环拦截器数组,调用传入的 fn函数,返回去看这个fn函数,把请求拦截器按照后来先进的顺序加入到 chain 的头部,响应拦截器按照先来先进的顺序加入到尾部。中间部分不就是之前定义数组时候,就放进去的 请求 分发嘛。
再往下,就是循环chain数组,每次取两个元素,刚好一个是成功的回调,一个是失败的回调,用promise链把整个流程链接起来,形如 :
(resolve0,reject0)=>(resolve1,reject1)=>(dispatchRequest, undefined)=>(resolve3,reject3)
而且贯穿整个promise请求的参数就是请求配置的config。这时候大概也就明白为什么会定义一个undefined了,因为每次都是从chain数组内取两个元素出来,而拦截器传入的都是两个回调,所以dispatchRequest,后面就需要定义一个undefined来占位置。以免位置出现偏差。而promise状态的成功失败与否都由dispatchRequest内处理。
当我们调用axios.get 或者 post 的时候,promise链步骤应该是这样
- 请求拦截器 (成功,或者失败的回调)
- 请求分发(成功的回调, undefined)
- 响应拦截器 (成功,或者失败的回调)
- 最终我们代码自己定义的成功,或者失败的回调
捋到这为止,就解开了心里的第一个疑惑:拦截器时怎么附加的
1、当调用 use 函数时,会往Axios的 interceptors 属性的 request 或者 response 数组里添加两个promise的回调函数,分别对应调用use函数时,传入的两个回调。
2、在Axios的 request 方法里,会定义一个chain数组,默认存放dispatchRequest,也就是请求分发,然后会循环request和response两个数组,把请求拦截器插入到chain数组的头部,响应拦截器插入到chain数组尾部
3、循环chain数组,每次取出两个元素,刚好是promise的成功回调和失败回调,并把这些promise 链在一起。
搞懂了拦截器是怎么附加的,但是axios还有个重点部分,发送请求,也就是之前看到的 定义chain数组时,就默认给到的dispatchRequest ,接下来去研究一下 dispatchRequest.js里究竟做了什么事情
前面一些代码主要是一些请求数据转换,以及请求头的处理,重点在下面
首先是定义adapter请求适配器,然后定义将要传入adapter内的resolve和reject函数,可以明显的看到,两者又对请求结果进行了处理。
那么 config.adapter || defaults.adapter 来自哪呢,我去们看下 default.js,开头所说的 axios的特点之一:
在node.js中发送请求使用http,在浏览器使用XMLHttpRequest便来自下面代码。
我们先去看浏览器环境下,使用的xhr,暂且跳过node环境的http。找到xhr.js
第一行,第二行代码是拿到请求头和请求体。
第三行是针对请求体内容FormData类型时,删除设置的content-type,交由浏览器自行设置。之所以要交给浏览器设置是因为请求体内容为FormData类型时,多为文件上传,而文件上传时,涉及到一个 boundary ,也就是分隔符,如果手动设置了content-type的话,就必须自己传入一个boundary ,否则上传文件就会失败。而交由浏览器自己设置的话,浏览器就会自己生成随机的boundary 。至于boundary 更详细的解释,可以自己去谷歌一下。
第四行new了一个XMLHttpRequest的实例。
第五行是关于authentication身份认证的设置,第六行 。。。。后面的操作暂且跳过
先看下监听请求状态变化的函数,
前面代码是些判断,以及响应头,响应体处理,然后我们看下settle方法,传入的是从dispatchRequest里传入到xhr中的resolve和reject方法,以及响应体,并根据响应状态码,确定请求成功还是失败
第一行代码中的validateStatus方法也在defaults.js中
axios 的取消请求具体又是怎么实现的
上面代码都比较简单,但是我们至今都还没找到取消请求在哪啊,我们回到xhr.js接着往下看,终于找到了cancelToken相关。
代码先判断是否配置了cancelToken,再回顾我们取消请求时,需要写的代码 ,
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
当我们传入了cancelToken,就会执行后面的代码,也就是一个promise的成功回调,在这个回调函数里调用了request.abort()方法中断请求,并且调用从dispatchRequest里传进来的reject方法,以失败结束这段promise请求。至此终于逮到了取消请求在哪写的了。
接下来去cancelToken.js中,这里首先定义了一个resolvePromise用来保存promise的resolve操作
cancelToken接收一个执行器,其实这个执行器接收的cancel函数,也就是我们取消请求时所调用的source.cancel()。
当我们在外界代码中调用source.cancel(reason)时,这个函数被调用,然后resolvePromise被调用,使这个promise成功,进入then回调,也就是xhr.js中的那段代码来中断请求。
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
然后再来看一点点细节,执行器的函数内部首先判断了token.reason,这是为什么呢?再往下看一步,token.reason被赋予了 Cancel的实例,而Cancel内部是这样的
也就是说,如果token.reason存在,那么请求就必定被取消过了,这时就执行跳出。并且注意下 最后一句
Cancel.prototype.CANCEL = true;
往Cancel的原型对象上加这个属性有什么用呢,还记得dispatchRequest.js中,关于请求失败的回调,
有一个判断, isCacel(),也就是Cancel.js文件内下面这句代码,看到这不得不佩服框架作者设计的巧妙
至此,第二个疑问,关于取消请求的原理也搞清楚了。
1、在xhr.js中,xhrAdapter 内,定义了下面一段代码,判断发送请求时,是否在config内传入了cancelToken。如果有传,则定义好cancelToken的then回调。并在回调内进行中断请求,以及结束整个promise链,进入失败回调
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
2、 然后在cancelToken.js内,定义了一个promise,当我们在代码中执行,source.cancel(abort reason)时,调用resolve()方法,使promise成功,进入第一步所说的then回调,从而中断请求。
补充:
至于ssource.cancel(abort reason)方法怎么来的,以为什么调用它就会使promise进入then回调,可以参考下面源码:
这是我们取消请求时的写法:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
这是源码
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);
});
}
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
本文地址:https://blog.csdn.net/qq_41885871/article/details/109205583
上一篇: [数据库] 通用分页存储过程第1/5页
下一篇: Android Wifi小记 (2)