首页
/ axios 文件上传实战指南:postForm 与 FormData 的 multipart/form-data 上传机制

axios 文件上传实战指南:postForm 与 FormData 的 multipart/form-data 上传机制

2026-09-05 10:11:23作者:平淮齐Percy

axios 让文件上传变得很直接:当你需要以 multipart/form-data 格式上传文件时,使用 postForm 或手动构造 FormData 即可。本篇基于 axios 官方文档中的“File posting(文件上传)”指南,结合当前仓库源码,系统讲解浏览器与 Node.js 两种环境下上传单个文件、多个文件、Buffer 与流式读取文件的完整做法,并深入剖析 postForm 方法如何自动生成 Content-TypetransformRequest 如何把 FileList 包装成 files[] 字段、以及 toFormData 转换器的底层规则。

两种上传方式:postForm 与 FormData

axios 提供了两条上传路径:

  1. postForm(以及 putFormpatchForm:直接把普通 JS 对象作为请求体传入,axios 自动将其序列化为 multipart/form-data,并自动设置正确的请求头;
  2. 手动构造 FormData:当需要精细控制字段名、顺序或附件元数据(如 filenamecontentType)时,直接使用浏览器或 form-data 包构造 FormData 对象,再传给 axios.post

从源码结构看,postForm 等便捷方法是动态生成的。lib/core/Axios.js 中对 postputpatch 三个带数据的方法统一遍历:

utils.forEach(['post', 'put', 'patch', 'query'], function forEachMethodWithData(method) {
  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);
  }
});

也就是说 Axios.prototype.postForm = generateHTTPMethod(true)——它和普通 post 的唯一区别,就是预先在请求头中写入了 Content-Type: multipart/form-data。这个 Content-Type 正是后续 transformRequest 触发表单序列化的关键开关。TypeScript 类型定义中同样声明了该方法,见 index.d.ts

上传单个文件(浏览器)

File 对象直接作为字段值传入,axios 会识别它并自动使用正确的内容类型:

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

这条链路在源码中可以被完整印证:默认的 transformRequest(位于 lib/defaults/index.js)按以下顺序处理请求体:

  • 若数据是 HTMLFormElement,先转为 FormData
  • 若数据已是 FormData,原样返回(除非 Content-Type 是 JSON);
  • 若数据是 ArrayBufferBufferStreamFileBlobReadableStream 之一,直接透传;
  • 若 Content-Type 包含 multipart/form-data(正是 postForm 设置的),调用 toFormData 完成序列化。

File 的检测来自 lib/utils.js 中的 isFile = kindOfTest('File');而整个对象到 FormData 的递归转换则交给 lib/helpers/toFormData.js 中的 toFormData 函数完成——它按 key 逐个遍历对象,遇到嵌套的可访问对象(普通对象/数组)就继续递归,遇到标量或文件就直接 formData.append(...)

上传多个文件(浏览器)

方式一:直接传 FileList。把 <input type="file" multiple> 得到的 FileList 整体作为请求体传入,所有文件会被发送在同一个字段名下(files[]):

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

之所以固定是 files[],可以直接在默认转换逻辑里找到依据(lib/defaults/index.js):

if (
  (isFileList = utils.isFileList(data)) ||
  contentType.indexOf('multipart/form-data') > -1
) {
  return toFormData(
    isFileList ? { 'files[]': data } : data,
    _FormData && new _FormData(),
    formSerializer
  );
}

当整个请求体就是一个 FileList 时,axios 会先把它包装成 { 'files[]': data } 再交给 toFormData

方式二:自定义字段名,键名加 [] 后缀toFormData 的默认访问器(lib/helpers/toFormData.js)对「值为 FileList」或「键名以 [] 结尾」这两种情况都会走批量追加分支:

} else if (
  (utils.isArray(value) && isFlatArray(value)) ||
  ((utils.isFileList(value) || utils.endsWith(key, '[]')) && (arr = utils.toArray(value)))
) {
  // eslint-disable-next-line no-param-reassign
  key = removeBrackets(key);

  arr.forEach(function each(el, index) {
    !(utils.isUndefined(el) || el === null) &&
      formData.append(
        // eslint-disable-next-line no-nested-ternary
        indexes === true
          ? renderKey([key], index, dots)
          : indexes === null
            ? key
            : key + '[]',
        convertValue(el)
      );
  });
  return false;
}
await axios.postForm("https://httpbin.org/post", {
  "files[]": document.querySelector("#fileInput").files,
});

注意 removeBrackets 会先剥掉键名末尾的 [],追加时再按 indexes 选项决定字段名的最终形态:

  • indexes: true:每个元素带数字下标,如 files[0]files[1]
  • indexes: null:所有元素共用同一字段名 files
  • 默认(indexes: false):每个元素都追加为 files[]

方式三:每个文件使用不同字段名。手动构造 FormData 即可:

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

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

捕获上传进度(浏览器)

使用 onUploadProgress 回调向用户展示进度条或百分比:

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}%`);
  },
});

进度事件上还有更多可用字段(如传输速率、rate 等),完整字段清单可参考仓库中的 进度捕获文档

在 Node.js 中上传文件

在 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);

提示:在 Node.js 环境中需要安装 npm 包 form-data 才能创建 FormData 对象。在较新的 Node.js(v18+)中,全局 FormData 已原生可用。

上传 Buffer(Node.js)

内存中的 Buffer 也可以直接上传,并通过 append 的第三个参数指定文件名、内容类型与已知长度:

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);

其中 knownLength 尤其重要:它让 axios 能够提前计算 Content-Length,避免回退到 chunked 传输编码。

注意一:在 Node.js 环境中,对 FormData 的上传进度捕获目前不受支持。

注意二(关键):在 Node.js 中上传可读流时,建议设置 maxRedirects: 0,以防止 follow-redirects 包在跟随重定向前将整个流缓冲进内存(流一旦读完就无法重放,重定向会导致请求失败或内存膨胀)。

toFormData 转换器的更多细节

除了上述上传场景,toFormData 还通过 axios.toFormData 公开导出(见 lib/axios.js),可供业务侧手动调用。理解它的几个关键选项和约束,有助于排查序列化问题:

  • maxDepth(默认 100):嵌套深度上限,由 lib/helpers/toFormData.js 中的 DEFAULT_FORM_DATA_MAX_DEPTH = 100 定义;超过限制会抛出 ERR_FORM_DATA_DEPTH_EXCEEDED 错误。递归构建过程中还会通过 stack 数组检测循环引用,发现时抛出 Circular reference detected 错误;
  • metaTokens(默认 true):控制 key{} 这类“元 token”是否保留在最终字段名中,{} 后缀表示该嵌套对象会被 JSON.stringify 序列化成一个字段值;
  • dots / indexes:分别控制嵌套键用点号(a.b)还是方括号(a[b])渲染,以及数组元素是否带数字下标;
  • 类型转换convertValue 会把 null 转为空串、Date 转为 ISO 字符串、布尔转为字符串;在 Node 环境中若目标 FormData 不支持 Blob,则 ArrayBuffer/TypedArray 会被转换为 Buffer,而对 Blob 会抛出“请改用 Buffer”的错误;
  • 该转换器同时服务于 AxiosURLSearchParams(见 lib/helpers/AxiosURLSearchParams.js)和 x-www-form-urlencoded 序列化(lib/helpers/toURLEncodedForm.js),因此上述规则也影响 URL 编码表单的生成。

相关行为在仓库测试中亦有覆盖:toFormData 的转换逻辑见 tests/unit/toFormData.test.js,浏览器环境下的 FormData 处理见 tests/browser/formdata.browser.test.js,可对照阅读验证本文结论。

小结

  • 浏览器端优先用 postForm:单文件直接放 File 对象,多文件直接传 FileList(自动落到 files[])或用 "files[]" 键自定义字段名;进度跟踪用 onUploadProgress
  • Node.js 端用 form-data 包(或 Node 18+ 全局 FormData)配合 fs.createReadStream 做流式上传,Buffer 上传时记得提供 knownLength
  • 流式上传在 Node 中建议 maxRedirects: 0
  • 序列化规则的核心实现在 lib/defaults/index.jstransformRequestlib/helpers/toFormData.jstoFormData,遇到字段名或嵌套结构不符合预期时,应优先检查这两个文件中的分支逻辑。
登录后查看全文
热门项目推荐
相关项目推荐