axios 文件上传实战指南:postForm、FormData 与 multipart/form-data 的完整用法
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 在生成请求方法别名时,对 post、put、patch 等带数据的动词各生成一个 xxxForm 变体,并在请求配置中强制注入 Content-Type: multipart/form-data 头(query 因其幂等读语义被刻意排除):
- 方法别名生成逻辑:lib/core/Axios.js#L281-L306
// 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-data(postForm 已自动设置)时,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还接受indexes、dots、visitor等序列化选项(通过请求配置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 即可(传入 FormData 时 transformRequest 会原样放行,不再做 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。
从源码看,三个适配器都实现了上传进度:
- XHR 适配器基于
request.upload的progress事件,并用progressEventReducer做节流:lib/adapters/xhr.js#L206-L207; - Fetch 适配器在请求体是流时通过包装流逐块累计
loaded:lib/adapters/fetch.js#L355-L385; - Node.js 的 http 适配器同样支持
onUploadProgress(基于底层请求流的 data 事件):lib/adapters/http.js#L854-L876。
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-Length(lib/helpers/formDataToStream.js#L68-L117)。这段代码也解释了为何 Buffer 上传示例里knownLength很重要:只有各 part 大小已知时,axios 才能算出准确的Content-Length,否则只能使用分块传输(chunked)。
默认 transformRequest 的完整分流逻辑
把文档中的各个场景串起来,lib/defaults/index.js#L43-L107 的默认 transformRequest 按如下优先级处理请求体:
- 请求体是 HTML
<form>元素 → 自动转为FormData; - 请求体是
FormData→ 原样返回(除非 Content-Type 为 JSON,则formDataToJSON序列化); ArrayBuffer/Buffer/Stream/File/Blob/ReadableStream→ 原样返回;URLSearchParams→ 序列化为 querystring 并设置x-www-form-urlencoded;- 普通对象 +
FileList或multipart/form-dataContent-Type → 走toFormData转换(FileList被包装为{'files[]': data}); - 其余对象 → 默认 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.js(
postForm别名)、lib/defaults/index.js(请求体转换)、lib/helpers/toFormData.js(对象/FileList序列化)、lib/helpers/formDataToStream.js(multipart 编码)、tests/unit/toFormData.test.js(序列化行为的单测印证)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00