首页
/ axios 文件上传实战指南:postForm、FormData 与 multipart/form-data 的完整用法

axios 文件上传实战指南:postForm、FormData 与 multipart/form-data 的完整用法

2026-09-06 20:20:04作者:姚月梅Lane

axios 把文件上传(multipart/form-data)封装成了几乎零成本的操作:浏览器端可以直接传 File/FileList 对象,Node.js 端可以传流或 Buffer。读完本文,你将掌握 postForm/putForm 快捷方法、多文件字段命名(files[] 约定)、上传进度监听(onUploadProgress)、Node.js 下流式上传与内存 Buffer 上传的完整写法,并理解这些能力在 axios 源码中的真实实现路径。

postForm:multipart/form-data 请求的“快捷方式”

需要上传文件时,官方推荐直接使用 postForm(或 putForm 等)而不是 post,因为表单请求方法会自动设置 Content-Type: multipart/form-data

这一行为可以在源码中确认:axios 在生成请求方法别名时,对 postputpatch 等带数据的动词各生成一个 xxxForm 变体,并在请求配置中强制注入 Content-Type: multipart/form-data 头(query 因其幂等读语义被刻意排除):

// lib/core/Axios.js(节选)
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);
  }
});

有了这个 Content-Type,默认的 transformRequest 就会把普通 JS 对象自动转换为 FormData(详见下文“默认 transformRequest 的分流逻辑”),所以你无需手动 new FormData(),也无需自己写 boundary。

浏览器端:单文件上传

最简写法是把 File 对象直接作为字段值传给 postForm——axios 会识别它并自动使用正确的 content type:

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

这里的识别与转换发生在 lib/defaults/index.js 的默认 transformRequest 中:当请求体是普通对象,且 Content-Type 包含 multipart/form-datapostForm 已自动设置)时,axios 调用 toFormData 把整个对象转成 FormData

// lib/defaults/index.js(节选)
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,   // 注意这里对 FileList 的包装
    _FormData && new _FormData(),
    formSerializer
  );
}

可以看到两个要点:一是 File/Blob 等值会被原样保留并交给 FormData.append,浏览器随后自动填充其 Content-Type;二是如果你直接对 axios.postForm(url, fileInput.files) 传一个裸 FileList,axios 会替你把它包装成 { 'files[]': filelist }——这正是下面“多文件”一节的字段名由来。

浏览器端:多文件上传

同一个字段名下批量上传

直接把 FileList 作为请求体传入,所有文件都会以同一字段名(files[])发送:

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

自定义字段名:在 key 上追加 []

也可以把 FileList(或 File 对象数组)显式放在自定义字段名下,约定是在 key 后面追加 []

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

这个 [] 约定不是魔法字符串,而是由 toFormData 的默认 visitor 实现的。在 lib/helpers/toFormData.js#L208-L228 中,当值是一个“扁平数组”(元素均不可继续展开)、或值是 FileList、或 key 以 [] 结尾时,axios 会把 [] 剥掉(removeBrackets),再对每个元素逐一 append,元素自身的 key 统一渲染为 key + '[]'

// lib/helpers/toFormData.js(节选)
} else if (
  (utils.isArray(value) && isFlatArray(value)) ||
  ((utils.isFileList(value) || utils.endsWith(key, '[]')) && (arr = utils.toArray(value)))
) {
  key = removeBrackets(key);
  arr.forEach(function each(el, index) {
    !(utils.isUndefined(el) || el === null) &&
      formData.append(
        indexes === true
          ? renderKey([key], index, dots)
          : indexes === null ? key : key + '[]',
        convertValue(el)
      );
  });
  return false;
}

这意味着:

  • 数组中为 undefined/null 的元素会被静默跳过;
  • toFormData 还接受 indexesdotsvisitor 等序列化选项(通过请求配置 formSerializer 透传,见 lib/defaults/index.js#L80-L96),可以为元素生成带下标(files[0])或带点号(files.0)的键名;
  • toFormData 对嵌套深度设了默认上限(DEFAULT_FORM_DATA_MAX_DEPTH = 100,见 lib/helpers/toFormData.js#L9-L11),防止异常深的对象结构造成失控。

每个文件使用不同字段名

当服务端要求每个文件对应独立的字段名时,手动构造 FormData 再交给 axios.post 即可(传入 FormDatatransformRequest 会原样放行,不再做 JSON 序列化):

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

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

放行逻辑同样在默认 transformRequest 里:检测到 utils.isFormData(data) 后,除非你显式指定了 JSON Content-Type(此时会被 formDataToJSON 转成 JSON),否则直接 return data 交给适配器。

监听上传进度(浏览器)

上传大文件时,用 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}%`);
  },
});

进度事件对象的完整字段清单参见 Progress capturing

从源码看,三个适配器都实现了上传进度:

Node.js:用文件流上传

在 Node.js 中上传本地文件,推荐用 fs.createReadStream 把文件包装成流加入 FormData,从而避免把整个文件一次性读进内存:

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 环境下需要 form-data 这个 npm 包来创建 FormData 对象;现代 Node.js(v18+)也原生提供全局 FormData。axios 的 Node 平台适配实际上就是把平台 FormData 绑定为 form-data 包:见 lib/platform/node/classes/FormData.js

当请求体是 Node 风格的 form-data 实例(它带有 getHeaders() 等方法,但不是标准 Web FormData)时,http 适配器会检测到 FormData 数据并做相应处理(lib/adapters/http.js#L795)。而 fetch 适配器对标准 Web FormData(v18+ 全局对象)则直接交给底层 fetch 处理,并在 Content-Type 缺少 boundary 时补全(lib/adapters/fetch.js#L423-L425)。

内存 Buffer 上传

如果文件内容已经在内存中,也可以直接把 Buffer append 进 FormData,并手动提供文件名、MIME 类型与已知长度:

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

警告:在 Node.js 环境中,FormData 上传进度(onUploadProgress)目前对 FormData 请求体本身不支持捕获(http 适配器依赖可读取的数据流来累计字节数,form-data 包的封装方式与之不同)。

危险:在 Node.js 上传可读流时,请设置 maxRedirects: 0,防止 follow-redirects 包为支持重放重定向而把整个流缓冲进 RAM,大文件场景下会显著放大内存占用。

底层原理速览:multipart 请求体如何生成

理解了上面的行为后,可以再补一块拼图:boundary 与 Content-Type 到底由谁生成。

  • 浏览器:把 FormData 交给 XMLHttpRequest/fetch 后,boundary 由浏览器/引擎自动生成,应用层无需关心;
  • Node.js + 标准 Web FormData:如果走 fetch 适配器,同样由底层处理;axios 会在必要时补上缺少的 boundary(lib/adapters/fetch.js#L423-L425);
  • axios 自身的 stream 化路径lib/helpers/formDataToStream.js 展示了 axios 生成 multipart 编码的完整实现——用 form.entries() 遍历每个 part,为文件 part 生成 Content-Disposition: form-data; name="..."; filename="..."Content-Type 头(缺省为 application/octet-stream),随机生成 25 位 alphanumeric+-_ 的 boundary,并预计算整个请求的 Content-Lengthlib/helpers/formDataToStream.js#L68-L117)。这段代码也解释了为何 Buffer 上传示例里 knownLength 很重要:只有各 part 大小已知时,axios 才能算出准确的 Content-Length,否则只能使用分块传输(chunked)。

默认 transformRequest 的完整分流逻辑

把文档中的各个场景串起来,lib/defaults/index.js#L43-L107 的默认 transformRequest 按如下优先级处理请求体:

  1. 请求体是 HTML <form> 元素 → 自动转为 FormData
  2. 请求体是 FormData → 原样返回(除非 Content-Type 为 JSON,则 formDataToJSON 序列化);
  3. ArrayBuffer/Buffer/Stream/File/Blob/ReadableStream → 原样返回;
  4. URLSearchParams → 序列化为 querystring 并设置 x-www-form-urlencoded
  5. 普通对象 + FileListmultipart/form-data Content-Type → 走 toFormData 转换(FileList 被包装为 {'files[]': data});
  6. 其余对象 → 默认 JSON 序列化并设置 application/json

这条链路解释了本文所有示例“为什么能直接跑”:postForm 负责注入 Content-Type,transformRequest 负责把对象/FileList 转成 FormData,各适配器负责把 FormData 交给平台能力发出 multipart 请求。

小结

  • 浏览器单文件/多文件上传优先用 postForm;裸传 FileList 时字段名默认为 files[],想自定义就在 key 上追加 [];需要逐文件独立字段名则手动 new FormData()
  • 上传进度用 onUploadProgress,事件字段详见 Progress capturing
  • Node.js 中本地文件用 fs.createReadStream + form-data 包做流式上传,内存数据用 Buffer 并显式给出 filename/contentType/knownLength
  • Node.js 下上传可读流时务必设 maxRedirects: 0 以避免流被整体缓冲;
  • 相关实现入口:lib/core/Axios.jspostForm 别名)、lib/defaults/index.js(请求体转换)、lib/helpers/toFormData.js(对象/FileList 序列化)、lib/helpers/formDataToStream.js(multipart 编码)、tests/unit/toFormData.test.js(序列化行为的单测印证)。
登录后查看全文
热门项目推荐
相关项目推荐