JavaScript FormData 怎样上传多个同名字段
来源:17golang原创
时间:2026-10-29 08:28:18 459浏览 收藏
FormData 上传多个同名字段时,对同一个字段名反复调用 append(),每个值单独追加一次即可。多个文件也逐个追加 File 对象。不要在循环里调用 set():它会把该名称已有的全部值替换成一个新值。发送时把 FormData 直接交给 fetch 的 body,并让浏览器生成 multipart 的 Content-Type 和 boundary。
官方文档地址:https://developer.mozilla.org/en-US/docs/Web/API/FormData
先约定一个字段名可以出现多次
以资料提交接口为例:一份资料有一个 title、若干 tag,以及若干 attachment。title 是单值;tag 和 attachment 是多值。前端需要表达的是“同名条目组成的集合”,不能先把所有字段放进只保留一个同名属性的普通对象,再期待接收端还原丢失的条目。
| 字段名 | 约定 | 写入方式 |
|---|---|---|
| title | 一条文本 | 一次 set |
| tag | 零条或多条文本 | 每条 append |
| attachment | 一条或多条文件 | 每个 File 一次 append |
这里采用 tag 和 attachment 作为字面字段名。tag[] 是另一个名称,方括号不是 FormData 自动开启的数组开关;是否采用这种命名取决于接收端框架和接口约定。不要前端发送 attachment,接收端却查找 attachment[]。
用 append 追加,用 getAll 读取
FormData 可以保存同名的多个条目。append 表示增加一个条目,set 表示把该名称收敛成一个值。读取时,get 只取第一个匹配值;getAll 返回该名称的全部值,名称不存在时得到空数组。
// 多值字段逐条追加;标量字段单独设置。
const payload = new FormData();
payload.set("title", "设备巡检资料");
for (const tag of ["巡检", "待归档"]) {
payload.append("tag", tag);
}
// 以下是按接口语义推导的值,供理解,不表示已在本机运行。
const tags = payload.getAll("tag"); // ["巡检", "待归档"]
const firstTag = payload.get("tag"); // "巡检"
// 如果业务要把全部标签改为一个标签,才使用 set。
payload.set("tag", "已归档");
const replacedTags = payload.getAll("tag"); // ["已归档"]上述最后一次 set 专门展示覆盖语义,不应放进上传多个标签的构建循环。如果需求是替换成一组新标签,可以先 delete("tag"),再对新集合逐次 append;单次 set 不能表达多个独立同名值。

多文件逐个追加,不把数组直接塞进去
下面的表单有标签复选框和多文件控件,均声明 name。构建时直接使用 new FormData(form),浏览器会收集可提交的表单控件;已经由表单收集的文件,不要再循环追加一遍。未勾选的复选框不提交,禁用控件也不收集。
如果文件来自拖放区域或应用自己维护的 File 列表,可以从空 FormData 构建,而不是从表单重复收集。下面的构建函数与表单构建方式二选一:
function buildMaterialPayload(title, tags, files) {
// 显式要求字符串和 File 集合,避免数组、对象被隐式转成字符串。
if (typeof title !== "string" || title.trim() === "") {
throw new TypeError("标题不能为空");
}
if (!Array.isArray(tags) || !tags.every(tag => typeof tag === "string")) {
throw new TypeError("标签必须是字符串数组");
}
const fileList = Array.from(files);
if (fileList.length === 0 || !fileList.every(file => file instanceof File)) {
throw new TypeError("至少提供一份 File 附件");
}
const data = new FormData();
data.set("title", title.trim());
for (const tag of tags) data.append("tag", tag);
for (const file of fileList) {
// 第三个参数声明传给服务端的文件名,不会读取用户的本地路径。
data.append("attachment", file, file.name);
}
return data;
}不要写 data.append("tag", tags) 来代替循环。非字符串、非 Blob 的值会被转为字符串,数组不会因此展开成多个条目;普通对象也不会自动编码为 JSON。若接口明确要求 JSON 字段,应主动 JSON.stringify,并在接口中约定这个字段的解析方式。
让浏览器填写 multipart 边界并处理失败
下面接续前面的表单示例。/api/materials 是需要按自己项目实现的同源接口路径,示例没有提供后端,也没有假设返回 JSON。它把 HTTP 非成功状态与网络失败都展示给调用方,避免 fetch 收到 400 或 500 后仍显示成功。
const form = document.querySelector("#material-form");
const status = document.querySelector("#material-status");
const submit = form.querySelector('button[type="submit"]');
form.addEventListener("submit", async (event) => {
event.preventDefault();
// 这里选择从表单构建,不再额外追加同一批文件。
const payload = new FormData(form);
const files = payload.getAll("attachment");
if (!files.some(file => file instanceof File && file.name !== "")) {
status.textContent = "请先选择附件";
return;
}
submit.disabled = true;
status.textContent = "正在提交";
try {
const response = await fetch("/api/materials", {
method: "POST",
body: payload,
// 不手写 Content-Type;浏览器需要填写与请求体匹配的 boundary。
});
if (!response.ok) {
throw new Error(`提交失败,HTTP ${response.status}`);
}
status.textContent = "接口已返回成功状态";
} catch (error) {
// 使用 textContent,错误消息只作为文字显示。
status.textContent = error instanceof Error ? error.message : "提交失败";
} finally {
submit.disabled = false;
}
});若项目的公共请求封装默认加入 application/json 或固定的 multipart/form-data,需要在发送 FormData 时移除该默认值。只写 multipart/form-data 而没有与实际请求体对应的 boundary,也会妨碍解析。HTTP 成功状态表示接口按约定响应,文件是否完成持久化仍由后端接口的成功语义定义。

接收端按集合取值,避免转换时丢失数据
前端保留了重复条目,不代表任意后端绑定函数都会自动返回数组。接收端需要启用对应的 multipart 解析能力,并按照框架提供的多值字段或多文件接口读取。请求里的字面名称与接收端读取名称应一致;是否需要 [] 后缀必须共同约定。
在前端排查自己构建的载荷时,用 getAll("tag") 和 getAll("attachment") 查看集合。不要用 Object.fromEntries(payload) 判断重复字段是否存在:普通对象同名属性只能留下一个值,这种转换会丢失多值信息。保留条目对或按已知字段显式组织数组,才符合这个接口的语义。
| 现象 | 需要区分的原因 | 对应处理 |
|---|---|---|
| 每个标签只剩最后一个 | 循环 set 或转成普通对象 | 改用 append,保持条目集合 |
| 附件重复 | 表单已收集后又手动 append | 选择一种构建来源 |
| 服务端只读到一项 | 单值绑定函数或字段名约定不同 | 使用框架的集合读取方式 |
| multipart 无法解析 | 默认请求头覆盖浏览器编码头 | 移除手写 Content-Type |
同名字段需要写成数组 JSON 吗?
仅当接口明确要求某个字段保存 JSON 时才这样做。重复文本条目、JSON 字符串字段和多个文件条目是不同的接口设计,不能随意互换。
文件名相同会不会覆盖 FormData 中前一个文件?
append 不会因为文件名相同而把之前的条目改成 set。后端如何命名存储文件是另一项约定,不能用客户端文件名充当唯一存储键。
空标签数组怎样表达?
对空数组不执行 append,则载荷里没有 tag 条目。接收端应明确缺省代表空集合、保持旧值还是参数错误;不要用一个空字符串条目冒充空集合。
可以直接链式调用 append 吗?
append 返回 undefined,不返回 FormData。应在同一个对象上逐次调用,然后把该对象交给 fetch。
参考文档:MDN append、MDN set、MDN getAll、MDN FormData 使用指南、MDN Fetch 使用指南。
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
330 收藏
-
109 收藏
-
246 收藏
-
文章 · 前端 | 1小时前 | 前端 · javascript · JavaScript input textarea setRangeText selectionStart selectionEnd313 收藏
-
279 收藏
-
文章 · 前端 | 1小时前 | 前端 · javascript · 浏览器API · JavaScript ResizeObserver ResizeObserverEntry contentBoxSize inlineSize blockSize427 收藏
-
257 收藏
-
文章 · 前端 | 1小时前 | 前端 · 流式处理 · javascript · JavaScript ReadableStream TransformStream pipeThrough Streams API402 收藏
-
258 收藏
-
340 收藏
-
489 收藏
-
330 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习