我们都知道,axios 是是一个跨平台请求方案,在浏览器端采用 XMLHttpRequest API 进行封装,而在 Node.js 端则采用 http/https 模块进行封装。axios 内部采用适配器模式将二者合二为一,在隐藏了底层的实现的同时,又对外开放了一套统一的开放接口。
那么本文,我们将来探讨这个话题:axios 的跨端架构是如何实现的?
从 axios 发送请求说起
我们先来看看 axios 是如何发送请求的。
// 发送一个 GET 请求
axios({
method: 'get',
url: 'https://jsonplaceholder.typicode.com/comments'
params: { postId: 1 }
})
// 发送一个 POST 请求
axios({
method: 'post'
url: 'https://jsonplaceholder.typicode.com/posts',
data: {
title: 'foo',
body: 'bar',
userId: 1,
}
})
dispatchRequest() 方法
当使用 axios 请求时,实际上内部是由 Axios[3] 实例的 .request() 方法处理的。
// /v1.6.8/lib/core/Axios.js#L38
async request(configOrUrl, config) {
try {
return await this._request(configOrUrl, config);
} catch (err) {}
}
而 ._request() 方法内部会先将 configOrUrl, config 2 个参数处理成 config 参数。
// /v1.6.8/lib/core/Axios.js#L62
_request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
// ...
}
这里是为了同时兼容下面 2 种调用方法。
// 调用方式一
axios('https://jsonplaceholder.typicode.com/posts/1')
// 调用方式二
axios({
method: 'get',
url: 'https://jsonplaceholder.typicode.com/posts/1'
})
当然,这不是重点。在 ._request() 方法内部请求最终会交由 dispatchRequest() 处理。
// /v1.6.8/lib/core/Axios.js#L169-L173
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
dispatchRequest() 是实际调用请求的地方,而实际调用是采用 XMLHttpRequest API(浏览器)还是http/https 模块(Node.js),则需要进一步查看。
// /v1.6.8/lib/core/dispatchRequest.js#L34
export default function dispatchRequest(config) { /* ... */ }
dispatchRequest() 接收的是上一步合并之后的 config 参数,有了这个参数我们就可以发送请求了。
跨端适配实现
// /v1.6.8/lib/core/dispatchRequest.js#L49
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
这里就是我们所说的 axios 内部所使用的适配器模式了。
axios 支持从外出传入 adapter 参数支持自定义请求能力的实现,不过很少使用。大部分请求下,我们都是使用内置的适配器实现。
defaults.adapter
defaults.adapter 的值如下:
// /v1.6.8/lib/defaults/index.js#L40
adapter: ['xhr', 'http'],
adapters.getAdapter(['xhr', 'http']) 又是在做什么事情呢?
适配器实现
首先,adapters 位于 lib/adapters/adapters.js[4]。
所属的目录结构如下:
图片
可以看到针对浏览器和 Node.js 2 个环境的适配支持:http.js、xhr.js。
adapters 的实现如下。
首先,将内置的 2 个适配文件引入。
// /v1.6.8/lib/adapters/adapters.js#L2-L9
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter
}
knownAdapters 的属性名正好是和 defaults.adapter 的值 ['xhr', 'http'] 是一一对应的。
而 adapters.getAdapter(['xhr', 'http']) 的实现是这样的:
// /v1.6.8/lib/adapters/adapters.js#L27-L75
export default {
getAdapter: (adapters) => {
// 1)
adapters = Array.isArray(adapters) ? adapters : [adapters];
let nameOrAdapter;
let adapter;
// 2)
for (let i = 0; i < adapters.length; i++) {
nameOrAdapter = adapters[i];
adapter = nameOrAdapter;
// 3)
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[String(nameOrAdapter).toLowerCase()];
}
if (adapter) {
break;
}
}
// 4)
if (!adapter) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request `,
'ERR_NOT_SUPPORT'
);
}
return adapter;
}
}
内容比较长,我们会按照代码标准的序号分 4 个部分来讲。
1)这里是为了兼容调用 axios() 时传入 adapter 参数的情况。
// `adapter` allows custom handling of requests which makes testing easier.
// Return a promise and supply a valid response (see lib/adapters/README.md).
adapter: function (config) {
/* ... */
},
因为接下来 adapters 是作为数组处理,所以这种场景下,我们将 adapter 封装成数组 [adapters]。
// /v1.6.8/lib/adapters/adapters.js#L28
adapters = Array.isArray(adapters) ? adapters : [adapters];
2)接下来,就是遍历 adapters 找到要用的那个适配器。
到目前为止,adapters[i](也就是下面的 nameOrAdapter)既可能是字符串('xhr'、'http'),也可能是函数(function (config) {})。
// /v1.6.8/lib/adapters/adapters.js#L37
let nameOrAdapter = adapters[i];
adapter = nameOrAdapter;
3)那么,我们还要检查 nameOrAdapter 的类型。
// /v1.6.8/lib/adapters/adapters.js#L42-L48
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
}
isResolvedHandle() 是一个工具函数,其目的是为了判断是否要从 knownAdapters 获取适配器。
// /v1.6.8/lib/adapters/adapters.js#L24
const isResolvedHandle = (adapter) => typeof adapter === 'function' || adapter === null || adapter === false;
简单理解,只有 adapter 是字符串的情况('xhr' 或 'http'),isResolvedHandle(nameOrAdapter) 才返回 false,才从 knownAdapters 获得适配器。
typeof adapter === 'function' || adapter === null 这个判断条件我们容易理解,这是为了排除自定义 adapter 参数(传入函数或 null)的情况。
而 adapter === false 又是对应什么情况呢?
那是因为我们的代码只可能是在浏览器或 Node.js 环境下运行。这个时候 httpAdapter 和 xhrAdapter 具体返回是有差异的。
// /v1.6.8/lib/adapters/xhr.js#L48
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {/* ...*/}
// /v1.6.8/lib/adapters/http.js#L160
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config) {/* ... */}
也就是说:在浏览器环境 httpAdapter 返回 false,xhrAdapter 返回函数;在 Node.js 环境 xhrAdapter 返回 false,httpAdapter 返回函数。
因此,一旦 isResolvedHandle() 逻辑执行完成后。
if (!isResolvedHandle(nameOrAdapter)) {/* ... */}
会检查 adapter 变量的值,一旦有值(非 false)就说明找到适配器了,结束遍历。
if (adapter) {
break;
}
4)最终在返回适配器前做空检查
// 4)
if (!adapter) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request `,
'ERR_NOT_SUPPORT'
);
}
return adapter;
如此,就完成了跨端架构的实现。
总结
本文我们讲述了 axios 的跨端架构原理。axios 内部实际发出请求是通过 dispatchRequest() 方法处理的,再往里看则是通过适配器模式取得适应于当前环境的适配器函数。
axios 内置了 2 个适配器支持:httpAdapter 和 xhrAdapter。httpAdapter 是 Node.js 环境实现,通过 http/https 模块;xhrAdapter 这是浏览器环境实现,通过 XMLHttpRequest API 实现。Node.js 环境 xhrAdapter 返回 false,浏览器环境 httpAdapter 返回 false——这样总是能返回正确的适配器。
参考资料
[1]axios 是如何实现取消请求的?: https://juejin.cn/post/7359444013894811689
[2]你知道吗?axios 请求是 JSON 响应优先的: https://juejin.cn/post/7359580605320036415
[3]Axios: https://github.com/axios/axios/blob/v1.6.8/lib/core/Axios.js
[4]lib/adapters/adapters.js: https://github.com/axios/axios/blob/v1.6.8/lib/adapters/adapters.js