2021年6月22日

表单数据

本章介绍如何发送 HTML 表单:带或不带文件,带附加字段等等。

FormData 对象可以帮助我们实现这一点。正如你可能猜到的,它是一个用于表示 HTML 表单数据的对象。

构造函数是

let formData = new FormData([form]);

如果提供了 HTML form 元素,它会自动捕获其字段。

FormData 的特别之处在于,网络方法(如 fetch)可以接受 FormData 对象作为主体。它会被编码并以 Content-Type: multipart/form-data 的形式发送出去。

从服务器的角度来看,这看起来像一个普通的表单提交。

发送一个简单的表单

让我们先发送一个简单的表单。

如你所见,这几乎是一行代码。

<form id="formElem">
  <input type="text" name="name" value="John">
  <input type="text" name="surname" value="Smith">
  <input type="submit">
</form>

<script>
  formElem.onsubmit = async (e) => {
    e.preventDefault();

    let response = await fetch('/article/formdata/post/user', {
      method: 'POST',
      body: new FormData(formElem)
    });

    let result = await response.json();

    alert(result.message);
  };
</script>

在这个例子中,服务器代码没有展示,因为它超出了我们的范围。服务器接受 POST 请求并回复“用户已保存”。

FormData 方法

我们可以使用方法修改 FormData 中的字段

  • formData.append(name, value) – 添加一个具有给定 namevalue 的表单字段,
  • formData.append(name, blob, fileName) – 添加一个字段,就像它是 <input type="file"> 一样,第三个参数 fileName 设置文件名(不是表单字段名),就像用户文件系统中的文件名一样,
  • formData.delete(name) – 删除具有给定 name 的字段,
  • formData.get(name) – 获取具有给定 name 的字段的值,
  • formData.has(name) – 如果存在具有给定 name 的字段,则返回 true,否则返回 false

从技术上讲,表单允许具有多个具有相同 name 的字段,因此对 append 的多次调用会添加更多同名字段。

还有一个 set 方法,语法与 append 相同。区别在于 .set 会删除所有具有给定 name 的字段,然后添加一个新字段。因此,它确保只有一个具有该 name 的字段,其余部分与 append 相同。

  • formData.set(name, value),
  • formData.set(name, blob, fileName).

我们还可以使用 for..of 循环遍历 formData 字段

let formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');

// List key/value pairs
for(let [name, value] of formData) {
  alert(`${name} = ${value}`); // key1 = value1, then key2 = value2
}

发送包含文件的表单

表单始终以 Content-Type: multipart/form-data 发送,这种编码允许发送文件。因此,<input type="file"> 字段也会被发送,类似于普通的表单提交。

以下是一个包含此类表单的示例

<form id="formElem">
  <input type="text" name="firstName" value="John">
  Picture: <input type="file" name="picture" accept="image/*">
  <input type="submit">
</form>

<script>
  formElem.onsubmit = async (e) => {
    e.preventDefault();

    let response = await fetch('/article/formdata/post/user-avatar', {
      method: 'POST',
      body: new FormData(formElem)
    });

    let result = await response.json();

    alert(result.message);
  };
</script>

发送包含 Blob 数据的表单

正如我们在 Fetch 章节中所见,很容易发送动态生成的二进制数据,例如图像,作为 Blob。我们可以直接将其作为 fetch 参数 body 提供。

然而,在实践中,通常将图像作为表单的一部分发送,而不是单独发送,并附带其他字段,例如“名称”和其他元数据,会更方便。

此外,服务器通常更适合接受多部分编码的表单,而不是原始二进制数据。

此示例使用 FormData 将来自 <canvas> 的图像以及其他一些字段作为表单提交。

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));

      let formData = new FormData();
      formData.append("firstName", "John");
      formData.append("image", imageBlob, "image.png");

      let response = await fetch('/article/formdata/post/image-form', {
        method: 'POST',
        body: formData
      });
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

请注意图像 Blob 是如何添加的。

formData.append("image", imageBlob, "image.png");

这与在表单中存在 <input type="file" name="image">,并且访问者提交了一个名为 "image.png"(第三个参数)的文件,该文件包含来自其文件系统的 imageBlob(第二个参数)数据相同。

服务器读取表单数据和文件,就像它是一个常规的表单提交一样。

总结

FormData 对象用于捕获 HTML 表单并使用 fetch 或其他网络方法提交它。

我们可以从 HTML 表单创建 new FormData(form),也可以在没有表单的情况下创建对象,然后使用方法追加字段。

  • formData.append(name, value)
  • formData.append(name, blob, fileName)
  • formData.set(name, value)
  • formData.set(name, blob, fileName)

让我们注意这里有两个特殊之处。

  1. set 方法会删除具有相同名称的字段,而 append 不会。这是它们之间唯一的区别。
  2. 要发送文件,需要使用 3 个参数的语法,最后一个参数是文件名,通常是从用户文件系统为 <input type="file"> 获取的。

其他方法是

  • formData.delete(name)
  • formData.get(name)
  • formData.has(name)

就是这样!

教程地图

评论

在评论之前阅读此内容…
  • 如果您有任何改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请将它们包装在 <pre> 标签中,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)。