首页
/ Axios 文件上传实战:postForm、FileList 与 Node.js 流式传输的源码级解析

Axios 文件上传实战:postForm、FileList 与 Node.js 流式传输的源码级解析

2026-09-06 12:14:35作者:伍霜盼Ellen

Axios 通过 postForm 系列快捷方法、File/FileList 自动识别以及 Node.js 下的 form-data 流式封装,让 multipart/form-data 文件上传在浏览器与 Node.js 两个环境都可以保持简洁写法。本文以 Axios 仓库的官方文件上传文档为主线,结合 lib/core/Axios.jslib/defaults/index.jslib/helpers/toFormData.jslib/adapters/http.js 的源码实现,完整覆盖单文件、多文件、进度回调、Node 流式上传、Buffer 上传的写法,并给出 maxRedirectsformDataHeaderPolicy 等关键参数的底层依据与注意事项。

一、两种上传入口:postForm 与 FormData

Axios 文件上传文档(docs/fr/pages/advanced/file-posting.md)开篇即给出结论:当需要 multipart/form-data 上传时,使用 postForm 或手工构造 FormData。这两条路径在源码中的差异非常清晰。

post/put/patch 及对应的 Form 变体方法在 lib/core/Axios.js 中由同一个工厂函数 generateHTTPMethod(isForm) 生成:

// lib/core/Axios.js(节选)
function generateHTTPMethod(isForm) {
  return function httpMethod(url, data, config) {
    return this.request(
      mergeConfig(config || {}, {
        method,
        headers: isForm
          ? { 'Content-Type': 'multipart/form-data' }
          : {},
        url,
        data,
      })
    );
  };
}

Axios.prototype[method] = generateHTTPMethod();
if (method !== 'query') {
  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
}

从源码结构看,postForm 与普通 post唯一区别是:postForm 会预先写入 Content-Type: multipart/form-data 请求头(且该头不带 boundary),之后交给默认的 transformRequest 决定是否把对象转换为 FormData

这一转换逻辑位于 lib/defaults/index.js

// lib/defaults/index.js(节选,transformRequest 默认实现)
if (isObjectPayload) {
  const formSerializer = own(this, 'formSerializer');
  if (contentType.indexOf('application/x-www-form-urlencoded') > -1) {
    return toURLEncodedForm(data, formSerializer).toString();
  }

  if (
    (isFileList = utils.isFileList(data)) ||
    contentType.indexOf('multipart/form-data') > -1
  ) {
    const env = own(this, 'env');
    const _FormData = env && env.FormData;

    return toFormData(
      isFileList ? { 'files[]': data } : data,
      _FormData && new _FormData(),
      formSerializer
    );
  }
}

这里有两处直接对应文档行为的关键分支:

  1. Content-Type 已含 multipart/form-data(即 postForm 或手动设置该头的 post)→ 调用 lib/helpers/toFormData.js 把普通对象转成 FormData
  2. data 本身就是一个 FileList → 先包一层 { 'files[]': data } 再转换,这正是文档中“多文件默认字段名”的来源。

此外,若 data 已经是 FormData 实例,transformRequest 会原样放行(除非 Content-Type 是 JSON,则经 formDataToJSON 序列化),所以手工 FormData 路径不经过任何转换。

仓库中的可运行示例 examples/postMultipartFormData/index.html 同时演示了两种入口:既可以 new FormData() 后 append 文件再 POST,也可以传普通对象并让 Axios 自动转换,配合 examples/postMultipartFormData/server.js 可本地验证请求体内容。

二、浏览器:上传单个文件

文档“Single file (browser)”一节的写法是把 File 对象直接作为字段值传入 postForm

await axios.postForm("https://httpbin.org/post", {
  description: "My profile photo",
  file: document.querySelector("#fileInput").files[0],
});

Axios 会检测该字段并自动使用正确的内容类型。其原理链条是:

  • postForm 设置 Content-Type: multipart/form-data(见上一节);
  • 默认 transformRequest 发现 Content-Type 匹配后调用 toFormData(data)
  • lib/helpers/toFormData.jsconvertValue 负责值级转换:null 变空字符串、Date 转 ISO 字符串、布尔转字符串;在 Node 环境中 ArrayBuffer/TypedArray 会被转成 Blob(若目标 FormData 是规范兼容实现)或 Buffer(平台支持 Buffer 时,见 lib/platform/node/classes/Buffer.js),否则抛出 AxiosError('Blob is not supported. Use a Buffer instead.')

浏览器中的 File 是规范兼容的 Blob,会被原样 append 到 FormData,浏览器在发送时自动为其生成 filenameContent-Type(取 File.type)。这也是为什么文档强调“不需要手动设置文件字段的 Content-Type”——真正需要 boundary 的 multipart 头由浏览器(或 Node 适配器,见下文)在发送阶段补齐。

三、浏览器:上传多个文件

文档“Multiple files (browser)”给出三种多文件策略,均可由 lib/helpers/toFormData.jsdefaultVisitor 解释。

1. 直接传 FileList(默认字段名 files[])

await axios.postForm(
  "https://httpbin.org/post",
  document.querySelector("#fileInput").files
);

FileListtransformRequest 中被识别后包装为 { 'files[]': data }lib/defaults/index.js),随后 defaultVisitor[] 结尾的键执行 removeBrackets,把每个元素 append 为 files[]。结果:所有文件共享同一字段名 files[],服务端按同名数组接收。

[] 后缀的识别逻辑在 lib/helpers/toFormData.js

function removeBrackets(key) {
  return utils.endsWith(key, '[]') ? key.slice(0, -2) : key;
}

以及 lib/helpers/toFormData.js 的数组分支(indexes 选项控制字段名形态):

// 默认 indexes = false:key + '[]';indexes = true:key[index];indexes = null:直接用 key
indexes === true
  ? renderKey([key], index, dots)
  : indexes === null
    ? key
    : key + '[]'

2. 自定义字段名(键名加 [])

await axios.postForm("https://httpbin.org/post", {
  "files[]": document.querySelector("#fileInput").files,
});

即把 FileList(或 File 对象数组)显式挂在自定义键下,只要键以 [] 结尾就会触发逐个 append;不写 [] 的话数组整体不是“可访问对象”,会被 convertValue 处理,行为与文档承诺的“custom field name”一致的前提就是加上 [] 后缀。

3. 每个文件使用不同字段名

当服务端要求每个文件对应独立字段时,文档建议手工构造 FormData 并使用普通 post

const formData = new FormData();
formData.append("avatar", avatarFile);
formData.append("cover", coverFile);

await axios.post("https://httpbin.org/post", formData);

由于 data 已是 FormDatatransformRequest 直接放行,字段名完全由你控制。仓库测试 tests/unit/toFormData.test.jstoFormData 的键渲染(dotsindexesmetaTokens 选项)有成体系的用例,可作行为参照。

四、浏览器:跟踪上传进度

文档“Tracking upload progress (browser)”一节:

await axios.postForm("https://httpbin.org/post", {
  file: document.querySelector("#fileInput").files[0],
}, {
  onUploadProgress: (progressEvent) => {
    const percent = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    );
    console.log(`Upload progress: ${percent}%`);
  },
});

浏览器侧的实现位于 XHR 适配器 lib/adapters/xhr.js

if (onUploadProgress && request.upload) {
  [uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress);
}

request.upload 绑定的是 XMLHttpRequest 的 Upload 对象,progress 事件经 lib/helpers/progressEventReducer.js 节流/防抖后回调用户函数。完整的事件字段列表(loadedtotalpercentlengthComputable 等)见文档 docs/pages/advanced/progress-capturing.md

五、Node.js:用文件流上传

文档“Files in Node.js”一节的推荐做法是用 fs.createReadStream 避免整文件驻留内存:

import fs from "fs";
import FormData from "form-data";
import axios from "axios";

const form = new FormData();
form.append("file", fs.createReadStream("/path/to/file.jpg"));
form.append("description", "My uploaded file");

await axios.post("https://httpbin.org/post", form);

文档提示(tip):Node.js 环境中创建 FormData 需要 npm 包 form-data;Node.js v18+ 已原生提供全局 FormData。仓库源码印证了这两条路径在 HTTP 适配器中分别处理,见 lib/adapters/http.js

// support for spec compliant FormData objects
if (utils.isSpecCompliantForm(data)) {
  const userBoundary = headers.getContentType(/boundary=([-_\w\d]{10,70})/i);

  data = formDataToStream(
    data,
    (formHeaders) => {
      headers.set(formHeaders);
    },
    {
      tag: `axios-${VERSION}-boundary`,
      boundary: (userBoundary && userBoundary[1]) || undefined,
    }
  );
  // support for https://www.npmjs.com/package/form-data api
} else if (
  utils.isFormData(data) &&
  utils.isFunction(data.getHeaders) &&
  data.getHeaders !== Object.prototype.getHeaders
) {
  setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy'));
  // ...若未带 Content-Length,尝试 data.getLength() 补齐
}

两条路径的要点:

  • 规范兼容 FormData(Node 18+ 全局实现):由 lib/helpers/formDataToStream.jsFormData 转成可流式消费的 Readable Stream,boundary 默认自动生成,也可从用户提供的 Content-Type 头中解析复用;
  • npm form-data:Axios 通过 data.getHeaders() 获取该包生成的头(含带 boundary 的 Content-Type),并经 lib/core/setFormDataHeaders.js 合并到请求头;若请求未声明 Content-Length,还会异步调用 data.getLength() 尝试补齐。

form-data 包的引入位置即 lib/platform/node/classes/FormData.js,它只是 import FormData from 'form-data' 的一行再导出,被 toFormData 在 Node 平台下用作默认 FormData 构造器(见 lib/helpers/toFormData.jsPlatformFormData 导入与 new (PlatformFormData || FormData)())。

六、Node.js:直接上传 Buffer

文档“Uploading a Buffer (Node.js)”一节展示了内存 Buffer 的上传方式:

const buffer = Buffer.from("Hello, world!");

const form = new FormData();
form.append("file", buffer, {
  filename: "hello.txt",
  contentType: "text/plain",
  knownLength: buffer.length,
});

await axios.post("https://httpbin.org/post", form);

第三个参数是 npm form-data 包的 append 选项:filename 决定 multipart 中的文件名,contentType 决定该 part 的 MIME 类型,knownLength 用于让 form-data 能计算精确的 Content-Length。在 Node 侧,toFormDataconvertValue 也会把 ArrayBuffer/TypedArray 转成 Buffer(当目标是 form-data 这类非规范实现时,见 lib/helpers/toFormData.js),因此对 putForm/postForm 直接传包含 Buffer 字段的对象同样是可行的。

七、两条关键注意事项(含源码依据)

文档以 warning 与 danger 形式给出两条 Node.js 限制,均有源码支撑:

1. Node.js 下 FormData 上传暂不支持进度捕获

Node 的 HTTP 适配器虽然读取了 onUploadProgresslib/adapters/http.js 解构出该配置),但对 FormData 流式数据(spec-compliant 转 formDataToStream,或 form-data 包流)并未把上传字节数回传给回调;文档明确“Capturing FormData upload progress is not currently supported in Node.js environments”,实际项目中应只在浏览器侧依赖 onUploadProgress

2. 上传可读流时建议设置 maxRedirects: 0

文档原文(danger 块):When uploading a readable stream in Node.js, set maxRedirects: 0 to prevent the follow-redirects package from buffering the entire stream in RAM.

源码依据在 lib/adapters/http.js 的传输层选择逻辑:

} else if (maxRedirects === 0) {
  transport = isHttpsRequest ? https : http;
  isNativeTransport = true;
} else {
  // 默认走 follow-redirects 封装的 transport
  if (maxRedirects) {
    options.maxRedirects = maxRedirects;
  }
  ...
}

maxRedirects: 0 时 Axios 直接使用原生 http/https 模块,流被原样 pipe 出去;否则 follow-redirects 为了支持重定向回放,会把整个请求体缓冲到内存/临时状态中,大文件场景下这是显性的内存风险。代价是请求不再自动跟随重定向,若接口有 3xx 跳转需自行处理。

补充:formDataHeaderPolicy

合并 npm form-data 头部时还可通过 formDataHeaderPolicy 控制策略,实现在 lib/core/setFormDataHeaders.js:取值 'content-only' 时仅复制 content-type/content-length 两个头,其余策略下整包合并 data.getHeaders() 的结果。默认策略为合并全部头,一般无需配置。

八、toFormData 的高级行为速览

postForm 的对象转换由 lib/helpers/toFormData.js 完成,除上文提及的 indexes/[] 规则外,还有几个默认行为值得知道:

  • {} 元标记:键以 {} 结尾且值为对象时,值会被 JSON.stringify 成字符串字段(metaTokens 默认 true 时保留 {} 后缀);
  • 嵌套深度限制DEFAULT_FORM_DATA_MAX_DEPTH = 100lib/helpers/toFormData.js),超过会抛出 ERR_FORM_DATA_DEPTH_EXCEEDED;对象中存在循环引用会直接抛 Circular reference detected 错误;
  • formSerializer 配置项:可通过 formSerializer 传入 visitor/dots/metaTokens/indexes 等选项定制序列化(见 lib/defaults/index.jsown(this, 'formSerializer') 的传递);
  • React Native 特判isReactNative(formData) && isReactNativeBlob(value) 时直接 append 原始 Blob(lib/helpers/toFormData.js)。

九、适用前提与小结

  • 适用前提:以上结论基于当前仓库的 lib/ 源码与 docs/pages/advanced/file-posting.mddocs/fr/pages/advanced/file-posting.md 文档;Node 侧示例要求能安装 npm 包 form-data,或运行在 Node v18+(原生全局 FormData)环境;
  • 行为速查:postForm = post + 预置 multipart 头;FileList 裸传 → 字段名 files[];键名带 [] → 同名多值;手工 FormData → 字段名完全自定义;
  • 进阶控制:onUploadProgress 仅限浏览器 XHR 路径;Node 流式上传务必考虑 maxRedirects: 0formDataHeaderPolicyformSerializer 分别控制表单头合并与字段序列化细节。

参考实现与测试入口:examples/postMultipartFormData/index.htmlexamples/postMultipartFormData/server.jstests/unit/toFormData.test.jstests/unit/axiosHeaders.test.jslib/adapters/xhr.jslib/adapters/http.js

登录后查看全文
热门项目推荐
相关项目推荐