Axios源码
Axios的特性
- 从浏览器中构建XMLHttpRequests对象
- 从node中构建http请求
- 支持promise
- 拦截请求和响应,对数据做转换、封装等操作
- 可以取消请求
- 自动转化JSON格式
- 客户端可以支持CSRF(伪造跨域请求)
Axios类
源码
-
Axios的类源码如下:
function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { // 处理请求的拦截器 request: new InterceptorManager(), // 处理响应的拦截器 response: new InterceptorManager() }; }
这个类结构非常简单,里面首先用内部属性defaults存储了实例的config对象,然后interceptors用来保存请求和响应的拦截器,主要用于use方法(后面讲到)
-
然后,官方在这个类的原型上拓展了request方法:
Axios.prototype.request = function request(config) { // ...... }
这个方法内部主要用来处理拦截器任务和请求,然后按照顺序执行。
这个方法返回了一个Promise,以实现异步调用。
-
紧接着,就是实现具体method类型的方法。
// Provide aliases for supported request methods utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { /*eslint func-names:0*/ 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) { /*eslint func-names:0*/ Axios.prototype[method] = function(url, data, config) { return this.request(mergeConfig(config || {}, { method: method, url: url, data: data })); }; });
可以看出,axios将7中方法分成两大类,这两类的config方式不同,需要加以区分。
实现
-
这里我们用ES6的class来实现Axios类,省去原型定义这步骤。
-
先实现最基本的请求功能(浏览器请求):
class Axios { constructor() { } request(config) { return Promise((resolve,reject) => { let {url = "",method = "get",data = {}} = config; // 判断是否处于浏览器环境 let req = null; if(typeof XMLHttpRequests !== undefined) { req = new XMLHttpRequests(); req.open(url,method); req.onload = function() { resolve(req.responseText); } req.onerror = function() { reject(req.responseText); } req.send(data); }else { // node环境的http.request } }) } }
方法首先返回的是一个Promise,然后从config获取到基本的信息url、method、data并赋予默认值。
然后判断现在的运行环境,构造xhr对象,进行请求,并将报文resolve或者reject。
Axios的请求方式
Axios有两种请求方法:
axios(config)
axios.method(config)
axios(config)
源码
-
源码中,首先是构造一个Axios实例,然后将这个实例的request方法导出:
function createInstance(defaultConfig) { var context = new Axios(defaultConfig); var instance = bind(Axios.prototype.request, context); // 将Axios原型上的方法混入给这个request utils.extend(instance, Axios.prototype, context); // 将context的属性混入到这个request utils.extend(instance, context); // 工厂将默认的config和用户的config进行组合 instance.create = function create(instanceConfig) { return createInstance(mergeConfig(defaultConfig, instanceConfig)); }; return instance; }
实现
-
由于我们目前还没有实现具体方法,因此只需要构造实例,然后将实例的方法导出即可:
function createInstance() { let axios = new Axios(); // 记得绑定作用域 let request = axios.request.bind(axios); return request; } let axios = createInstance();
-
到目前为止,我们已经实现了
axios(config)
方式来请求,下面测试一下.
测试
-
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <script src="./myAxios.js"></script> </head> <body> <button id="btn">点我发送get</button> <p id="content"></p> </body> <script> document.querySelector("#btn").addEventListener("click", function () { axios({ url : "http://localhost:3000/get/server", method : 'get' }).then(res => { console.log(res); document.querySelector('#content').innerHTML = res; }) }) </script> </html>
-
axios.method(config)
源码
-
在前面,我们提过将方法定义在Axios类上:
// Provide aliases for supported request methods utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { /*eslint func-names:0*/ 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) { /*eslint func-names:0*/ Axios.prototype[method] = function(url, data, config) { return this.request(mergeConfig(config || {}, { method: method, url: url, data: data })); }; });
实现
-
自己实现的话,只需要对两种方法进行区分即可:
const methods = ["get", "delete", "head", "options", "put", "patch", "post"]; methods.forEach(method => { if(["get", "delete", "head", "options"].includes(method)) { // 这里得用es5定义,否则this指向将错误 Axios.prototype[method] = function () { return this.request({ method, url : arguments[0], ...(arguments[1] || {}) }) } }else { Axios.prototype[method] = function() { return this.request({ method, url: arguments[0], data : arguments[1], ...(arguments[2] || {}) }) } } })
-
但是,我们使用的是axios.method,而不是Axios.method,这这么解决?
-
答案很简单,就是将Axios上的方法混入到request方法的属性中,然后将context设置为Axios实例。
-
这里的混入函数如下:
function extend(a,b,context) { for(let key in b) { if(b.hasOwnProperty(key)) { if(typeof b[key] === 'function') { a[key] = b[key].bind(context); }else { a[key] = b[key]; } } } }
-
然后,我们在构造实例的时候进行混入即可:
function createInstance() { let axios = new Axios(); // 记得绑定作用域 let request = axios.request.bind(axios); // new extend(request,Axios.prototype,axios); return request; } let axios = createInstance();
测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./myAxios.js"></script>
</head>
<body>
<button id="btn">点我发送get</button>
<p id="content"></p>
</body>
<script>
document.querySelector("#btn").addEventListener("click", function () {
axios.get("http://localhost:3000/get/server").then((res) => {
console.log(res);
document.querySelector("#content").innerHTML = res;
});
});
</script>
</html>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nCBNbNb2-1634130229407)(https://raw.githubusercontent.com/huangyivi/mygraph/master/202110131920871.png)]
- 到此,axios的两种请求方式已经基本实现了,其中有些default option的内容还需要自己阅读源码实现。
Axios的拦截器
-
Axios中拦截器的使用方式如下:
// 对请求数据进行拦截 axios.interceptors.request.use( function (config) { console.log("请求被我拦截了!我可以在这里对你的请求进行修改"); return config; }, function (err) { console.log("请求发送了错误,可能需要做一下数据分析,并抛出错误。"); return Promise.reject(err); } ); // 对响应进行拦截 axios.interceptors.response.use( function (response) { console.log("响应被我拦截了,你看着办吧!"); response = "你的数据已经被我盗走了,想要拿回的话,给爷站起来"; return response; }, function (err) { console.log("响应发生了某些错误,被我拦截了,需要对错误进行一些操作"); return Promise.reject(err); } );
- 值得注意的是,reject的回调函数需要返回Promise,这样才能将错误向下传递
源码
-
官方的拦截器源码非常简单:
function InterceptorManager() { this.handlers = []; } /** * Add a new interceptor to the stack * * @param {Function} fulfilled The function to handle `then` for a `Promise` * @param {Function} rejected The function to handle `reject` for a `Promise` * * @return {Number} An ID used to remove interceptor later */ 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; }; /** * Remove an interceptor from the stack * * @param {Number} id The ID that was returned by `use` */ InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } };
-
在拦截器内部定义私有变量handlers,是一个队列用于保存用户自定义的拦截器。
-
然后在原型上实现use方法和eject方法,前者用于保存拦截操作,后者用于取消拦截。
实现
-
首先,我们还是用ES6的类来实现拦截器类:
class InterceptorManager { constructor() { this.handlers = []; } use(fullfilled,rejected) { this.handlers.push({ fullfilled, rejected }) } eject(id) { if(this.handlers[id]) { this.handlers[id] = null; } } }
-
然后在Axios类中初始化处理请求和响应的拦截器:
class Axios { constructor() { // new this.interceptors = { request : new InterceptorManager(), response : new InterceptorManager() } } request(config) { return Promise((resolve,reject) => { let {url = "",method = "get",data = {}} = config; let req = null; if(typeof XMLHttpRequests !== undefined) { req = new XMLHttpRequests(); req.open(url,method); req.onload = function() { resolve(req.responseText); } req.onerror = function() { reject(req.responseText); } req.send(data); }else { // node环境的http.request } }) } }
-
最后,看到官方的用法是直接在request.interceptors使用,因此我们也需要将这个属性混入到request方法中:
function CreateInstance() { let axios = new Axios(); let req = axios.request.bind(axios); extend(req, Axios.prototype, axios); extend(req, axios); return req; }
实现2
-
到目前为止,我们已经定义好了拦截器类,怎么实现拦截效果呢?答案就是在发送请求之前,将request拦截器队列中的任务拿出来执行一遍,然后在请求发送完成后,收到响应时,将response拦截器中的任务执行一次即可。
-
由于axios是会区分环境来选择请求方式的,因此我们将请求封装成一个方法:
sendAjax(config) { return new Promise((resolve, reject) => { const { url = "", method = "get", data = {} } = config; let xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function () { resolve(xhr.responseText); }; xhr.onerror = function () { reject(xhr.responseText); }; xhr.send(data); }); }
-
然后在request中判断环境选择请求方法,这样定义:
request(config) { let queue; if (typeof XMLHttpRequest !== undefined) { // queue初始化 queue = [this.sendAjax(config), undefined]; } // 将request拦截器中的任务插入到队头 this.interceptors.request.handlers.forEach((interceptor) => { queue.unshift(interceptor.fullfilled, interceptor.rejected); }); // response插入到队尾 this.interceptors.response.handlers.forEach((interceptor) => { queue.push(interceptor.fullfilled, interceptor.rejected); }); // 如果length===2,说明没有拦截器,直接执行请求 if (queue.length == 2) { return this.sendAjax(config); } // 将config传给拦截器,便于拦截器对请求做出修改 let promise = Promise.resolve(config); // 每次执行一对任务 while (queue.length) { promise = promise.then(queue.shift(), queue.shift()); } return promise; }
测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./myAxios.js"></script>
</head>
<body>
<button id="btn">点我发送get</button>
<p id="content"></p>
</body>
<script>
// 对请求数据进行拦截
axios.interceptors.request.use(
function (config) {
console.log("请求被我拦截了!我可以在这里对你的请求进行修改");
return config;
},
function (err) {
console.log("请求发送了错误,可能需要做一下数据分析,并抛出错误。");
return Promise.reject(err);
}
);
// 对响应进行拦截
axios.interceptors.response.use(
function (response) {
console.log("响应被我拦截了,你看着办吧!");
response = "你的数据已经被我盗走了,想要拿回的话,给爷站起来";
return response;
},
function (err) {
console.log("响应发生了某些错误,被我拦截了,需要对错误进行一些操作");
return Promise.reject(err);
}
);
document.querySelector("#btn").addEventListener("click", function () {
axios.get("http://localhost:3000/get/server").then((res) => {
console.log(res);
document.querySelector("#content").innerHTML = res;
});
});
</script>
</html>
- 拦截成功!!!