Skip to content

Axios 项目解读

项目信息

  • 项目名称:Axios
  • 项目描述:Axios 是一个跨运行时(Browser + Node + React Native + Bun + Deno)的 Promise-based HTTP 客户端,通过"能力匹配适配器 + 严格分离 core/I/O + 拦截器 LIFO/FIFO + 统一错误码"四件套,把跨端 API 碎片化、取消语义双轨、配置合并安全等棘手问题用克制的工程决策解决。
  • 项目地址:https://github.com/axios/axios

1. 项目概览

1.1 项目定位与核心价值

  • 一句话定位:Axios 是一个跨运行时(Browser + Node.js + React Native + Bun + Deno)的 Promise-based HTTP 客户端库,通过适配器模式在多端提供统一的"请求 / 响应 / 拦截器 / 错误 / 取消"API。

  • 解决的核心痛点

    1. 跨端 API 碎片化:浏览器侧 XHR/Fetch、Node 侧 http/https、React Native 侧又有自己的网络栈——开发者需要写多套代码。axios 通过能力匹配的 adapter 抽象抹平差异。
    2. 请求生命周期管理缺失:原生 fetch/XHR 没有标准的"拦截器 / 转换链 / 统一错误模型"。axios 提供 InterceptorManager + AxiosError 错误码体系 + transformRequest/transformResponse 把横切关注点(鉴权头注入、CSRF、重试、错误归一化)变成可插拔组件。
    3. 取消语义不统一:旧 CancelToken 与新 AbortSignal 在不同生态中各占一边。axios 同时支持两者且不破坏任一路径,适配旧代码的渐进式迁移。
    4. TypeScript 与多模块形态兼容:源是 ESM(type: module),但下游可能是 CJS/UMD/Bun/Deno/React-Native。exports 条件字段 + Rollup 矩阵保证任意入口形态都可用。
  • 目标用户/开发者画像

    • 前端工程师:在 React/Vue/Svelte 等 SPA 中调用 REST API,需要拦截器统一注入 token、统一错误处理。
    • Node.js 后端工程师:使用 SSR、BFF、CLI 工具、爬虫、代理聚合服务,需要稳定 HTTP 客户端与流式上传能力。
    • 全栈/SDK 作者:需要为多种运行时(浏览器 + Node + React Native + Bun + Deno)发布一致的 SDK 体验。
    • 测试 / 工具开发者:通过 getAdapter / mergeConfig 暴露的能力,构造 mock 适配器或自定义重试逻辑。

1.2 目标场景与典型 Case

  • 场景一:单页应用的统一 API 客户端

    • 通过 axios.create({ baseURL }) 派生实例;通过 interceptors.request.use(...) 注入 token;通过 interceptors.response.use(...) 归一化错误并弹 toast。
    • 通过 validateStatus 灵活控制哪些 HTTP 状态码算"成功"(业务方常将 4xx 视作业务错误而非异常)。
  • 场景二:Node 服务的文件上传 / 代理 / 重定向跟随

    • 使用 HTTP 适配器,支持 FormData / 流式上传(formDataToStream),支持 https-proxy-agent 代理,支持 follow-redirects 跟随重定向。
    • maxRedirectsmaxBodyLengthmaxContentLength 控制边界。
  • 场景三:请求取消与路由切换防抖

    • 浏览器侧:使用 AbortController.signal 在路由切换时中止未完成请求,dispatchRequest 阶段同步检查 + 适配器侧移除监听(避免泄漏)。
    • Node 侧:使用旧 CancelTokensignal 取消进行中的流式上传/下载。
  • 场景四:渐进式替换底层

    • 通过 axios.getAdapter(['xhr', 'fetch']) 让库能力匹配:若浏览器支持 fetch,优先用 fetch(更现代、Stream 原生);否则回退 XHR。
    • 第三方 SDK 作者可注入自定义 adapter(如 gRPC-Web bridge、Electron IPC)。

典型使用 Case(伪代码)

javascript
// Case 1: 浏览器单页应用
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  validateStatus: (s) => s >= 200 && s < 300 || s === 304,
});

api.interceptors.request.use((config) => {
  config.headers.set('Authorization', `Bearer ${getToken()}`);
  return config;
});

api.interceptors.response.use(
  (r) => r.data,
  (err) => {
    if (err.response?.status === 401) logout();
    return Promise.reject(err);
  }
);

await api.get('/user/me');

// Case 2: Node 端上传 + 取消
const ctrl = new AbortController();
axios.post('https://api.example.com/upload', formData, {
  signal: ctrl.signal,
  maxBodyLength: 100 * 1024 * 1024,
  onUploadProgress: (e) => console.log(e.loaded / e.total),
});
setTimeout(() => ctrl.abort(), 5000);

1.3 核心技术亮点

亮点说明
适配器能力匹配不依赖环境名硬编码,按能力真假值选择
拦截器 LIFO/FIFO请求拦截器后注册先执行;响应拦截器先注册先执行;支持 synchronous 优化
统一错误码14 个 ERR_* 常量 + AxiosError.from() 包装器
Header 标准化AxiosHeaders 大小写不敏感 + 动态注入链式 accessor
配置合并安全Object.create(null) + 显式过滤 __proto__/constructor/prototype
取消双轨并存CancelToken + AbortSignal 同时支持
包形态兼容ESM 源 + Rollup 多形态产物(CJS/UMD/Browser/Node)

1.4 技术栈与选型对比

类别技术备注
源模块系统ESM (type: module)显式 .js 后缀导入
打包器Rollup多形态产物:ESM/UMD/CJS × browser/node
构建编排Gulpclear / version 任务
测试框架Vitest单元/浏览器/headless 三 project
浏览器自动化PlaywrightVitest 浏览器 project 驱动
Lint / 规范ESLint v10(扁平) + Prettier + lint-staged + commitlint强制 130 字符 commit header
Git 钩子huskynpm rebuild husky && npx husky 激活
依赖完整性lockfile-lint校验 HTTPS host + integrity
Node 端网络follow-redirects / form-data / https-proxy-agent / proxy-from-env4 个运行时依赖
类型系统TypeScript(仅类型检查)index.d.ts / index.d.cts
文档站VitePress多语言:英文 + 中文 + 法语 + 西班牙语

选型对比决策点

  • A. 适配器模式 vs 运行时分支:能力匹配 + 平台层 package.json 替换 → 核心保持纯净、扩展点开放
  • B. ESM + CJS 双形态:现代工具链 ESM 优先 + 老生态 CJS 兜底
  • C. AxiosError 独立体系:消费方写一次错误处理就能跨平台工作
  • D. 拦截器 LIFO/FIFO + 同步选项:性能与表达力兼得
  • E. 配置合并 own-prop 严格判定:默认安全,避免常见 footgun
  • F. 取消语义双轨:向后兼容与现代化同时推进

2. 整体架构设计

2.1 架构概述

Axios 采用分层 + 适配器 + 中间件链的混合架构,分为四层:

  1. 用户接口层 (Entry)index.jslib/axios.js,负责实例化、暴露方法别名。
  2. 核心域逻辑层 (Core)lib/core/*,包含 Axios 类、调度、配置合并、Header 容器、错误、拦截器、状态判定等与 I/O 无关的域逻辑。
  3. 平台抽象层 (Platform)lib/platform/{browser,node,common},通过 package.jsonbrowser/react-native 字段在打包时静态替换,再由 lib/platform/index.js 聚合运行时类引用(FormData/URLSearchParams/Blob)。
  4. I/O 适配层 (Adapters)lib/adapters/{xhr,http,fetch}.js,真正发起网络请求;lib/adapters/adapters.js 提供能力匹配getAdapter 入口。

辅助层(Helpers/Tools/Defaults/Cancel/Env)横向服务于上述四层,提供可独立复用的工具函数。

2.2 整体架构图

text
+================================================================+
|  用户接口层 (Entry Layer)                                       |
|  +-------------------+    +--------------------------------+    |
|  |  index.js (ESM)   | -> |  lib/axios.js                  |    |
|  |  (default + named)|    |  (createInstance + 静态属性)   |    |
|  +-------------------+    +---------------+----------------+    |
|                                              | (Axios instance) |
+================================================================+
|  核心域逻辑层 (Core Domain - I/O 无关)                          |
|  +-----------------+   +-----------------+   +----------------+ |
|  | Axios.js        |   | mergeConfig.js  |   | AxiosHeaders.js| |
|  | - request()     |   | - 过滤 __proto__|   | - 大小写不敏感 | |
|  | - method 别名   |   | - headers 深度  |   | - normalize()  | |
|  +-----------------+   +-----------------+   +----------------+ |
|  +-----------------+   +-----------------+   +----------------+ |
|  | dispatchRequest |   | InterceptorMgr  |   | AxiosError.js  | |
|  | - 取消检测      |   | - LIFO/FIFO     |   | - 14 个 ERR_*  | |
|  | - transform     |   | - synchronous   |   | - from() 包装  | |
|  +-----------------+   +-----------------+   +----------------+ |
|  +-----------------+   +-----------------+                      |
|  | settle.js       |   | buildFullPath   |                      |
|  | - validateStatus|   | - baseURL+url   |                      |
|  +-----------------+   +-----------------+                      |
+================================================================+
|  辅助层 (Helpers / Defaults / Cancel / Env)                     |
|  +-----------------+   +-----------------+   +----------------+ |
|  | defaults/       |   | cancel/         |   | env/data.js    | |
|  | - transform链   |   | - CancelToken   |   | - VERSION      | |
|  | - validateStatus|   | - CanceledError |   +----------------+ |
|  +-----------------+   +-----------------+                      |
|  +--------------------------------------------------------------+|
|  | helpers/  (36 个,通用工具:bind/buildURL/parseHeaders/...)   ||
|  | utils.js (953 行:KindOf/Type/merge/extend 等)               ||
|  +--------------------------------------------------------------+|
+================================================================+
|  平台抽象层 (Platform Abstraction)                              |
|  +-----------------+   +-----------------+   +----------------+ |
|  | browser/        |   | node/           |   | common/        | |
|  | - index.js      |   | - index.js      |   | - utils.js     | |
|  | - classes/      |   | - classes/      |   | (kindOf 增强)  | |
|  |   Blob,FormData |   |   FormData,URL  |   +----------------+ |
|  +-----------------+   +-----------------+                      |
|  静态替换策略: package.json browser 字段 →                       |
|    ./lib/adapters/http.js -> ./lib/helpers/null.js               |
|    ./lib/platform/node/index.js -> ./lib/platform/browser/...   |
+================================================================+
|  I/O 适配层 (Adapters)                                          |
|  +-------------------------------------------------------------+|
|  |  adapters.js                                                ||
|  |  - knownAdapters = { http, xhr, fetch }                     ||
|  |  - getAdapter(['xhr','http','fetch']) ← 能力匹配            ||
|  +--------+----------------+---------------------+-------------+|
|           |                |                     |              |
|  +--------v-------+  +-----v-------+  +----------v-------+      |
|  | http.js        |  | xhr.js      |  | fetch.js         |      |
|  | - Node HTTP/   |  | - XHR       |  | - Fetch API      |      |
|  |   HTTPS/HTTP2  |  | - 浏览器    |  | - 流 + AbortSig  |      |
|  | - 1325 行      |  | - 227 行    |  | - 552 行         |      |
|  | - 代理/重定向/ |  |             |  |                  |      |
|  |   FormData/流  |  |             |  |                  |      |
|  +----------------+  +-------------+  +------------------+      |
+================================================================+

分层职责说明

层级关键模块职责
用户接口层index.jslib/axios.js默认实例工厂、命名导出、静态属性挂载
核心域逻辑层lib/core/*请求派发、配置合并、Header 标准化、错误、拦截器、状态判定(无 I/O
平台抽象层lib/platform/*跨平台类引用、协议白名单差异处理
I/O 适配层lib/adapters/*XHR / HTTP / Fetch 三大适配器 + 能力匹配选择
辅助层lib/utils.jslib/helpers/*lib/defaults/*lib/cancel/*lib/env/*通用工具、默认配置、取消语义、运行时元数据

2.3 目录结构

shell
axios/
├── AGENTS.md                       # 【配置】AI/人类贡献者协作规范(架构边界/命名约定/错误码/拦截器顺序/安全准则)
├── CLAUDE.md                       # 【配置】Claude Code 项目入口(引用 AGENTS.md)
├── CHANGELOG.md                    # 【配置】发布历史(release-owned,由发版时维护)
├── COLLABORATOR_GUIDE.md           # 【配置】维护者协作指南
├── CONTRIBUTING.md                 # 【配置】贡献者指南
├── CONTRIBUTORS.md                 # 【配置】贡献者名单
├── CODE_OF_CONDUCT.md              # 【配置】社区行为准则
├── ECOSYSTEM.md                    # 【配置】生态与官方插件列表
├── LICENSE                         # 【配置】MIT 许可证
├── MIGRATION_GUIDE.md              # 【配置】v0.x → v1.x 迁移指南
├── PRE_RELEASE_CHANGELOG.md        # 【配置】未发布变更记录
├── PRE_RELEASE_DOCS.md             # 【配置】未发布文档待办
├── README.md                       # 【配置】英文 README
├── README-CN.md                    # 【配置】中文 README(国际化基线,本次新增)
├── SECURITY.md                     # 【配置】安全策略
├── THREATMODEL.md                  # 【配置】威胁建模文档

├── index.js                        # 【核心基建】ESM 顶层入口(unwrap axios default + named exports)
├── index.d.ts                      # 【配置】TypeScript 类型定义(739 行)
├── index.d.cts                     # 【配置】TypeScript CJS 类型(export = axios)

├── lib/                            # 【核心基建】源码主目录
   ├── axios.js                    # 【核心基建】默认实例工厂(createInstance + 静态属性挂载)
   ├── utils.js                    # 【工具集】通用工具聚合(953 行:Object/Type/KindOf 判定/合并/扩展)

   ├── core/                       # 【核心基建】axios 域逻辑(不依赖具体 I/O)
   ├── Axios.js                # 【核心基建】Axios 类(请求派发入口、method 别名、拦截器链、headers 合并)
   ├── AxiosError.js           # 【核心基建】标准化错误类 + 错误码常量(ERR_*) + from() 包装器 + redact
   ├── AxiosHeaders.js         # 【核心基建】Header 容器(大小写不敏感 + 解析 + 规范化 + accessor)
   ├── InterceptorManager.js   # 【核心基建】拦截器注册/移除/遍历(use/eject/clear/forEach)
   ├── dispatchRequest.js      # 【核心基建】请求调度(取消检测 → headers 转换 → adapter → 响应转换)
   ├── mergeConfig.js          # 【核心基建】配置合并(安全敏感:过滤 __proto__/constructor/prototype)
   ├── buildFullPath.js        # 【核心基建】URL 全路径构建(baseURL + url + allowAbsoluteUrls)
   ├── transformData.js        # 【核心基建】请求/响应数据转换链
   ├── settle.js               # 【核心基建】状态判定(validateStatus → resolve/reject)
   └── README.md               # 【配置】core 模块说明

   ├── adapters/                   # 【核心基建】I/O 适配器
   ├── adapters.js             # 【核心基建】能力探测与适配器选择(getAdapter 能力匹配)
   ├── xhr.js                  # 【核心基建】浏览器 XMLHttpRequest 适配器(227 行)
   ├── http.js                 # 【核心基建】Node.js http/https/http2 适配器(1325 行:重定向/FormData/流/代理/HTTP2 sessions)
   ├── fetch.js                # 【核心基建】Fetch API 适配器(552 行:进度模拟/流/AbortSignal/Cookies)
   └── README.md               # 【配置】adapters 说明

   ├── cancel/                     # 【核心基建】取消语义(双轨:CancelToken + AbortSignal)
   ├── CancelToken.js          # 【核心基建】传统取消令牌(subscribe + throwIfRequested + toAbortSignal)
   ├── CanceledError.js        # 【核心基建】取消错误类(extends AxiosError,带 __CANCEL__ 标记)
   └── isCancel.js             # 【核心基建】取消判定工具

   ├── defaults/                   # 【核心基建】默认配置
   ├── index.js                # 【核心基建】默认配置(transformRequest/transformResponse/validateStatus/headers)
   └── transitional.js         # 【核心基建】transitional 默认值(向后兼容行为开关)

   ├── env/                        # 【核心基建】运行时元数据
   ├── data.js                 # 【核心基建】version 注入(gulp version 生成,勿手改)
   ├── README.md               # 【配置】env 说明
   └── classes/                # 【工具集】跨平台类引用(URLSearchParams 等 polyfill 入口)

   ├── helpers/                    # 【工具集】通用工具(尽量可独立于 axios 复用)
   ├── AxiosTransformStream.js # 【工具集】请求体 transform 流封装
   ├── AxiosURLSearchParams.js # 【工具集】URLSearchParams 包装(带迭代/排序)
   ├── Http2Sessions.js        # 【工具集】HTTP/2 会话复用池
   ├── HttpStatusCode.js       # 【工具集】标准 HTTP 状态码枚举
   ├── ZlibHeaderTransformStream.js # 【工具集】zstd 头检测 transform
   ├── bind.js                 # 【工具集】Function.prototype.bind 替代(保留 arguments)
   ├── buildURL.js             # 【工具集】URL + params + serializer 拼接
   ├── callbackify.js          # 【工具集】Promise → 回调风格转换
   ├── combineURLs.js          # 【工具集】baseURL + path 拼接
   ├── composeSignals.js       # 【工具集】AbortSignal 组合(任一触发即 abort)
   ├── cookies.js              # 【工具集】浏览器 cookie 读写
   ├── deprecatedMethod.js     # 【工具集】方法弃用提示包装
   ├── estimateDataURLDecodedBytes.js # 【工具集】data: URL 字节数估算
   ├── formDataToJSON.js       # 【工具集】FormData → JSON 转换
   ├── formDataToStream.js     # 【工具集】FormData → Node stream
   ├── fromDataURI.js          # 【工具集】data: URL 解析
   ├── isAbsoluteURL.js        # 【工具集】绝对 URL 判定
   ├── isAxiosError.js         # 【工具集】AxiosError 判定
   ├── isURLSameOrigin.js      # 【工具集】同源判定(XSRF 配套)
   ├── null.js                 # 【工具集】空模块占位(browser 字段下替换 http.js)
   ├── parseHeaders.js         # 【工具集】header 字符串解析
   ├── parseProtocol.js        # 【工具集】URL 协议解析
   ├── progressEventReducer.js # 【工具集】进度事件节流/聚合 + 装饰器
   ├── readBlob.js             # 【工具集】Blob 读取工具
   ├── resolveConfig.js        # 【工具集】请求/响应配置解析
   ├── sanitizeHeaderValue.js  # 【工具集】Header 值清洗(防 CRLF 注入)+ toByteStringHeaderObject
   ├── shouldBypassProxy.js    # 【工具集】noProxy 判定
   ├── speedometer.js          # 【工具集】速率采样(带宽限流)
   ├── spread.js               # 【工具集】数组参数展开
   ├── throttle.js             # 【工具集】流式限流
   ├── toFormData.js           # 【工具集】JS 对象 → FormData
   ├── toURLEncodedForm.js     # 【工具集】JS 对象 → URL-encoded 字符串
   ├── trackStream.js          # 【工具集】流式进度追踪
   ├── validator.js            # 【工具集】config 校验断言(assertOptions + spelling)
   └── README.md               # 【配置】helpers 说明

   └── platform/                   # 【核心基建】平台差异抽象
       ├── index.js                # 【核心基建】平台选择入口(导出 common utils + 运行时 classes)
       ├── common/
   └── utils.js            # 【工具集】跨平台通用工具(kindOf 增强等)
       ├── browser/
   ├── index.js            # 【核心基建】浏览器平台入口(默认协议白名单等)
   └── classes/            # 【工具集】浏览器环境类引用占位(FormData/URLSearchParams/Blob)
       └── node/
           ├── index.js            # 【核心基建】Node 平台入口(protocols 扩展)
           └── classes/            # 【工具集】Node 环境类引用(FormData/URLSearchParams 走 form-data 包)

├── docs/                           # 【配置】文档站(VitePress 源码,多语言 zh/fr/es)
   ├── .vitepress/                 # 【配置】VitePress 配置与主题
   ├── pages/                      # 【配置】英文文档页
   ├── getting-started/        # 【配置】入门
   ├── advanced/               # 【配置】进阶
   └── misc/                   # 【配置】杂项
   ├── zh/                         # 【配置】中文文档
   ├── fr/                         # 【配置】法语文档
   ├── es/                         # 【配置】西班牙语文档
   ├── data/                       # 【配置】文档数据
   ├── patches/                    # 【配置】文档依赖补丁
   ├── public/                     # 【配置】静态资源
   ├── scripts/                    # 【配置】文档脚本
   ├── package.json                # 【配置】文档子包
   └── index.md                    # 【配置】文档首页

├── examples/                       # 【业务模块】示例代码(按用例分目录)
   ├── server.js                   # 【配置】示例 server
   ├── network_enhanced.js         # 【业务模块】网络增强示例
   ├── improved-network-errors.md  # 【配置】增强错误说明
   ├── abort-controller/           # 【业务模块】AbortController 用例
   ├── all/                        # 【业务模块】axios.all 用例
   ├── amd/                        # 【业务模块】AMD 加载
   ├── get/                        # 【业务模块】GET 用例
   ├── post/                       # 【业务模块】POST 用例
   ├── postMultipartFormData/      # 【业务模块】多部分表单用例
   ├── transform-response/         # 【业务模块】响应转换用例
   └── upload/                     # 【业务模块】上传用例

├── sandbox/                        # 【业务模块】sandbox(本地试运行)

├── scripts/                        # 【工具集】仓库维护脚本
   └── axios-build-instance.js     # 【工具集】构建配置样例生成

├── tests/                          # 【质量保证】测试目录(runtime-first 布局)
   ├── README.md                   # 【配置】测试说明
   ├── setup/                      # 【工具集】Vitest setup
   ├── server.js               # 【工具集】本地 HTTP server(务必 try/finally 清理)
   └── browser.setup.js        # 【工具集】浏览器测试 setup
   ├── unit/                       # 【质量保证】单元测试(vitest)
   ├── api.test.js             # 【质量保证】API 测试
   ├── axios.test.js           # 【质量保证】Axios 类测试
   ├── axiosHeaders.test.js    # 【质量保证】AxiosHeaders 测试
   ├── prototypePollution.test.js # 【质量保证】原型污染回归
   ├── adapters/               # 【质量保证】适配器测试
   └── ...                     # 【质量保证】其他单元测试
   ├── browser/                    # 【质量保证】浏览器测试(需 Playwright)
   ├── smoke/                      # 【质量保证】打包后烟囱测试
   ├── cjs/                    # 【质量保证】CJS 烟囱
   ├── esm/                    # 【质量保证】ESM 烟囱
   ├── bun/                    # 【质量保证】Bun 烟囱
   └── deno/                   # 【质量保证】Deno 烟囱
   └── module/                     # 【质量保证】模块兼容性测试
       ├── cjs/                    # 【质量保证】CJS + TS 4.9
       └── esm/                    # 【质量保证】ESM + TS 5.x

├── gulpfile.js                     # 【配置】gulp 任务(clear / version / build 编排)
├── rollup.config.js                # 【配置】Rollup 配置(多形态:ESM/UMD/CJS × browser/node)
├── vitest.config.js                # 【配置】Vitest 配置(unit/browser/browser-headless 三个 project)
├── eslint.config.js                # 【配置】ESLint v10 扁平配置
├── tsconfig.json                   # 【配置】TypeScript 配置
├── tslint.json                     # 【废弃】旧 TSLint 配置(保留兼容)
├── webpack.config.js               # 【配置】Webpack 配置(sandbox 演示)
├── package.json                    # 【配置】npm 元数据(exports/dependencies/scripts)
├── package-lock.json               # 【配置】锁文件(lockfile-lint 校验)
└── .npmrc                          # 【配置】npm 配置(ignore-scripts=true)

3. 模块依赖与调用关系

3.1 全局入口与核心路由

请求入口分两条

  1. 静态方法(如 axios.get()):index.js 暴露的 default 导出其实是 lib/axios.js 创建的实例(createInstance(defaults))。调用 axios.get(url, config) 实际是 Axios.prototype.get 闭包调用 this.request(config)
  2. 实例方法(如 axios.create().get()):用户在 createInstance 闭包内通过 instance.create(instanceConfig) 派生新实例,新实例继承了 Axios.prototype 上的方法。

request() 是真正的总入口 → 内部进入 _request() 完成 config 合并 + headers 合并 + 拦截器链构建 → dispatchRequest() 选中 adapter → adapter 发起 I/O → 响应回到响应拦截器链 → resolve/reject。

3.2 调用拓扑

text
index.js  (ESM 顶层入口)
  └── default = axios
       └── lib/axios.js  createInstance(defaults)
            ├── const context = new Axios(defaults)
            │       ├── this.defaults = defaults
            │       └── this.interceptors = { request, response }  (两个 InterceptorManager)
            ├── const instance = bind(Axios.prototype.request, context)
            ├── utils.extend(instance, Axios.prototype, context, { allOwnKeys: true })
            │       -> instance.{get,post,put,patch,delete,head,options,request,...}
            ├── utils.extend(instance, context, null, { allOwnKeys: true })
            │       -> instance.{defaults, interceptors}
            ├── instance.create(instanceConfig) = createInstance(mergeConfig(defaults, instanceConfig))
            └── 静态挂载: Axios / AxiosError / CanceledError / CancelToken / isCancel
                          / VERSION / toFormData / isAxiosError / AxiosHeaders
                          / formToJSON / getAdapter / HttpStatusCode / all / spread
                          / mergeConfig / default = axios

Axios.prototype.request(configOrUrl, config)            [lib/core/Axios.js:39-80]
  └── await this._request(...)   (async 包装,补全错误栈)
       └── _request(configOrUrl, config)                [lib/core/Axios.js:82-233]
            ├── 1. config = mergeConfig(this.defaults, config)
            ├── 2. 校验 transitional / paramsSerializer
            ├── 3. config.method = (config.method || this.defaults.method || 'get').toLowerCase()
            ├── 4. headers = AxiosHeaders.concat(common[method], headers)
            ├── 5. 构建 requestInterceptorChain (LIFO,过滤 runWhen)
            ├── 6. 构建 responseInterceptorChain (FIFO)
            ├── 7. (synchronousRequestInterceptors 优化路径)
            │       while (i < len) newConfig = onFulfilled(newConfig)   // 同步执行
            │       promise = dispatchRequest.call(this, newConfig)
            │       while (i < len) promise = promise.then(...)           // 响应链
            └── 8. (默认异步路径)
                    chain = [dispatchRequest.bind(this), undefined, ...responseInterceptors]
                    chain.unshift(...requestInterceptors)
                    promise = Promise.resolve(config)
                    while (i < len) promise = promise.then(chain[i++], chain[i++])

dispatchRequest(config)                                  [lib/core/dispatchRequest.js:34-89]
  ├── throwIfCancellationRequested(config)                // 取消检测
  ├── config.headers = AxiosHeaders.from(config.headers) // 标准化
  ├── config.data = transformData.call(config, transformRequest)  // 请求体转换
  ├── config.headers.setContentType('application/x-www-form-urlencoded', false)  // post/put/patch
  ├── adapter = adapters.getAdapter(config.adapter || defaults.adapter, config)   // 能力匹配
  └── adapter(config).then(
        onAdapterResolution =>  throwIfCancellationRequested + transformResponse + AxiosHeaders.from
        onAdapterRejection  =>  if (!isCancel)  throwIfCancellationRequested + transformResponse
      )

adapters.getAdapter(adapters, config)                    [lib/adapters/adapters.js:65-115]
  ├── adapters = Array.isArray ? adapters : [adapters]
  ├── for i in adapters:
  │     ├── nameOrAdapter = adapters[i]
  │     ├── if !isResolvedHandle: adapter = knownAdapters[id.toLowerCase()]
  │     ├── if adapter && (typeof fn === function || (adapter = adapter.get(config))): break
  └── return adapter  (或抛 AxiosError ERR_NOT_SUPPORT)

knownAdapters:                                            [lib/adapters/adapters.js:16-22]
  - http:     import httpAdapter from './http.js'  (Node 端)
  - xhr:      import xhrAdapter from './xhr.js'   (浏览器端,导出时已 capability-check)
  - fetch:    { get: fetchAdapter.getFetch }       (能力检测后再 get)

3.3 核心业务实体与关联

text
[Axios Instance] 1 -------> 1 [InterceptorManager (request)]
       |                  \
       |                   1 -------> 1 [InterceptorManager (response)]
       |
       | 1 -------> 1 [defaults (config snapshot)]
       |
       | 1 -------> N [Interceptor Handler]
       |                   |
       |                   +-- { fulfilled, rejected, synchronous, runWhen }
       |
       | 1 -------> 1 [AxiosHeaders (request)]
       | 1 -------> 1 [AxiosHeaders (response)]
       |
       | 1 -------> 1 [Config (per-request)]
                       |
                       +-- url, method, baseURL, headers, data, params
                       +-- timeout, signal, cancelToken, validateStatus
                       +-- transformRequest[], transformResponse[]
                       +-- adapter (string|function|array)
                       +-- onUploadProgress, onDownloadProgress
                       +-- httpAgent, httpsAgent, maxContentLength, ...

[AxiosError] 1 <-------> 1 [CancelToken | AbortSignal]
       |
       +-- name = 'AxiosError', isAxiosError = true
       +-- code (14 个常量: ERR_NETWORK, ECONNABORTED, ETIMEDOUT, ERR_FR_TOO_MANY_REDIRECTS, ...)
       +-- config, request, response (可选)

[CancelToken] 1 <-------> 1 [CanceledError (extends AxiosError)]
       |
       +-- reason (Cancel 调用时填充)
       +-- _listeners (订阅列表)
       +-- toAbortSignal()  (与 AbortSignal 互转)

[Adapter] 1 <-------> 1 [Request/Response Pipeline]
       |
       +-- xhr     -> XMLHttpRequest
       +-- http    -> http.ClientRequest / https.ClientRequest / http2 session
       +-- fetch   -> fetch API

4. 核心模块详解

模块一:Axios 请求调度器 (lib/core/Axios.js + lib/core/dispatchRequest.js)

  • 模块名称Axios + dispatchRequest
  • 设计说明双层调度Axios._request() 负责拦截器链构建;dispatchRequest() 负责请求体转换 + adapter 选择 + 响应体转换。两者解耦后,_request 关心"链",dispatchRequest 关心"内容"。
  • 拦截器顺序控制:通过 transitional.legacyInterceptorReqResOrdering 切换新旧行为(LIFO 推入 vs 旧版 push)。
  • 同步优化:当所有请求拦截器都声明 synchronous: true 时,跳过 Promise.then 链,性能与可读性兼得。
  • 关键安全:错误处理 request() async 包装补全 stack 链(避免 Node 异步调用丢失栈帧)。

内部结构图 :

text
+--------------------------+
|  Axios.prototype.request |  (async 包装,补全 stack)
+-----------+--------------+
            |
            v
+--------------------------+
|  Axios.prototype._request|
+------+----------+--------+
       |          |
       |          | headers (concat common[method] + user headers)
       |          v
       |    +------------------+
       |    |  AxiosHeaders    |
       |    +------------------+
       |
       | build requestInterceptorChain (LIFO)
       | build responseInterceptorChain (FIFO)
       |
       v
+----------------------------------------------------+
|  chain = [dispatchRequest.bind(this), undefined,   |
|           ...requestInterceptors, ...responseInterceptors] |
|  promise = Promise.resolve(config)                 |
|  while (i < len) promise = promise.then(chain[i++],|
|                                          chain[i++])|
+--------------------+-------------------------------+
                     |
                     v
+--------------------------------------+
|  dispatchRequest(config)             |
|  - throwIfCancellationRequested      |
|  - AxiosHeaders.from(headers)        |
|  - transformData(transformRequest)   |
|  - getAdapter(...)                   |
|  - adapter(config)                   |
|  - .then(transformResponse + 取消再检)|
+--------------------------------------+

模块二:适配器能力匹配 (lib/adapters/adapters.js)

  • 模块名称adapters 适配器注册表与能力匹配
  • 设计说明不依赖环境名,靠能力真假值判定。
    • http 永远是已加载的 httpAdapter 函数(Node 端),但浏览器端 package.jsonhttp.js 替换为 helpers/null.js → 导入的是 null
    • xhr 在导入时立即 capability-check:typeof XMLHttpRequest !== 'undefined',否则导出 false
    • fetch{ get: getFetch } 对象,get(config) 内部再次运行时检查 typeof fetch === 'function'
  • getAdapter 策略:遍历传入数组,第一个能成功解析(函数 or 对象.get 返回真值)的胜出;全失败抛 AxiosError('There is no suitable adapter...', 'ERR_NOT_SUPPORT')
  • 关键安全Object.defineProperty(fn, 'name', { __proto__: null, value }) 使用 null-proto 描述符,防止被污染的 Object.prototype.name 干扰。
text
adapters.getAdapter(adapters, config)
  +-- isResolvedHandle = utils.isFunction || === null || === false
  +-- forEach adapter:
  |     +-- if isResolvedHandle:  直接用
  |     +-- else:  knownAdapters[name.toLowerCase()]
  |                +-- if undefined: throw AxiosError("Unknown adapter " + id)
  +-- 全部失败: throw AxiosError("There is no suitable adapter...", ERR_NOT_SUPPORT)

模块三:Header 标准化 (lib/core/AxiosHeaders.js)

  • 模块名称AxiosHeaders 大小写不敏感 Header 容器
  • 设计说明:用 Symbol-keyed 内部状态($internals),键名归一化到小写,但保留原始大小写写入(构建时 formatHeader 可选归一)。accessor() 静态方法动态为常用 header 注入 getXxx/setXxx/hasXxx 链式方法。
  • 关键安全buildAccessors 使用 __proto__: null 描述符,防止被污染的 Object.prototype.get 转 accessor。
  • 可迭代协议Symbol.iterator + Symbol.toStringTag 让它看起来像原生容器;toJSON()/toString() 让它能直接喂给 XHR/Fetch/JSON。
text
+---------------------------+
|  AxiosHeaders             |
+---------------------------+
  - 内部: Object 实例,键归一化为小写
  - 静态 accessor: Content-Type/Content-Length/Accept/Accept-Encoding/User-Agent/Authorization
  - 构造: new AxiosHeaders(rawHeaders) -> set(rawHeaders)
  - 实例方法: set/get/has/delete/clear/normalize/concat/toJSON/toString/getSetCookie
  - 静态: from(thing) / concat(first, ...targets) / accessor(header)

模块四:配置合并 (lib/core/mergeConfig.js)

  • 模块名称mergeConfig 配置安全合并
  • 设计说明
    • 结果对象用 Object.create(null),并用 Object.defineProperty 还原 hasOwnProperty 为不可枚举的 own slot。
    • 显式 for 循环遍历合并键,遇到 __proto__/constructor/prototype 直接 return(防 Prototype Pollution)。
    • mergeMap 表驱动:每种 config 字段指定合并策略(valueFromConfig2 / defaultToConfig2 / mergeDirectKeys / mergeDeepProperties)。
    • transitional.validateStatusUndefinedResolves 配合 config2.validateStatus === undefined 的处理:a8e4f13 修复点。
  • 关键安全:每一步都用 utils.hasOwnProp 严格 own-prop 判定;headers 合并用 caseless: true 走大小写不敏感路径。
text
mergeConfig(config1, config2)
  +-- config = Object.create(null) + restore hasOwnProperty
  +-- mergeMap = { url/method/data: valueFromConfig2,
  |               baseURL/...: defaultToConfig2,
  |               validateStatus: mergeDirectKeys,
  |               headers: (a, b) => mergeDeepProperties(headersToObject(a), headersToObject(b), true) }
  +-- forEach prop in (Object.keys({...config1, ...config2})):
  |     +-- skip __proto__/constructor/prototype
  |     +-- a = ownProp(config1, prop), b = ownProp(config2, prop)
  |     +-- config[prop] = mergeMap[prop] || mergeDeepProperties
  +-- transitional.validateStatusUndefinedResolves 特殊处理
  +-- return config

模块五:取消语义 (lib/cancel/*)

  • 模块名称CancelToken + CanceledError + isCancel
  • 设计说明
    • CancelToken 基于 Promise + 监听者模式;executor 接收 cancel(message, config, request) 回调,调用时构造 CanceledError 并 resolve。
    • 支持链式:每个 then 都被包装成可独立 cancel 的派生 promise。
    • toAbortSignal() 把旧式 CancelTokenAbortSignal(迁移桥)。
    • AbortSignal 并存:dispatchRequest 同时检查 config.cancelTokenconfig.signal;adapter 内部在 done() 中清理 listener 防泄漏。
text
CancelToken
  +-- constructor(executor)  - executor 接收 cancel(msg, config, request)
  +-- throwIfRequested()     - 同步检测,已取消则抛 CanceledError
  +-- subscribe(listener)    - 注册监听
  +-- unsubscribe(listener)  - 反注册
  +-- toAbortSignal()        - 转 AbortSignal(迁移桥)
  +-- static source()        - 工厂方法,返回 { token, cancel }

CanceledError extends AxiosError
  +-- __CANCEL__ = true (作为 isCancel 判定依据)

isCancel(value)  - value?.__CANCEL__ === true

5. 关键数据流程

场景一:简单 JSON GET 请求

场景二:拦截器链(含 LIFO + 异步)

场景三:取消请求(双轨并存)

6. 接口与契约规范

6.1 核心内部模块契约 (TypeScript Interfaces)

typescript
/**
 * Axios 实例核心契约
 */
export interface IAxiosInstance {
  (config: AxiosRequestConfig): AxiosPromise;
  (url: string, config?: AxiosRequestConfig): AxiosPromise;
  defaults: AxiosRequestConfig;
  interceptors: {
    request: InterceptorManager<AxiosRequestConfig>;
    response: InterceptorManager<AxiosResponse>;
  };
  getUri(config?: AxiosRequestConfig): string;
  request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
  get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  delete<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  head<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  options<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  put<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  patch<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  postForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  putForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  patchForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  create(config?: AxiosRequestConfig): AxiosInstance;
}

/**
 * 请求配置契约
 */
export interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method;
  baseURL?: string;
  allowAbsoluteUrls?: boolean;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: (RawAxiosRequestHeaders & AxiosHeaders) | AxiosHeaders;
  params?: any;
  paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer;
  data?: D;
  timeout?: number;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapter | AxiosAdapterName | AxiosAdapterName[];
  responseType?: ResponseType;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  xsrfHeaderValue?: string | ((config: AxiosRequestConfig) => string | undefined);
  onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
  onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
  decompress?: boolean;
  maxContentLength?: number;
  maxBodyLength?: number;
  beforeRedirect?: (options: { headers: AxiosHeaders; statusCode: number }) => void;
  transport?: any;
  httpAgent?: any;
  httpsAgent?: any;
  cancelToken?: CancelToken;
  signal?: GenericAbortSignal;
  formDataHeaderPolicy?: 'legacy' | 'content-only';
  validateStatus?: ((status: number) => boolean) | null;
  env?: { FormData?: typeof FormData; Blob?: typeof Blob };
  maxRedirects?: number;
  maxRate?: number;
  socketPath?: string | null;
  allowedSocketPaths?: string[];
  responseEncoding?: string;
  formSerializer?: FormSerializerOptions;
  parseReviver?: (key: string, value: any, context: { source: string }) => any;
  transitional?: TransitionalOptions;
  redact?: string[];
  [extra: string]: any;
}

/**
 * 响应契约
 */
export interface AxiosResponse<T = any, D = any> {
  data: T;
  status: number;
  statusText?: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}

/**
 * 拦截器管理契约
 */
export interface InterceptorManager<V> {
  use(fulfilled?: (value: V) => V | Promise<V>, rejected?: (error: any) => any, options?: { synchronous?: boolean; runWhen?: (config: AxiosRequestConfig) => boolean }): number;
  eject(id: number): void;
  clear(): void;
  forEach(fn: (interceptor: { fulfilled: (value: V) => V | Promise<V>; rejected: (error: any) => any; synchronous: boolean; runWhen: ((config: AxiosRequestConfig) => boolean) | null }) => void): void;
}

/**
 * 适配器契约
 */
export type AxiosAdapter = (config: AxiosRequestConfig) => AxiosPromise;

/**
 * 错误契约
 */
export class AxiosError<T = unknown, D = any> extends Error {
  constructor(message?: string, code?: string, config?: AxiosRequestConfig<D>, request?: any, response?: AxiosResponse<T, D>);
  static from<T = unknown, D = any>(error: Error | string | object, code?: string, config?: AxiosRequestConfig<D>, request?: any, response?: AxiosResponse<T, D>, customProps?: object): AxiosError<T, D>;
  toJSON(): object;
  cause?: Error;
  config?: AxiosRequestConfig<D>;
  code?: string;
  request?: any;
  response?: AxiosResponse<T, D>;
  status?: number;
  isAxiosError: boolean;
  name: string;
  // 错误码常量
  static ERR_BAD_OPTION_VALUE: string;
  static ERR_BAD_OPTION: string;
  static ECONNABORTED: string;
  static ETIMEDOUT: string;
  static ECONNREFUSED: string;
  static ERR_NETWORK: string;
  static ERR_FR_TOO_MANY_REDIRECTS: string;
  static ERR_DEPRECATED: string;
  static ERR_BAD_RESPONSE: string;
  static ERR_BAD_REQUEST: string;
  static ERR_CANCELED: string;
  static ERR_NOT_SUPPORT: string;
  static ERR_INVALID_URL: string;
  static ERR_FORM_DATA_DEPTH_EXCEEDED: string;
}

6.2 对外 API 契约(节选 — OpenSpec 形式)

axios 本质是客户端库,没有对外 HTTP 端点。下表是核心公开 API 入口的契约清单:

yaml
# === Axios 默认实例 ===
- name: axios
  type: function
  signature: "axios(configOrUrl, config?) => Promise<AxiosResponse>"
  description: |
    核心调用入口。支持 axios(config) 与 axios(url, config) 两种形式
    (后者模拟 fetch API 风格)。
  examples:
    - "axios({ method: 'post', url: '/user', data: { name: 'fred' } })"
    - "axios('/user/12345')"

- name: axios.get|delete|head|options
  type: function
  signature: "(url, config?) => Promise<AxiosResponse>"
  description: 不带请求体的方法别名。

- name: axios.post|put|patch|query
  type: function
  signature: "(url, data?, config?) => Promise<AxiosResponse>"
  description: 带请求体的方法别名。
  notes: post/put/patch 还有 *Form 变体(multipart/form-data)。

- name: axios.create
  type: function
  signature: "(config?) => AxiosInstance"
  description: 派生新实例,defaults = mergeConfig(parent.defaults, config)。

- name: axios.interceptors
  type: object
  properties:
    request: InterceptorManager<AxiosRequestConfig>
    response: InterceptorManager<AxiosResponse>

# === 静态挂载 ===
- name: Axios
  type: class
  description: 暴露 Axios 类以支持继承(extends Axios)。

- name: AxiosError
  type: class
  description: 标准错误类 + 14 个错误码常量。
  factory: AxiosError.from(error, code?, config?, request?, response?, customProps?)

- name: CanceledError
  type: class
  description: extends AxiosError,标记 isCancel 判定。

- name: CancelToken
  type: class
  status: deprecated
  description: |
    旧式取消令牌。仍受支持(向后兼容)但新代码应使用 AbortController.signal。
  factory: CancelToken.source() => { token, cancel }
  methods: [subscribe, unsubscribe, throwIfRequested, toAbortSignal]

- name: isCancel
  type: function
  signature: "(value: any) => boolean"
  description: 判定一个错误是否是 CanceledError(err.__CANCEL__ === true)。

- name: AxiosHeaders
  type: class
  description: 大小写不敏感的 Header 容器。
  static_methods: [from(thing), concat(first, ...targets), accessor(header)]

- name: mergeConfig
  type: function
  signature: "(config1: AxiosRequestConfig, config2: AxiosRequestConfig) => AxiosRequestConfig"
  description: |
    安全合并两个 config。结果用 Object.create(null) 防原型污染,
    显式过滤 __proto__/constructor/prototype。

- name: getAdapter
  type: function
  signature: "(adapters: AdapterName|AdapterFunction|[...], config: AxiosRequestConfig) => AdapterFunction"
  description: 能力匹配第一个可用的适配器。

- name: HttpStatusCode
  type: enum
  description: 标准 HTTP 状态码枚举(Ok, MultipleChoices, ...)。

- name: toFormData
  type: function
  signature: "(obj: object, formData?: FormData, formSerializer?: FormSerializerOptions) => FormData"
  description: 把 JS 对象转换为 FormData 实例。

- name: formToJSON
  type: function
  signature: "(form: FormData|HTMLFormElement) => object"
  description: FormData → 普通对象(支持嵌套 formFields)。

- name: isAxiosError
  type: function
  signature: "(payload: any) => boolean"
  description: 判定对象是否是 AxiosError。

- name: all
  type: function
  status: deprecated
  description: 等价 Promise.all。

- name: spread
  type: function
  status: deprecated
  description: 回调风格参数展开。

- name: VERSION
  type: string
  description: 由 gulp version 任务注入到 lib/env/data.js。

7. 快速开始

7.1 环境配置

  • Node.js 18+(推荐 LTS)
  • npm 8+(package-lock.json 锁定)
  • 浏览器自动化(可选):npx playwright install --with-deps

7.2 安装与运行

bash
# 克隆仓库
git clone https://github.com/axios/axios.git
cd axios

# 安装依赖(使用 npm ci 走 lockfile)
npm ci

# 启用 husky git hooks(首次安装后)
npm rebuild husky && npx husky

# 单元测试
npm run test:vitest:unit

# 浏览器测试
npx playwright install
npm run test:vitest:browser:headless

# 构建产物
npm run build  # 产出 dist/

7.3 典型用例

Case 1: 浏览器 SPA 统一 API 客户端

javascript
import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  validateStatus: (s) => s >= 200 && s < 300 || s === 304,
});

api.interceptors.request.use((config) => {
    // 每次请求前添加 token
    config.headers.set('Authorization', `Bearer ${getToken()}`);
    return config;
});

api.interceptors.response.use(
  (r) => r.data,
  (err) => {
    if (err.response?.status === 401) logout();
    return Promise.reject(err);
  }
);

await api.get('/user/me');

Case 2: Node 端上传 + 取消

javascript
import axios from 'axios';

const ctrl = new AbortController();
axios.post('https://api.example.com/upload', formData, {
  signal: ctrl.signal,
  maxBodyLength: 100 * 1024 * 1024,
  onUploadProgress: (e) => console.log(e.loaded / e.total),
});
setTimeout(() => ctrl.abort(), 5000);

Case 3: 自定义 Adapter

javascript
import axios from 'axios';

const instance = axios.create({
  adapter: (config) => {
    // 自定义 I/O:返回 Promise<AxiosResponse>
    return new Promise((resolve, reject) => {
      // ... 你的实现
    });
  },
});

Case 4: 解构覆盖默认 Adapter

javascript
import axios from 'axios';

// 强制使用 fetch 适配器
const instance = axios.create({ adapter: 'fetch' });

// 或按顺序尝试
const instance2 = axios.create({ adapter: ['xhr', 'fetch'] });