0%

Axios源码解析(四):核心工具方法(1)

上篇 Axios 源码解析(三):适配器 解析了适配器部分的源码。下面来解析核心工具方法的代码,本篇先看 default.js/cancel 目录。

https://github.com/MageeLin/axios-source-code-analysis 中的 analysis 分支可以看到当前已解析完的文件。

default.js

default.js 文件中,最终导出了 10 个变量和方法,顾名思义,这 10 个导出的内容全都是为默认功能服务的:

  • transitional: 默认过渡 option 提示标志(之后的版本会被废弃)
  • adapter: 默认的适配器
  • transformRequest: 默认的请求转换器
  • transformResponse: 默认的响应转换器
  • timeout: 默认超时时间(0 代表无限制)
  • xsrfCookieName: 默认 XSRFcookie 配置
  • xsrfHeaderName: 默认 XSRFheader 配置
  • maxContentLength: 默认 content 最大长度限制
  • maxBodyLength: 默认 body 最大长度限制
  • validateStatus: 默认状态校验器(200-300 才被看作为正常响应)
  • headers: 默认的 header 配置,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defaults.headers = {
common: {
Accept: 'application/json, text/plain, */*',
},
delete: {},
get: {},
head: {},
post: {
'Content-Type': 'application/x-www-form-urlencoded',
},
put: {
'Content-Type': 'application/x-www-form-urlencoded',
},
patch: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};

default.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
'use strict';

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
var enhanceError = require('./core/enhanceError');

// 默认的content-type类型为form-urlencoded
var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded',
};

// 如果content-type没有设置,就设置content-type为value
function setContentTypeIfUnset(headers, value) {
if (
!utils.isUndefined(headers) &&
utils.isUndefined(headers['Content-Type'])
) {
headers['Content-Type'] = value;
}
}

// 获取默认的adapter
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器使用 XHR adapter
adapter = require('./adapters/xhr');
} else if (
typeof process !== 'undefined' &&
Object.prototype.toString.call(process) === '[object process]'
) {
// nodejs使用 HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}

var defaults = {
// 以下三个options是过渡性的
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false,
},

// 选择默认的适配器
adapter: getDefaultAdapter(),

// 请求时转化
transformRequest: [
function transformRequest(data, headers) {
// 标准化accept和content-type请求头
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');

// FormData、ArrayBuffer、Buffer、Stream、File、Blob类型时,直接返回data
if (
utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
// ArrayBuffer的View类型时,返回buffer
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
// URLSearchParam类型时,返回toString方法的结果
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(
headers,
'application/x-www-form-urlencoded;charset=utf-8'
);
return data.toString();
}
// 为其他对象时或者content-type为json时,返回stringify的结果
if (
utils.isObject(data) ||
(headers && headers['Content-Type'] === 'application/json')
) {
setContentTypeIfUnset(headers, 'application/json');
return JSON.stringify(data);
}
return data;
},
],

// 响应时转化
transformResponse: [
function transformResponse(data) {
var transitional = this.transitional;
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
var strictJSONParsing =
!silentJSONParsing && this.responseType === 'json';

// 解析响应结果
if (
strictJSONParsing ||
(forcedJSONParsing && utils.isString(data) && data.length)
) {
try {
return JSON.parse(data);
} catch (e) {
// 报错时返回错误对象
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw enhanceError(e, this, 'E_JSON_PARSE');
}
throw e;
}
}
}

return data;
},
],

/**
* 中止请求的超时时间(以毫秒为单位)。如果设置为 0(默认),则不会限制超时。
*/
timeout: 0,

// XSRF的配置
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',

// content和body的最大长度限制
maxContentLength: -1,
maxBodyLength: -1,

// 校验状态,只有2开头的请求才是正常响应
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
};

// 默认headers
defaults.headers = {
common: {
Accept: 'application/json, text/plain, */*',
},
};

// 设置六种方法的Content-Type
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

/cancel 目录

/cancel 目录的内容与主动取消 Axios 请求有关,包含三个文件。

1
2
3
4
├─cancel // 取消请求
│ Cancel.js
│ CancelToken.js
│ isCancel.js

Axioscancel token API 是基于 TC39cancelable promises proposal 提议封装的。使用时需要在配置项中手动添加 cancelToken 属性,通过执行回调函数的参数形式来实现“取消”功能。

源码分析

这块的源代码看着非常不直观,先从依赖最少的两个文件入手:

Cancel.js

首先 Cancel.js 中,实现了一个 Cancel 类,这个类有一个实例属性 message,一个原型方法 toString 来组装并返回 message,一个原型属性CANCEL来判别是否为 Cancel 类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Cancel.js
'use strict';

/**
* 当操作被取消时会抛出一个Cancel类的对象
*
* @class
* @param {String} message 取消的消息
*/
function Cancel(message) {
this.message = message;
}

// 给Cancel添加一个toString原型方法
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};

// 是否为Cancel类的标志
Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;

isCancel.js

在 isCancel.js 中,实现就更加简单,返回一个 isCancel 方法,通过CANCEL这个标志来判断是否为 Cancel 实例。

1
2
3
4
5
6
7
8
9
10
'use strict';

/**
* @description: 判断是否为Cancel类的对象
* @param {Any} value 要判断的值
* @return {Boolean}
*/
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};

CancelToken.js

下一步就是最让人一头雾水的 CancelToken.js,看源码之前先看官网给出的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 官方示例1
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
}),
});

// 取消请求
cancel();

通过实例可以发现,这里用的就是一个 CancelToken 类,所以咱们先看最核心的 CancelToken 类:

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
// CancelToken.js
/**
* `CancelToken` 是被用于取消请求操作的类
*
* @class
* @param {Function} executor 执行器
*/
function CancelToken(executor) {
// 执行器的类型判断
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}

// 自定义一个Promise,利用作用域拿到resolve
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});

var token = this;
// 给执行器传参为“cancel函数”,通过闭包给到调用方
// 最终目的是将resolvePromise通过两次闭包拿到外部,给用户来自己取消
executor(function cancel(message) {
if (token.reason) {
// 如果reason属性已经有值,说明已经取消过了
return;
}

// 将取消原因组成的Cancel对象 resolve出去
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}

分解为代码流:

CancelToken

大家可以反复理解下上面这段源码,这个地方最难的一点就是通过两层闭包将“resolve”最终拿给了外部。

剩下的两个原型方法就很容易理解了,一个是 throwIfRequested,用于抛出报错对象:

1
2
3
4
5
6
7
8
9
// CancelToken.js
/**
* 把Cancel对象报错抛出去
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};

另一个是 source,这个其实是个工厂方法,实现的原理和上面给出的官方示例1一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CancelToken.js
/**
* 返回一个对象,对象包含新的"CancelToken"对象和一个函数,该函数在调用时取消"CancelToken"。
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel,
};
};

当然,Axios 的官方也给出了 source 的使用方法,如下:

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
// 官方示例2
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 {
// 处理错误
}
});

axios.post(
'/user/12345',
{
name: 'new name',
},
{
cancelToken: source.token,
}
);

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

总结

本篇先介绍了 default.js 中的 10 个默认属性,又将较难理解的 CancelToken 进行了详细分析。

下一篇 Axios 源码解析(四):核心工具方法(2) 来解析另外一部分 Axios 中耦合度较高的工具方法。

👆 全文结束,棒槌时间到 👇