使用 JavaScript 发送表单

HTML 表单可以声明式地发送一个 HTTP 请求。但也可以通过 JavaScript 来为表单准备用于发送的 HTTP 请求(例如通过 XMLHttpRequest)。本文对这些方法进行了探讨。

表单不总是表单

在渐进式 Web 应用、单页应用和基于框架的应用中,通常会使用 HTML 表单来发送数据,而不会在收到响应数据时加载新文档。让我们先来谈谈为什么这需要一种不同的方法。

获得整体界面的控制

如前一篇文章所述,标准 HTML 表单提交会加载发送数据的 URL,这意味着浏览器窗口会以全页面加载的方式进行导航。避免全页面加载可以避免网络延迟和可能出现的视觉问题(如闪烁),从而提供更流畅的体验。

许多现代用户界面只使用 HTML 表单来收集用户输入,而不是用于数据提交。当用户尝试发送数据时,应用程序会控制并在后台异步传输数据,只更新用户界面中需要更改的部分。

表单提交和 AJAX 请求之间的区别?

XMLHttpRequest(XHR)DOM 对象可以构建 HTTP 请求、发送请求并获取结果。从历史上看,XMLHttpRequest 是为获取和发送 XML 作为交换格式而设计的,后来这种格式被 JSON 所取代。但是,XML 和 JSON 都不适合表单数据请求编码。表单数据(application/x-www-form-urlencoded)由键/值对的 URL 编码列表组成。为了传输二进制数据,HTTP 请求被重塑为 multipart/form-data

备注: 如今 Fetch API 常用于取代 XHR——它是 XHR 的更现代、更新的版本,工作原理类似,但有一些优点。你在本文中看到的大部分 XHR 代码都可以换成 Fetch。

如果你控制了前端(在浏览器中执行的代码)和后端(在服务器上执行的代码),就可以发送 JSON/XML,并随心所欲地处理它们。

但如果要使用第三方服务,就需要按照服务要求的格式发送数据。

那么我们应该如何发送这些数据呢?下面将介绍所需要的不同技术。

发送表单数据

一共有三种方式来发送表单数据:

  • 手工构建 XMLHttpRequest
  • 使用独立的 FormData 对象。
  • 使用绑定到 <form> 元素的 FormData

让我们仔细看一下。

手工构建 XMLHttpRequest

XMLHttpRequest 是进行 HTTP 请求的最安全可靠的方式。要使用 XMLHttpRequest 发送表单数据,请通过 URL 编码准备数据,并遵守表单数据请求的具体规定。

让我们看个示例:

html
<button>点我!</button>

这是 JavaScript 代码部分:

js
const btn = document.querySelector("button");

function sendData(data) {
  console.log("Sending data");

  const XHR = new XMLHttpRequest();

  const urlEncodedDataPairs = [];

  // 将数据对象转换为 URL 编码的键/值对数组。
  for (const [name, value] of Object.entries(data)) {
    urlEncodedDataPairs.push(
      `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
    );
  }

  // 将配对合并为单个字符串,并将所有 % 编码的空格替换为
  // “+”字符;匹配浏览器表单提交的行为。
  const urlEncodedData = urlEncodedDataPairs.join("&").replace(/%20/g, "+");

  // 定义成功数据提交时发生的情况
  XHR.addEventListener("load", (event) => {
    alert("耶!已发送数据并加载响应。");
  });

  // 定义错误提示
  XHR.addEventListener("error", (event) => {
    alert("哎呀!出问题了。");
  });

  // 建立我们的请求
  XHR.open("POST", "https://example.com/cors.php");

  // 为表单数据 POST 请求添加所需的 HTTP 头
  XHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

  // 最后,发送我们的数据。
  XHR.send(urlEncodedData);
}

btn.addEventListener("click", () => {
  sendData({ test: "ok" });
});

这里是实时演示效果:

备注: 当你想要往第三方网站传输数据时,使用 XMLHttpRequest 会受到同源策略的影响。如果你需要执行跨源请求,你需要熟悉一下 CORS 和 HTTP 访问控制

使用 XMLHttpRequest 和 FormData 对象

手动建立一个 HTTP 请求非常困难。幸运的是,XMLHttpRequest 规范提供了一种方便简单的方法——利用 FormData 对象来处理表单数据请求。

FormData 对象可以用来构建用于传输的表单数据,或是获取表单元素中的数据来管理它的发送方式。

该对象的使用详见使用 FormData 对象,下面是两个示例:

使用一个独立的 FormData 对象

html
<button>点我!</button>

你应该会觉得那个 HTML 示例很熟悉,现在来展示 JavaScript 代码:

js
const btn = document.querySelector("button");

function sendData(data) {
  const XHR = new XMLHttpRequest();
  const FD = new FormData();

  // 把我们的数据添加到这个 FormData 对象中
  for (const [name, value] of Object.entries(data)) {
    FD.append(name, value);
  }

  // 定义数据成功发送并返回后执行的操作
  XHR.addEventListener("load", (event) => {
    alert("耶!已发送数据并加载响应。");
  });

  // 定义发生错误时执行的操作
  XHR.addEventListener("error", (event) => {
    alert("Oops! 出错了。");
  });

  // 设置请求地址和方法
  XHR.open("POST", "https://example.com/cors.php");

  // 发送这个 formData 对象,HTTP 请求头会自动设置
  XHR.send(FD);
}

btn.addEventListener("click", () => {
  sendData({ test: "ok" });
});

这里是实时演示效果:

使用绑定到表单元素上的 FormData

你也可以把一个 FormData 对象绑定到一个 <form> 元素上。这会创建一个 FormData 对象,表示表单中包含的数据。

这段 HTML 是典型的情况:

html
<form id="myForm">
  <label for="myName">告诉我你的名字:</label>
  <input id="myName" name="name" value="小明" />
  <input type="submit" value="提交" />
</form>

但是 JavaScript 接管了这个表单:

js
window.addEventListener("load", () => {
  function sendData() {
    const XHR = new XMLHttpRequest();

    // 我们把这个 FormData 和表单元素绑定在一起。
    const FD = new FormData(form);

    // 我们定义了数据成功发送时会发生的事件
    XHR.addEventListener("load", (event) => {
      alert(event.target.responseText);
    });

    // 我们定义了失败的情形下会发生的事件
    XHR.addEventListener("error", (event) => {
      alert("哎呀!出了一些问题。");
    });

    // 我们设置了我们的请求
    XHR.open("POST", "https://example.com/cors.php");

    // 发送的数据是由用户在表单中提供的
    XHR.send(FD);
  }

  // 获取表单元素
  const form = document.getElementById("myForm");

  // 接管表单的 submit 事件
  form.addEventListener("submit", (event) => {
    event.preventDefault();

    sendData();
  });
});

这里是实时演示效果:

你甚至可以通过使用表单的 elements 属性来更多的参与此过程,来得到一个包含表单里所有数据元素的列表,并且逐一手动管理它们。想了解更多,请参见示例访问表单控件

处理二进制数据

如果你使用一个含有 <input type="file"> 组件的 FormData 表单对象,数据会被自动处理。但是要手动发送二进制数据的话,还有额外的工作要做。

在现代网络上,二进制数据有很多来源:例如 FileReaderCanvasWebRTC,等等。不幸的是,一些过时的浏览器无法访问二进制数据,或是需要非常复杂的工作环境。这些遗留问题已经超出了本文的涵盖范围。如果你想了解更多关于 FileReader API 的知识,参见如何在 web 应用程序中使用文件

发送二进制数据最简单的方法是使用 FormDataappend() 方法,如上图所示。如果必须手工操作,就比较麻烦了。

在下面的例子中,我们使用了FileReader API 来访问二进制数据,然后手动构建多部分表单数据请求:

html
<form id="myForm">
  <p>
    <label for="theText">文本数据:</label>
    <input id="theText" name="myText" value="一些文本数据" type="text" />
  </p>
  <p>
    <label for="theFile">文件数据:</label>
    <input id="theFile" name="myFile" type="file" />
  </p>
  <button>提交!</button>
</form>

如你所见,这个 HTML 只是一个标准的 <form>,没有什么神奇的事情。“魔法”都在 JavaScript 里:

js
// 因为我们想获取 DOM 节点,
// 我们在页面加载时初始化我们的脚本。
window.addEventListener("load", () => {
  // 这些变量用于存储表单数据
  const text = document.getElementById("theText");
  const file = {
    dom: document.getElementById("theFile"),
    binary: null,
  };

  // 使用 FileReader API 获取文件内容
  const reader = new FileReader();

  // 因为 FileReader 是异步的,会在完成读取文件时存储结果
  reader.addEventListener("load", () => {
    file.binary = reader.result;
  });

  // 页面加载时,如果一个文件已经被选择,那么读取该文件。
  if (file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // 如果没有被选择,一旦用户选择了它,就读取文件。
  file.dom.addEventListener("change", () => {
    if (reader.readyState === FileReader.LOADING) {
      reader.abort();
    }

    reader.readAsBinaryString(file.dom.files[0]);
  });

  // 发送数据是我们需要的主要功能
  function sendData() {
    // 如果存在被选择的文件,等待它读取完成
    // 如果没有,延迟函数的执行
    if (!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // 要构建我们的多部分表单数据请求,
    // 我们需要一个 XMLHttpRequest 实例
    const XHR = new XMLHttpRequest();

    // 我们需要一个分隔符来定义请求的每一部分。
    const boundary = "blob";

    // 将我们的主体请求存储于一个字符串中
    let data = "";

    // 所以,如果用户已经选择了一个文件
    if (file.dom.files[0]) {
      // 在请求体中开始新的一部分
      data += `--${boundary}\r\n`;

      // 把它描述成表单数据
      data +=
        "content-disposition: form-data; " +
        // 定义表单数据的名称
        `name="${file.dom.name}"; ` +
        // 提供文件的真实名字
        `filename="${file.dom.files[0].name}"\r\n`;
      // 和文件的 MIME 类型
      data += `Content-Type: ${file.dom.files[0].type}\r\n`;

      // 元数据和数据之间有一条空行。
      data += "\r\n";

      // 将二进制数据添加到主体请求中
      data += file.binary + "\r\n";
    }

    // 文本数据更简单一些
    // 在主体请求中开始一个新的部分
    data += `--${boundary}\r\n`;

    // 假设这是表单数据,并命名它
    data += `content-disposition: form-data; name="${text.name}"\r\n`;
    // 元数据和数据之间有一条空行。
    data += "\r\n";

    // 添加文本数据到主体请求中
    data += text.value + "\r\n";

    // 一旦完成,“关闭”主体请求
    data += `--${boundary}--`;

    // 定义成功提交数据执行的语句
    XHR.addEventListener("load", (event) => {
      alert("耶!已发送数据并加载响应。");
    });

    // 定义发生错误时做的事
    XHR.addEventListener("error", function (event) {
      alert("哎呀!出现了一些问题。");
    });

    // 建立请求
    XHR.open("POST", "https://example.com/cors.php");

    // 添加需要的 HTTP 头部来处理多部分表单数据 POST 请求
    XHR.setRequestHeader(
      "Content-Type",
      `multipart/form-data; boundary=${boundary}`,
    );

    // 最后,发送数据。
    XHR.send(data);
  }

  // 获取表单元素
  const form = document.getElementById("theForm");

  // 添加 submit 事件处理器
  form.addEventListener("submit", (event) => {
    event.preventDefault();
    sendData();
  });
});

这里是实时演示效果:

总结

取决于不同的浏览器和正在处理数据的类型,通过 JavaScript 发送数据可能会很简单,也可能会很困难。FormData 对象是通用的答案,所以请毫不犹豫地在旧浏览器上通过 polyfill 使用它:

参见

学习路径

更进一步