axios 文件上传实战指南:postForm 与 FormData 的 multipart/form-data 上传机制
axios 让文件上传变得很直接:当你需要以 multipart/form-data 格式上传文件时,使用 postForm 或手动构造 FormData 即可。本篇基于 axios 官方文档中的“File posting(文件上传)”指南,结合当前仓库源码,系统讲解浏览器与 Node.js 两种环境下上传单个文件、多个文件、Buffer 与流式读取文件的完整做法,并深入剖析 postForm 方法如何自动生成 Content-Type、transformRequest 如何把 FileList 包装成 files[] 字段、以及 toFormData 转换器的底层规则。
两种上传方式:postForm 与 FormData
axios 提供了两条上传路径:
postForm(以及putForm、patchForm):直接把普通 JS 对象作为请求体传入,axios 自动将其序列化为multipart/form-data,并自动设置正确的请求头;- 手动构造
FormData:当需要精细控制字段名、顺序或附件元数据(如filename、contentType)时,直接使用浏览器或form-data包构造FormData对象,再传给axios.post。
从源码结构看,postForm 等便捷方法是动态生成的。lib/core/Axios.js 中对 post、put、patch 三个带数据的方法统一遍历:
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); - 若数据是
ArrayBuffer、Buffer、Stream、File、Blob、ReadableStream之一,直接透传; - 若 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.js 的
transformRequest与 lib/helpers/toFormData.js 的toFormData,遇到字段名或嵌套结构不符合预期时,应优先检查这两个文件中的分支逻辑。
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 StartedRust0623
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