登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

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 不能表达多个独立同名值。

FormData同名条目、追加覆盖与读取方法的静态关系说明图
图1:同名字段结构说明图。写入语义分组区分追加与覆盖,条目分组表示可重复名称,读取语义分组区分首值与全值;连线表示对象关系,不表示执行顺序。

多文件逐个追加,不把数组直接塞进去

下面的表单有标签复选框和多文件控件,均声明 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 成功状态表示接口按约定响应,文件是否完成持久化仍由后端接口的成功语义定义。

FormData字段、multipart编码和接收端多值接口约定的静态关系说明图
图2:上传接口结构说明图。载荷分组包含文本与文件条目,编码分组关联 multipart 与 boundary,接收约定分组关联同名集合和接口契约;图示不是抓包或执行结果。

接收端按集合取值,避免转换时丢失数据

前端保留了重复条目,不代表任意后端绑定函数都会自动返回数组。接收端需要启用对应的 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 使用指南。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>