源码分析axios拦截器实现思路

一、前言

为什么要看这个呢,因为前段时间有人提了个问题,axios的 拦截器如果想取消是否还需要return以及return什么值的问题,带着这个问题,看了axios的源码

说是源码分析,其实就是我看代码以及梳理思路的过程,哈哈哈

个人感觉axios的代码规范做的很好,命名规范,注释简洁,建议亲自品尝!

二、准备工作

为了更好地调试代码,我觉得还是需要一个环境的,因此我基于json-serverwebpack-dev-server搭建了一个测试环境

选择webpack的原因很简单,我只会webpack。。

选择json-server的原因是快,其实基于express搭建一个服务也很快,但是写接口还是要费一定时间的。

json-server只需要一个Json文件就可以搭建一个RESTful API,非常优雅,【json-server】

项目结构和搭建过程在附录,需要的自行查看吧,可以自己搭建就直接忽视这一步

三、整体思路

1. 纵观

源码是在node_modules中拿的,为了偷懒npm i一下就可以直接测试,直接进入正题,梳理代码

最外层入口文件index.js只有一行代码

1
module.exports = require('./lib/axios');

追踪到axios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

var axios = createInstance(defaults);
// 挂载属性和方法的代码不展示了
// 最后导出实例
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;

实际也就做了一件事,执行了createInstance方法,创建了一个Axios的实例,在实例上绑定了各种属性,如spreadallCancel等等,最后抛出这个实例。

所以切入点就是这个createInstance方法了,参数来自default.js,根据名字猜测应该就是axios默认配置,出于好奇看一眼

1
2
3
4
5
6
7
8
9
10
11
12
var defaults = {
adapter: getDefaultAdapter(),
transformRequest: [function(){...}],
transformResponse: [function(){...}],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: function validateStatus(status) {}
};

module.exports = defaults;

代码精简后就是这个东西,其实就是axios的默认配置,也支持用户自定义,也就是create方法的参数

继续向下,将这个配置作为参数newAxios的一个实例,看一眼Axios这个文件,感觉像是核心部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(), // handler = []
response: new InterceptorManager() // handler = []
};
}

Axios.prototype.request = function request(config) {};

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
}));
};
});

module.exports = Axios;

构造函数其实没做啥,就把默认配置绑定到defaults属性上,又给interceptors了一个空的默认值,其实这个interceptors就是我们核心要看的拦截器。

如果没注册拦截器,默认就不拦截,相当于ajax套了一层promise的壳,后面分析就明白了

然后在Axios原型上声明了一个request属性,不知道干嘛用的。

继续向下,通过自定义的循环函数分别在Axios原型上绑定了deletegetpost等几个函数,值是刚才说到的不知道干嘛用的request函数,不过根据我多年代码经验,axios.get就是发送get请求,这里的request莫非就是发送请求?

带着疑问继续看axios.jscreateInstance函数的剩余部分

1
var instance = bind(Axios.prototype.request, context);

这句也出现了原型上的request,看来这函数挺重要的,看一下bind函数都做了什么,js中原生的bind是绑定上下文,这里也用了这个名字,猜测效果差不多

1
2
3
4
5
6
7
8
9
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};

果然,只不过这里的区别是在实例外面又套了一层函数,在这个函数里面执行了前面那个request方法,这样就可以通过axios(‘url’)访问了,也解决了我最初看代码的一个疑问

axios既然是一个实例,为什么能通过axios('/api/login');调用?

原来是bind函数本身又返回了一个函数

众望所归我们该去看Axios.prototype.request

2. 入微

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Axios.prototype.request = function request(config) {

// 拦截器队列
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;
};

核心部分在于chain这个拦截器队列,也就是实现请求和响应拦截的核心

chain默认包含两个元素,分别是真正执行请求的函数dispatchRequest和一个undefined,然后遍历请求拦截器和响应拦截器从队列的“首部”和“尾部”入队

这样形成了一种管道的结构,执行时依次执行请求拦截器,真正的请求,最后经过响应的拦截器

也就是这段代码

1
2
3
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

每次循环都从队列出队两个,分别作为Promise的成功和失败的回调函数,然后promise的执行结果作为下一次的参数,很巧妙的设计!

关于chain初始值的思考:

最初以为[dispatchRequestundefined]是为了凑一对,为了一次出队两个元素,后来发现并不是,undefined是作为请求的错误处理函数,如果此处不是undefined,则请求抛出的异常无法在响应拦截器中拦截到,后面分析取消请求会具体分析

继续分析,如果没有配置拦截器,则会默认执行dispatchRequest发送请求

这里面有个关于Promise的知识点:

如果promise1的参数是promise,则promise1的结果取决于promise,也就是说如果promise的结果是fulfilled,那么promise1的结果也是fulfilled

所以才可以形成一种promise链的感觉

3. 芥子

接下来是dispatchRequest,也就是实际发送请求的部分,也就是说如果不配置拦截器,只会执行这部分代码

这个函数主要做的事是:

  • 根据当前环境决定用哪个adapter发送请求

  • 在发送请求之前处理数据

  • 请求发送之后处理响应数据

  • 如果配置了取消请求,则也会在这里处理,这篇文章不分析了,其实就是在请求promise中抛出异常来终止promise链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module.exports = function dispatchRequest(config) {

// 如果配置了concelToken则取消请求
throwIfCancellationRequested(config);

// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);

// 处理并合并请求头
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);
});
};

如果请求正常则返回一个promise,交给下面的响应拦截器

如果请求失败则返回rejection状态的promise,由用户自行通过catch块捕获异常

根据这个理论,如果有些异常不想统一处理,可以通过Promise.reject将错误在响应拦截器中抛出去。

这是因为promise的异常有冒泡的特性,如果没人捕获这个异常,将持续向外冒泡,由最终的接盘侠接手哈哈

所以我们响应拦截器可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 测试请求
const instance = axios.create({
method: 'GET',
url: 'http://localhost:9000/pos1ts',
})

instance.interceptors.response.use(res => {
return res.data;
}, err => {
// 404的情况单独处理
if (err.response.status == 404) {
return Promise.reject('404了,我可不管')
}
})

// 实例通过catch捕获由拦截器抛出的异常
instance().catch(err => {
console.log(err)
})

4. 浩瀚

关于Promise “链”的解释

先上个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function handlerErr(err) {
console.log(err)
}

function requestInterceptor() {
return new Promise((resolve, reject) => {
resolve({
method: 'GET'
})
})
}

function responseInterceptor(res) {
return new Promise((resolve, reject) => {
resolve(res)
})
}

function request(config) {
return new Promise((resolve, reject) => {
resolve({
code: 200,
text: 'yes',
type:config.method
})
})
}

let chain = [requestInterceptor, handlerErr, request, handlerErr, responseInterceptor, handlerErr];

let promise = Promise.resolve({});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

promise.then(res => {
console.log(res)
})

这就是axios拦截器的核心实现

通过调试这段代码你就知道promise链式如何工作了

几种尝试:

  • ```javascript
    chain = [requestInterceptor, handlerErr, request, undefined, responseInterceptor, handlerErr];

    1
    2
    3
    4
    5

    将request的错误处理函数变成undefined,看看在最后一个异常处理函数中能否捕获到异常

    * ```javascript
    chain = [requestInterceptor, handlerErr, request, undefined, responseInterceptor, undefined];

    将最后一个异常处理函数也变成undefined,看看控制台如何输出

  • chain保持第二种情况,在promise.then中捕获异常看看是否能捕获到

这几种问题搞明白,基本axios拦截器原理你就明白了!

四、总结

历时一天多,代码难度不大,顺便复习了promise的一些用法

阅读代码的过程还是很愉悦的,可能是很喜欢作者的代码注释,但是作者的回调函数都没用匿名函数,总觉得怪怪的

五、附录:测试环境搭建

项目结构贴个地址吧,手打项目结构太痛苦了

https://gitee.com/rambler1501719577/learn-axios-source

1. JSON-Server

json-server的环境可以根据官网的配置来,只需要一个依赖加一个json文件

1
npm i json-server -g

然后再json文件的根目录通过

1
json-server --watch 你的json文件名

然后就可以通过localhost:3000访问了

2. webpack环境

webpack的环境需要安装webpack,webpack-cli和webpack-dev-server

1
npm i webpack webpack-cli webpack-dev-server -D

然后在项目根目录新建webpack.config.js,写入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
entry: resolve(__dirname, 'src/main.js'),
devServer: {
port: 9000,
static: {
directory: resolve(__dirname, 'public')
}
},
output: {
filename: 'bundle.[hash:8].js',
path: resolve(__dirname, 'dist')
},
devtool: 'source-map',
resolve: {
alias: {}
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
],
mode: 'development'
}

然后在项目根目录运行

1
npx webpack-dev-server

然后就可以通过localhost:9000访问你的项目了