侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 276 篇文章
  • 累计收到 7 条评论

FormData 不会自动带上提交按钮

2026-11-14 / 0 评论 / 8 阅读

FormData 不会自动带上提交按钮

一个表单里放着「保存草稿」和「发布」两个按钮,提交到同一个地址时,服务端需要知道这次点击的是哪一个。把表单交给 new FormData(form),很容易以为按钮也会像普通输入框一样进入数据。它不会。

这个细节会让页面看起来提交成功,后端却只能看到表单字段,无法区分用户选的动作。原生构造函数有第二个参数,可以把实际触发提交的按钮传进去。

按钮值是有条件进入表单数据的

最小表单如下:

<form id="checkout">
  <label>Email address
    <input name="email" value="reader@example.test">
  </label>
  <button name="intent" value="save">Save draft</button>
  <button name="intent" value="publish">Publish</button>
</form>

直接执行 new FormData(form),数据里只有 email。两个按钮虽然各自有 name 和 value,构造时却没有任何信息能说明用户按了哪一个。传入发布按钮后,输出里会多出 intent=publish;保存按钮对应的具体键值见验证日志。

验证脚本是 tools/verify-formdata-submitter.mjs,日志保存在 tools/verify-formdata-submitter.log。脚本通过本机 Chrome 的 CDP 打开一个 data URL 页面,直接调用浏览器的 FormData,再把页面截图保存为封面。没有访问网络。

传入真正的提交按钮

可以在提交事件里读取 event.submitter,将它和表单一起交给构造函数:

const form = document.querySelector('#checkout');

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const data = new FormData(form, event.submitter);
  console.log([...data.entries()]);
});

点击「保存草稿」时,event.submitter 指向保存按钮,结果中会出现 ['intent', 'save']。点击「发布」则对应 ['intent', 'publish']。这里需要传的是按钮元素本身,而非按钮的文本或 value 字符串。

我原先以为只要按钮带了 name,它就和输入框一样会被收集;本机结果推翻了这个判断。FormData(form) 只有表单元素,没有点击动作的信息。第二个参数正是把这份上下文补进去。

同一个 name="intent" 下,值 save 和 publish 表示接口动作,按钮上的文字只是页面标签。服务端应按提交字段判断动作,不要从文案反推;文案需要调整时,协议值仍可保持稳定。本次脚本检查了按钮值进入表单数据,没有覆盖多语言页面。

四种写法的边界

  1. 只需要字段,不关心动作:用 new FormData(form) 即可。若所有按钮都走同一处理路径,省略 submitter 更直接。
  2. 需要保存或发布的区别:在提交事件中传入 event.submitter。请求数据里会包含触发提交的按钮字段。
  3. 代码主动构造数据:明确传入对应的按钮节点,例如 new FormData(form, saveButton)。测试中按钮必须属于这个表单,不能用任意元素替代。
  4. 代码主动触发表单提交:调用 form.requestSubmit(button),让指定按钮参与正常提交过程。submit 事件里的 event.submitter 会指向它,监听器可以据此构造 FormData。

最后一条很容易在重构时踩中。requestSubmit(button) 不只是帮忙派发一个事件,它把指定的按钮带入提交流程;本机实测里,传入发布按钮后,监听器读到的 event.submitter 就是发布按钮。若辅助函数接收了一个含义不明的元素,浏览器也不会替代码猜它是不是按钮。

浏览器还会拒绝把非提交按钮元素当作第二个参数。本机把输入框和 type="button" 的预览按钮分别传给构造函数,两次结果都是 TypeError。按钮看起来像动作入口,并不代表它属于提交过程。若页面有预览按钮,它应继续走自己的点击逻辑,不能冒充提交按钮交给 FormData。

这一点和给按钮设置 name 是两件事。name=value 决定字段的键和值,第二个参数决定哪一个提交按钮参与当前数据构造。测试中,保存和发布按钮使用同一个 name="intent",但最后进入数据的值分别为 save 和 publish。接口收到 intent 后才能判断当前动作;按钮文字只负责显示给用户,不会自动变成提交字段。

本机脚本还调用了 form.requestSubmit(publishButton),并在 submit 事件中检查 event.submitter。观察值是 publish。这给程序触发提交提供了明确的入口,不必模拟鼠标点击,也不会把另一个按钮误带进去。实际页面仍应在事件处理器中使用表单节点和 event.submitter,而不是把某个按钮硬编码到所有路径里。

验证使用的是本机 HeadlessChrome 156。其他浏览器版本没有在这次验证里覆盖;脚本只验证表单数据构造、无效 submitter 的错误和 requestSubmit 的事件提交者,没有发出网络请求。遇到历史浏览器或自定义表单控件时,建议在目标运行环境补一遍同样的用例。

动作字段要由数据入口决定

按钮的 name=value 不是表单数据里的常驻字段,它描述的是当前提交者。脚本若在提交事件外重新创建 FormData,就该显式保留提交按钮;否则保存和发布可能落到同一份字段集合里。

如果只是上传字段,普通构造最简单。需要区分多个提交动作时,把实际提交按钮交给构造函数;若页面脚本负责触发提交流程,用 requestSubmit(button) 保留浏览器提交语义。

    🤞 分享