「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。
写在开头
上一周,因为工作上比较忙,又有小伙伴催更 Element源码系列 ,所以先跑去更新了两篇。时隔一周,小编又回来更新 Axios源码系列 啦。
就是不知道还有没有人在关注,哈哈哈,不过,这没有关系,咱还是要坚持下去,把该干的事情干完。
那么,经过上一篇 细读Axios源码系列二 - axios对象创建、request核心方法、发起网络请求 文章的学习,我们自己从头写的 axios
已经是能正常发起请求并且获取到响应结果了。
接下来,我们继续来完善它,但在开始本章内容前,我们先来看看下面这个例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<script>
// 添加拦截器
axios.interceptors.request.use(config => {
console.log('请求拦截1', config);
config.url = 'http://localhost:3000/posts'; // 拦截,添加请求路径
return config;
}, function request11(error) {
return Promise.reject(error);
});
axios.interceptors.request.use(config => {
console.log('请求拦截2', config);
return config;
}, function request11(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('响应拦截', response);
response.my = '橙某人'; // 拦截, 添加自定义响应结果
return response;
}, function request22(error) {
return Promise.reject(error);
});
// 发起请求
axios({
url: '', // 未添加路径
method: 'get'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
复制代码
记得启动自己的 测试服务 哦:json-server --watch server.json
查看浏览器控制台结果,我们能观察到上面这个例子的一些情况:
axios
对象身上会有axios.interceptors.[request/response].use(fn, fn)
方法,并且它接收两个函数作为参数。- 添加拦截器可以有多个,并且在相应时机都会被执行。
- 请求拦截器不是按添加顺序执行,是倒序执行,响应拦截器是按添加顺序进行顺序执行。
拦截器
了解完上面的例子后,下面我们就开始来逐步实现本章的核心拦截器。
.use(fn, fn) 方法实现
我们先来到 Axios.js
文件,进行拦截器的一些初始化操作:
// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');
var InterceptorManager = require('./InterceptorManager'); // 引入拦截器
function Axios() {
// 初始化拦截器容器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
var promise;
var newConfig = config;
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
return promise;
}
module.exports = Axios;
复制代码
新建 lib/core/InterceptorManager.js
文件:
function InterceptorManager() {
this.handlers = []; // 拦截器可以有多个
}
// 注册 use() 方法
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
console.log('use')
}
复制代码
上面代码我们初始化了请求拦截器和响应拦截器,还注册了 .use()
方法并且把它挂载在拦截器身上。
不过,要注意我们的拦截器现在是放在 Axios
对象身上,而 上一篇文章 我们说过使用 axios
对象本质是在执行 Axios.prototype.request()
方法,那当我们添加拦截器时 axios.interceptors.response.use()
,实际也就变成 request.interceptors.response.use()
这样子,但我们 request()
方法身上可没有拦截器,它在 Axios
对象身上,这可咋办?(。•́︿•̀。)
不要着急,我们来看看 axios
源码中是如何解决这个问题的,回到 axios.js
文件:
// lib/axios.js
var Axios = require('./core/Axios');
var bind = require('./helpers/bind');
var utils = require('./utils'); // 引入工具函数
function createInstance() {
var context = new Axios();
var instance = bind(Axios.prototype.request, context);
// 把 Axios 实例对象上原型的方法拷贝到 request() 身上, 如get/post
utils.extend(instance, Axios.prototype, context);
// 把 Axios 实例对象上本身的东西拷贝到 request() 身上, 如拦截器
utils.extend(instance, context);
return instance;
}
var axios = createInstance();
module.exports = axios;
复制代码
上面我们添加了两行代码,看代码就能知道 axios
源码中是把 Axios
对象身上拥有的东西直接拷贝到 request()
方法身上,从而来实现使用 axios
对象也能访问到 Axios
对象身上的过程。
再来新建 lib/utils.js
文件:
var bind = require('./helpers/bind');
/**
* 扩展对象 a 身上的属性, 会将 b 身上的属性拷贝到 a 身上
* @param {Object} a
* @param {Object} b
* @param {Object} thisArg: this 指向
*/
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;
}
// 判断是否是数组
function isArray(val) {
return toString.call(val) === '[object Array]';
}
// 重写 forEach
function forEach(obj, fn) {
if (obj === null || typeof obj === 'undefined') return;
// 不是对象类型, 则变成一个数组, 如函数
if (typeof obj !== 'object') obj = [obj];
if (isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
module.exports = {
extend: extend,
isArray: isArray,
forEach: forEach,
}
复制代码
utils.js 文件是 axios
放所有工具函数的文件,里面具体细节就不一一介绍了,感兴趣的小伙伴可以慢慢一个一个函数揪出来研究一下。(✪ω✪)
我们先测试一下 .use()
方法是否注册成功了,先把项目进行打包 grunt build
,然后编写测试用例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="../dist/axios.js"></script>
</head>
<body>
<script>
axios.interceptors.request.use(config => {
return config;
}, function request11(error) {
return Promise.reject(error);
});
axios({
url: 'http://localhost:3000/posts',
method: 'get'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
复制代码
可以看到 .use()
方法能正常访问、执行,那我们就放心。(-^O^-)
同步与异步拦截器
上面我们算是成功初始化了拦截器,接下来,我们来完善拦截器的执行过程。我们知道添加拦截器的时候会传递两个函数(axios.interceptors.[request/response].use(fn, fn)
),作为成功或失败的反馈回调。
因为拦截器可以添加多个,所以第一步我们需要先来把这些回调收集起来,来 InterceptorManager.js
文件:
// lib/core/InterceptorManager.js
function InterceptorManager() {
this.handlers = [];
}
/**
* 收集拦截器的回调函数
* @param {Function} fulfilled
* @param {Function} rejected
* @param {Object} options: 拦截器配置, {synchronous, runWhen}
* @return {Number}: .use() 方法执行后, 返回当前拦截器总个数, 实际也就是当前这个拦截器的索引
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false, // 默认情况下, axios 的拦截器是异步执行, 当添加请求拦截器时,可以通过 synchronous 来控制是否要同步执行
runWhen: options ? options.runWhen : null, // 可以自定义提供一个函数, 用于验证请求是否能执行该拦截器
});
return this.handlers.length - 1;
};
复制代码
上面代码可能让你感到疑惑的是
options
参数吧?
我们先暂且叫它 "拦截器配置",这个参数应该只有很少的小伙伴们知道吧。因为官方文档好像貌似也没有提及到,平时我们也很少会使用到它,不知道也正常。
它是一个对象,只有两个属性,synchronous
属性用来控制拦截器是否要同步执行,默认是异步执行的,这里需要注意拦截器可以有多个,如果是注册了多个请求拦截器,那么你想同步执行拦截器,必须把每个拦截器的synchronous
属性都设置为true
才行,不能只设置一个,除非你只注册了一个请求拦截器,你如果不理解,没关系,下面会有例子;而runWhen
属性用来控制当前拦截器是否被执行。
收集好注册的所有拦截器后,我们来看看拦截器会在什么时候被执行,继续回到 Axios.js
文件,主要来看 request()
方法:
// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');
var InterceptorManager = require('./InterceptorManager');
function Axios() {
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
// 拦截器是否是同步执行的
var synchronousRequestInterceptors = true;
// 请求拦截器
var requestInterceptorChain = [];
// 主要这里的 forEach() 方法会在 InterceptorManager.js 文件中注册
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 判断是否提供了使用拦截器的判断条件, 如果提供了拦截器的使用条件, 则需要通过才会执行拦截器, 否则该请求不执行拦截器
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) return;
// 可以配置 synchronous 使 axios 变成同步执行, 默认是异步的, true && false => false
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// 往头部依次插入拦截器回调, 这也就是为什么开头例子提到的 请求拦截器 会是倒序执行的原因
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 响应拦截器, 不需要考虑其他配置、执行顺序问题,直接取出来等着被执行就行
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// 异步执行
if(!synchronousRequestInterceptors) {
// 这里的 undefined 主要作用是保持完整性, 成功与失败
var chain = [dispatchRequest, undefined];
// 加入拦截器
Array.prototype.unshift.apply(chain, requestInterceptorChain); // 往头部添加 请求拦截器
chain = chain.concat(responseInterceptorChain); // 往尾部添加 响应拦截器
// 构建一个 fulfilled 状态的 Promise, 为后续的 Promise链 做准备
promise = Promise.resolve(config);
// 依次执行所有的回调函数
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift()); // 以 Promise链 来执行并传递结果
}
return promise;
}
}
复制代码
在 InterceptorManager.js
文件中,实现 forEach
方法:
// lib/core/InterceptorManager.js
var utils = require('./../utils');
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
...
};
/**
* 实现一个 forEach方法, 用于迭代所有的拦截器
* @param {Function} fn: 接收一个函数, 会执行函数, 把拦截器传递出去
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
fn(h);
});
};
module.exports = InterceptorManager;
复制代码
上面代码每行小编都贴心的写上详细的注释,可不能还说看不懂哦。当然,可能比较难的是关于 Promise链
的过程。
下面再写了一个小例子,希望对你有所帮助:
let promise;
promise = new Promise((resolve, reject) => {
resolve('橙某人');
});
promise = promise.then(r1 => {
return r1;
});
promise = promise.then(r2 => {
return r2;
});
promise = promise.then(res => {
console.log(res); // 橙某人
});
复制代码
接下来,我们再对项目进行测试,先执行 grunt build
命令对项目进行打包。
页面测试用例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="../dist/axios.js"></script>
</head>
<body>
<script>
axios.interceptors.request.use(config => {
console.log('请求拦截1', config);
config.url = 'http://localhost:3000/posts';
return config;
}, function request11(error) {
return Promise.reject(error);
});
axios.interceptors.request.use(config => {
console.log('请求拦截2', config);
return config;
}, function request11(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('响应拦截1', response)
// response.my = '橙某人' // 响应拦截, 因为我们的响应结果还没做处理, 它现在是一个字符串而已, 后续我们再对响应结果处理
return response;
}, function request22(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('响应拦截2', response)
return response;
}, function request22(error) {
return Promise.reject(error);
});
axios({
url: '',
method: 'get'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
复制代码
可以看到,控制台结果和我们文章开头的测试结果差不多,虽然我们还有很多东西没有完善,但是整体过程是正确的。
上面我们通过 Promise
对象来实现了异步的情况,下面我们来看看同步的情况应该如何做,我们先来把测试用例调整为同步情况:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="../dist/axios.js"></script>
</head>
<body>
<script>
axios.interceptors.request.use(config => {
console.log('请求拦截1', config);
config.url = 'http://localhost:3000/posts';
return config;
}, function request11(error) {
return Promise.reject(error);
}, {
synchronous: true // 同步
});
axios.interceptors.request.use(config => {
console.log('请求拦截2', config);
return config;
}, function request11(error) {
return Promise.reject(error);
}, {
synchronous: true // 同步
});
axios.interceptors.response.use(response => {
console.log('响应拦截1', response)
// response.my = '橙某人'
return response;
}, function request22(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('响应拦截2', response)
return response;
}, function request22(error) {
return Promise.reject(error);
});
axios({
url: '',
method: 'get'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
复制代码
此时会报错,不过没有关系,因为我们还没写同步的逻辑。
同步的逻辑不难,还是在 request()
方法中编写:
Axios.prototype.request = function request(config) {
var synchronousRequestInterceptors = true;
var requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) return;
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// 异步执行
if(!synchronousRequestInterceptors) {
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain = chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
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;
}
复制代码
写完,再次执行打包命令 grunt build
进行测试,如果没有报错了,并且正确返回结果就说明你大功告成了。
上面我们提到过拦截器的配置,还有一个 runWhen
属性,它是怎么被使用的呢?我写个用例大家应该就清楚了。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="../dist/axios.js"></script>
</head>
<body>
<script>
axios.interceptors.request.use(config => {
console.log('请求拦截1', config);
config.url = 'http://localhost:3000/posts';
return config;
}, function request11(error) {
return Promise.reject(error);
}, {
synchronous: true
});
axios.interceptors.request.use(config => {
console.log('请求拦截2', config);
return config;
}, function request11(error) {
return Promise.reject(error);
}, {
synchronous: true,
runWhen: (config) => { // runWhen
return config.method === 'post';
}
});
axios.interceptors.response.use(response => {
console.log('响应拦截1', response)
// response.my = '橙某人'
return response;
}, function request22(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('响应拦截2', response)
return response;
}, function request22(error) {
return Promise.reject(error);
});
axios({
url: '',
method: 'get'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
复制代码
可以发现 "请求拦截器2" 就不会被执行了,是不是也很简单。
移除拦截器
上面我们能通过 runWhen
属性来控制拦截器是否执行,接下来,我们来学一下手动移除拦截器的功能。
同样,我们先修改一下测试用例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="../dist/axios.js"></script>
</head>
<body>
<script>
let requestIndex;
let responseIndex;
axios.interceptors.request.use(config => {
console.log('请求拦截1', config);
config.url = 'http://localhost:3000/posts'
return config;
}, function request11(error) {
return Promise.reject(error);
});
requestIndex = axios.interceptors.request.use(config => {
console.log('请求拦截2', config);
return config;
}, function request11(error) {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log('响应拦截1', response)
// response.my = '橙某人'
return response;
}, function request22(error) {
return Promise.reject(error);
});
responseIndex = axios.interceptors.response.use(response => {
console.log('响应拦截2', response)
return response;
}, function request22(error) {
return Promise.reject(error);
});
// 移除拦截器
axios.interceptors.request.eject(requestIndex);
axios.interceptors.response.eject(responseIndex);
axios({
url: '',
method: 'get'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
复制代码
知道了移除拦截器的过程,我们来具体看看应该如何实现,先来 InterceptorManager.js
文件注册相关方法:
// lib/core/InterceptorManager.js
var utils = require('./../utils');
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
...
};
// 注册移除拦截器方法
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);
}
});
};
module.exports = InterceptorManager;
复制代码
最后,我们重新打包项目:grunt build
可以发现,"请求拦截器2" 和 "响应拦截器2" 都被移除不会执行了。(^ω^)
那么到此 axios
源码中与拦截器相关的内容就都讲完啦,希望你看完本篇文章能有所收获,我们下一章再见囖。
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。