http 客户端✅
axios 有哪些特性?
答案
- 跨平台支持 采用 Promise 方式
- 浏览器端默认使用 XMLHttpRequest,可以通过
adapter: 'fetch'
切换为 Fetch API - Node.js 使用 http 模块
- 浏览器端默认使用 XMLHttpRequest,可以通过
- 拦截器 通过
axios.interceptors
属性配置请求和响应拦截器 - 转换器 通过
transformRequest、transformResponse
属性配置请求和响应数据的转换,例如蛇形变驼峰等 - 取消请求 通过 CancelToken 实现请求取消
- 超时时间 通过
timeout
属性设置请求超时时间 - 查询参数序列化 支持嵌套项的查询参数序列化
- 请求体序列化 自动请求体序列化为:
- JSON (应用程序/ison)
- 多部分/表格数据 (多部分/表格数据)
- URL编码形式 (申请书/x-www-form-urlencoded )
- 以JSON格式发布HTML表单
- 进度监控 设置node.is的带宽限制
- XSRF保护 客户端对XSRF的保护支持
其他详见 官方介绍
- 适配器
- 拦截器
- 转换器
- 取消与超时
- 查询参数序列化
- 请求体序列化
- 进度
- XSRF
axios 适配器是用来做什么的?
答案
适配器是 Axios 的 传输层抽象
,基于宿主环境判断请求代理的选择,也支持自定义适配器。
适配器的核心职责是接受请求配置并返回统一的 AxiosResponse 对象,接口定义如下
// 返回 respones 对象的核心结构
export interface AxiosResponse<T = any, D = any, H = {}> {
data: T;
status: number;
statusText: string;
headers: H & RawAxiosResponseHeaders | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D>;
request?: any;
}
export type AxiosPromise<T = any> = Promise<AxiosResponse<T>>;
// 适配器的核心定义
export interface AxiosAdapter {
(config: InternalAxiosRequestConfig): AxiosPromise;
}
可以通过 adapter
指定配置器,例如 adapter: 'fetch'
。 制定使用 fetch 适配器(浏览器和 Node.js 均支持, axios 版本 1.7及以后) 此外也支持自定义适配器
- 指定 fetch
- 自定义适配器
优先考虑使用拦截器解决请求和响应处理问题,自定义适配器使用频次较低,典型场景如下
- 支持其他宿主环境,比如类似 RN 环境中支持 bridge 方式调用原生网络请求
- 测试场景,通过自定义适配器模拟不同环境下的请求
延伸阅读:
- Axios: Adapters 适配器机制与自定义
axios 是如何区分是 nodejs 环境还是浏览器环境的?
答案
Axios通过**适配器(adapter)**选择运行环境:标准浏览器优先用 XHR 适配器;Node.js 使用 http 适配器;可以设置 adapter: 'fetch'
手动切换到 fetch 适配器。
环境判断的核心流程是在
-
每次请求时触发
adapters.getAdapter(config.adapter || defaults.adapter)
方法, 先选择配置的 adapter(字符串或函数),否则使用默认适配器, 顺序为['xhr', 'http', 'fetch']
-
getAdapter 会基于初始化时的 knownAdapters 字典, 选择第一个可用的适配器,字典结构如下
const knownAdapters: {
// 存在则映射对应适配器,不存在则返回 false
http: false | ((config: any) => Promise<any>);
xhr: false | ((config: any) => Promise<any>);
fetch: false | ((config: any) => Promise<any>);
} -
每种适配器的判定逻辑
// node 环境判断 process 是否存在
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process'
// 浏览器 xhr
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'
// fetch
const isFetchSupported = typeof fetch === 'function' && typeof Request === 'function' && typeof Response === 'function'
打包器可能注入假的 process 导致误判。所以 axios 在判断 procees 存在后利用 utils.kindOf(process) === 'process'
基于类标签来确定为真实的 process 对象。工具函数核心逻辑如下
const kindOf = (cache => thing => {
const str = toString.call(thing)
// 提取 `[object process]` 类标签
return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase())
})(Object.create(null))
axios 拦截器(interceptor) 的原理?
答案
拦截器实现了对请求和响应的链式处理,在 axios 中通过 axios.interceptors.request.use
和 axios.interceptors.response.use
方法来注册请求和响应的拦截器。执行顺序请求拦截器是栈后进先出;响应拦截器是队列先进先出。核心配置如下。
// 请求拦截器支持的配置
export interface AxiosInterceptorOptions {
// 如果全部都传入 true 则请求拦截器通知执行,默认异步
synchronous?: boolean;
// 条件拦截,只有返回 true 才会触发拦截器
runWhen?: (config: InternalAxiosRequestConfig) => boolean;
}
// 注意返回的 number 表示对应在句柄的索引,在 eject 时需要传入
type AxiosRequestInterceptorUse<T> = (
onFulfilled?: ((value: T) => T | Promise<T>) | null,
onRejected?: ((error: any) => any) | null,
options?: AxiosInterceptorOptions
) => number;
// 注意返回的 number 表示对应在句柄的索引,在 eject 时需要传入
type AxiosResponseInterceptorUse<T> = (
onFulfilled?: ((value: T) => T | Promise<T>) | null,
onRejected?: ((error: any) => any) | null
) => number;
此外拦截器支持
eject(id)
方法用于移除已注册的拦截器。id 为调用use
方法时返回的索引。clear
方法用于清空所有已注册的拦截器。
- 执行顺序
- 请求拦截器选项
- 拦截器取消
拦截器的核心执行流程如下
-
axios 初始化阶段,会创建
request、response
拦截器队列。对应对象为 InterceptorManager -
调用
request.use/response.use
注册拦截器, 本质就是往数组中推入resove、reject
句柄对象,详见 InterceptorManager 实例上 use 方法, use 调用后会返回 handles 数组中对应索引,作为取消拦截器方法的入参eject(id)
-
当调用请求时触发 axios 内部 __request 方法
- 请求拦截器推入 requestInterceptorChain
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
- 响应拦截器推入responseInterceptorChain
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
- 请求拦截器推入 requestInterceptorChain
-
对于异步请求拦截器,则合并 dispatchRequest 后,顺序执行
// 异步请求拦截器
if (!synchronousRequestInterceptors) {
// 塞入拦截器队列的 resolve, reject
const chain = [dispatchRequest.bind(this), undefined];
// 将请求拦截器推入发起请求的队列之前
chain.unshift(...requestInterceptorChain);
// 将响应拦截器放到发起请求的队列后
chain.push(...responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
// 按顺序成对推入 resove、reject 句柄
promise = promise.then(chain[i++], chain[i++]);
}
// 返回最终的 Promise
return promise;
} -
对于同步拦截器,则先同步处理完请求拦截器后,在异步执行后续动作
// 同步拦截器成对处理推入的 resove、reject 句柄
while (i < len) {
const onFulfilled = requestInterceptorChain[i++];
const onRejected = requestInterceptorChain[i++];
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected.call(this, error);
break;
}
}
// 处理请求发送,此时为异步
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
i = 0;
len = responseInterceptorChain.length;
// 异步成对处理推入的 resove、reject 句柄
while (i < len) {
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}
// 返回最终的 Promise
return promise; -
eject(id)、clear
本质就是清除单个句柄或者全部句柄class InterceptorManager {
// id 为 use(resolve,reject,options) 中返回的索引位置
eject (id) {
if (this.handlers[id]) {
this.handlers[id] = null
}
}
// 清楚全部句柄
clear () {
if (this.handlers) {
this.handlers = []
}
}
}
axios 如何实现取消请求的?
答案
Axios 通过取消信号中断请求链路,推荐用 AbortController 的 signal,旧的 CancelToken 已弃用。
- 超时取消
timeout
配置超时时间触发取消signal
通过 AbortController 手动取消
- 手动取消
CancelToken
通过 CancelToken.source() 创建取消令牌, 老版本AbortController
通过 AbortController 手动取消, 新版本v0.22.0
及以后支持
方案 | 状态 | 适用 | 触发方式 | 错误标识 | 备注 |
---|---|---|---|---|---|
AbortController.signal | 推荐 | 浏览器/Node18+ | controller.abort()/AbortSignal.timeout(ms) | ERR_CANCELED | 标准 API,配合并发控制 |
CancelToken | 弃用 | 历史项目 | source.cancel() | axios.isCancel(thrown) | 基于撤回提案,不建议新用 |
timeout | 配置项 | 响应超时 | timeout: ms | ECONNABORTED/Timeout | 不等同“取消”,针对响应耗时 |
- abort取消
- cancelToken 取消
- timeout超时取消
- abort超时
取消的核心原理
- fetch 基于 fetch
signal
配置实现取消,利用 composedSignal 来兼容 cancelToken 和 AbortController 场景,cancelToken 通过内部还是基于 toAbortSignal 返回 abortController.signal 实例 - xhr 基于 XMLHttpRequest.abort 取消,内部利用
cancelToken.subscribe
或者signal.addEventListener('abort', onCanceled)
定于取消事件,触发 abort方法 - http 基于 EventEmitter 事件模型,监听
abort
事件触发取消,内部利用cancelToken.subscribe
或者signal.addEventListener('abort', onCanceled)
定义取消事件,触发 abort方法, 如果请求没发出会触发req.destroy(err)
,发出拿到响应取消会触发responseStream.emit('error', err);responseStream.destroy();
CancelToken 本质就是集成了一个发布订阅机制对象,通过静态工程方法 source 返回一个 {token,cancel}
对象,其中 token 继承了发布订阅功能,cancel 用来控制 token.promise
的状态变化,巧妙的地方在于通过重写 token.promise.then
方法来实现对 promise 的拦截,实现同时绑定多个句柄
延伸阅读:
- Axios: Cancellation — 取消请求与示例
- MDN: AbortController — 取消信号标准
- MDN: AbortSignal.timeout — 超时信号
- Axios: Timeouts — timeout 行为与配置
解释下 axios withCredentials 配置的作用?
答案
withCredentials
控制浏览器跨源请求是否携带“凭证”(cookies、HTTP 认证、客户端证书);默认 false。- 生效前提:服务端必须返回 Access-Control-Allow-Credentials: true,且 Access-Control-Allow-Origin 不能为 *,必须为具体源。
- Cookie 约束:跨站需 SameSite=None 且 Secure;第三方 Cookie 可能被浏览器策略拦截。
- 适配器差异:仅浏览器环境(XHR/fetch)生效;Node(http 适配器)无此概念。
- 与 XSRF:axios 在标准浏览器环境下,same-origin 或 withCredentials=true 时才会注入 xsrf 头。
示例说明:
// 浏览器示例(需后端正确设置 CORS)
// 1) 不携带凭证(默认)
axios.get('https://api.example.com/profile')
.then(r => console.log('no cred ok:', r.status))
.catch(e => console.log('no cred err:', e.message))
// 2) 携带凭证(跨源共享登录态)
axios.get('https://api.example.com/profile', { withCredentials: true })
.then(r => console.log('with cred ok:', r.status))
.catch(e => console.log('with cred err:', e.message))
// 服务器需要同时返回:
// Access-Control-Allow-Origin: https://app.example.com
// Access-Control-Allow-Credentials: true
// 以及允许预检请求包含的头(如 Authorization)
常见坑:返回了 Access-Control-Allow-Origin:* 又设置了 Allow-Credentials:true 会被浏览器拒绝;跨站 Cookie 未设置 SameSite=None; Secure 导致不发送;Node 端设置 withCredentials 无效。
延伸阅读:
- MDN: XMLHttpRequest.withCredentials — 基本语义
- WHATWG Fetch CORS — CORS 协议与预检
- MDN: Set-Cookie/SameSite — 跨站 Cookie 规则
- Axios XSRF — xsrfCookieName/xsrfHeaderName 行为说明