lbp的blog

纸上得来终觉浅,绝知此事要躬行

0%

axios源码解读

axios 是前端使用的主流网络请求库。本文从源码角度进行解读,主要分析浏览器环境下具体实现。

node环境下原理相同,区别仅是 axiosXMLHttpRequestshttp 的封装不同。

主要是对axios功能点从源码角度的解读,基于v0.21.0版本

features 解读

  1. Make XMLHttpRequests from the browser
  2. Make http requests from node.js
  3. Supports the Promise API
  4. Intercept request and response
  5. Transform request and response data
  6. Cancel requests
  7. Automatic transforms for JSON data
  8. Client side support for protecting against XSRF

请求的适配

1和2表明了 axios 能够在两种环境环境下使用 browsernode

在这两种环境下实际使用 XMLHttpRequestshttp requests from node.js 发出请求。

在不同环境下, axios 使用了 适配器模式 ,来进行兼容。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// axios 适配器模式支持浏览器和node环境
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 在浏览器环境下使用 XMLHttpRequest 发起请求
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// 在node环境系使用 http 库发起请求
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}

Supports the Promise API

axios 是基于 promise 来进行封住的请求,可以简化成如下形式:

注释部分代表在axios中实现的核心逻辑

Intercept request and response

借助于 Promise 的链式调用,在请求发起前进行 request 处理, 在请求结束,进行 response 处理。相关代码如下

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
Axios.prototype.request = function request(config) {
// some code ...

// Hook up interceptors middleware
// dispatchRequest 负责发起请求
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 把 request 拦截,unshift 在 chain 数组前面
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 把 response 拦截,push 在 chain 数组后面
chain.push(interceptor.fulfilled, interceptor.rejected);
});

/**
* 经过以上处理之后,chain 数组,基本形式应该如下(小序号表示先添加)
* [
* 请求拦截2resolve, 请求拦截2reject
* 请求拦截1resolve, 请求拦截1reject
* ...
* dispatchRequest, undefined
* 响应拦截1resolve, 响应拦截1reject
* 响应拦截2resolve, 响应拦截2reject
* ...
* ]
* */

// 遍历成对取出chain 数组元素,执行
while (chain.length) {
/**
* 请求拦截,使用push进入拦截管理器。forEach 取出后 unshift 到 chain数组,在使用时候 shift 出。
* 所以请求拦截,先添加的后执行
* 响应拦截,使用push进入拦截管理器。forEach 取出后 push 到 chain数组,在使用时候 shift 出。
* 所以响应拦截,先添加的先执行
* */
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

Transform request and response data

axios 会有一个默认配置对象 defaultConfig,配置了默认转换方法。

如果是字符串,使用 try 包裹起来,进行简单的json序列化。

1
2
3
4
5
6
7
8
9
10
// 对 response 默认转换函数
transformResponse: [function transformResponse(data) {
/*eslint no-param-reassign:0*/
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}]

// request 根据不同的数据类型,进行相应的处理

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
// 对request默认转换函数
transformRequest: [function transformRequest(data, headers) {
// 统一 Accept 大小写
normalizeHeaderName(headers, 'Accept');
// 统一 Content-Type 大小写
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}]

requestresponse 的转换方法都是放在数组里面。 axios 支持多个转换。

Cancel requests

在浏览器环境,取消最终是调用的 XMLHttpRequest.abort()。我们从取消一个请求的使用来分析 axios 对取消请求的实现。

1
2
3
4
5
6
7
8
9
10
11
12
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});

// cancel the request
cancel();

调用 cancel() ,会执行 cc是一个函数的参数。源码如下

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
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}

// 每个CancelToken实例对象,都会有一个 `Promise` 实例。默认处于`pending`。
// 并且外界(resolvePromise)持有Promise状态变更的resolve函数
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});

var token = this;
// cancel 函数就是 c。
executor(function cancel(message) {
// 当请求已经取消过,直接返回
if (token.reason) {
// Cancellation has already been requested
return;
}

// 实例化一个 Cancel 对象,抛出
token.reason = new Cancel(message);
// 更改CancelToken实例中promise属性状态
resolvePromise(token.reason);
// 当resolvePromise(token.reason)调用之后,promise的状态会从pending变更为fulfill
// 触发CancelToken实例对象属性promise.then()的执行,取消请求。
});
}

总结来说,取消请求的实现,是借助了 Promise
每一个请求的配置中会有一个处于 pedding 状态的实例化对象。当需要取消请求时,调用 resolve ,触发 promise.then 的执行来取消网络请求

Automatic transforms for JSON data

参考上面Transform request and response data

Client side support for protecting against XSRF

XSRF 又称作 CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

因为同源策略的限制,第三方网站无法获取到被攻击网站的cookie。所以前端可以在请求中携带后端返回的cookie来防止 CSRF。

axios中代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;

if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}

以上内容其实也可以使用拦截器实现。

config

axios 的使用基本都是围绕config,来进行。

Supports the Promise API 中也可以看出,config贯穿整个项目。

axios 文档对config进行了细致的说明,可以参考axios

总结

从axios源码的学习,我们可以总结一个网络请求库大致做了什么事情

  • url 的组装(baseUrl,query参数挂载)
  • 拦截器的设计
  • 响应体数据结构的设计
  • 请求取消的实现
  • 数据转换设计
  • 请求发起的封装