跳到主要内容

http 客户端✅

axios 有哪些特性?

答案
  • 跨平台支持 采用 Promise 方式
    • 浏览器端默认使用 XMLHttpRequest,可以通过 adapter: 'fetch' 切换为 Fetch API
    • Node.js 使用 http 模块
  • 拦截器 通过 axios.interceptors 属性配置请求和响应拦截器
  • 转换器 通过 transformRequest、transformResponse 属性配置请求和响应数据的转换,例如蛇形变驼峰等
  • 取消请求 通过 CancelToken 实现请求取消
  • 超时时间 通过 timeout 属性设置请求超时时间
  • 查询参数序列化 支持嵌套项的查询参数序列化
  • 请求体序列化 自动请求体序列化为:
    • JSON (应用程序/ison)
    • 多部分/表格数据 (多部分/表格数据)
    • URL编码形式 (申请书/x-www-form-urlencoded )
    • 以JSON格式发布HTML表单
  • 进度监控 设置node.is的带宽限制
  • XSRF保护 客户端对XSRF的保护支持

其他详见 官方介绍

<!doctype html>
<meta charset="UTF-8" />
<title>Axios 适配器(XHR vs fetch)</title>
<script type="module">
  import axios from "https://esm.sh/axios";

  async function main() {
    console.log("使用默认 XHR 适配器发起请求...");
    const r1 = await axios.get("https://httpbin.org/get", { params: { q: "xhr" } });
    console.log("XHR →", r1.status, r1.statusText, r1.data.args);

    console.log("使用 fetch 适配器发起请求...");
    const r2 = await axios.get("https://httpbin.org/get", { params: { q: "fetch" }, adapter: "fetch" });
    console.log("fetch →", r2.status, r2.statusText, r2.data.args);
  }

  main().catch(err => console.error("adapter 示例出错:", err));
</script>

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及以后) 此外也支持自定义适配器

<script type="module">
  import axios from "https://esm.sh/axios";

  axios
    .get("https://jsonplaceholder.typicode.com/todos/1", {
      adapter: "fetch",
    })
    .then((res) => {
      console.log(res.data);
    })
    .catch((error) => {
      console.error(error);
    });
</script>

延伸阅读:

axios 是如何区分是 nodejs 环境还是浏览器环境的?

答案

Axios通过**适配器(adapter)**选择运行环境:标准浏览器优先用 XHR 适配器;Node.js 使用 http 适配器;可以设置 adapter: 'fetch' 手动切换到 fetch 适配器。

环境判断的核心流程是在

  1. 每次请求时触发 adapters.getAdapter(config.adapter || defaults.adapter) 方法, 先选择配置的 adapter(字符串或函数),否则使用默认适配器, 顺序为 ['xhr', 'http', 'fetch']

  2. getAdapter 会基于初始化时的 knownAdapters 字典, 选择第一个可用的适配器,字典结构如下

    const knownAdapters: {
    // 存在则映射对应适配器,不存在则返回 false
    http: false | ((config: any) => Promise<any>);
    xhr: false | ((config: any) => Promise<any>);
    fetch: false | ((config: any) => Promise<any>);
    }
  3. 每种适配器的判定逻辑

    // 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.useaxios.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 方法用于清空所有已注册的拦截器。
<script type="module">
  import axios from "https://esm.sh/axios";

  // 整体拦截器执行符合洋葱模型
  // 请求拦截器按照栈后注入的先执行
  axios.interceptor.request.use((config) => {
    console.log("Request Interceptor 1:", config);
    return config;
  });
  axios.interceptor.request.use((config) => {
    console.log("Request Interceptor 2:", config);
    return config;
  });

  // 响应拦截器按照队列先进先出
  axios.interceptor.response.use((config) => {
    console.log("Response Interceptor 1:", config);
    return config;
  });
  axios.interceptor.response.use((config) => {
    console.log("Response Interceptor 2:", config);
    return config;
  });

  axios.get("https://jsonplaceholder.typicode.com/todos/1")
</script>

拦截器的核心执行流程如下

  1. axios 初始化阶段,会创建 request、response 拦截器队列。对应对象为 InterceptorManager

  2. 调用 request.use/response.use 注册拦截器, 本质就是往数组中推入 resove、reject句柄对象,详见 InterceptorManager 实例上 use 方法, use 调用后会返回 handles 数组中对应索引,作为取消拦截器方法的入参 eject(id)

  3. 当调用请求时触发 axios 内部 __request 方法

    1. 请求拦截器推入 requestInterceptorChain requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
    2. 响应拦截器推入responseInterceptorChain responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
  4. 对于异步请求拦截器,则合并 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;
    }
  5. 对于同步拦截器,则先同步处理完请求拦截器后,在异步执行后续动作

      // 同步拦截器成对处理推入的 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;

  6. 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: msECONNABORTED/Timeout不等同“取消”,针对响应耗时
<script type="module">
// AbortController 手动取消(浏览器/Node 18+,此处示例以浏览器 ESM 运行)
import axios from 'https://esm.sh/axios'

const ctrl = new AbortController()
const p = axios.get('https://httpbin.org/delay/3', { signal: ctrl.signal })
  .then(r => console.log('OK status:', r.status))
  // 利用 axios.isCancel 判断是取消请求
  .catch(e => console.dir(axios.isCancel(e) ? 'manual cancel ->' : 'error ->', e.code || e.name || e.message))

setTimeout(() => ctrl.abort(), 10) // 300ms 后中断
await p

</script>

取消的核心原理

  1. fetch 基于 fetch signal 配置实现取消,利用 composedSignal 来兼容 cancelToken 和 AbortController 场景,cancelToken 通过内部还是基于 toAbortSignal 返回 abortController.signal 实例
  2. xhr 基于 XMLHttpRequest.abort 取消,内部利用 cancelToken.subscribe 或者 signal.addEventListener('abort', onCanceled) 定于取消事件,触发 abort方法
  3. 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 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 无效。

延伸阅读: