使用 JavaScript 发送表单

正如在前面的文章中讲到的,HTML 表单可以声明式地发送一个 HTTP 请求。 但也可以通过 JavaScript 来为表单准备用于发送的 HTTP 请求。 本文探讨如何做到这一点。

表单不总是表单

开放式Web应用程序中,使用 HTML form 而不是文字表单让人们来填写变得越来越普遍了 — 越来越多的开发人员正致力于控制传输数据。

获得整体界面的控制

标准的 HTML 表单提交会加载数据要发送到的URL,这意味着浏览器窗口以整页加载进行导航。 可以通过隐藏闪烁和网络滞后来避免整页加载以提供更平滑的体验。

许多现代用户界面只使用HTML表单来收集用户的输入。 当用户尝试发送数据时,应用程序将在后台采取控制并且异步地传输数据,只更新UI中需要更改的部分。

异步地发送任何数据被称为 AJAX,它代表 "Asynchronous JavaScript And XML"。

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

AJAX 技术主要依靠 XMLHttpRequest (XHR) DOM 对象。它可以构造 HTTP 请求、发送它们,并获取请求结果。

注意: 老旧的 AJAX 技术可能不依赖 XMLHttpRequest。例如 JSONPeval() 函数。这也行得通,但是有严重的安全问题,不推荐使用它。使用它的唯一原因是为了不支持 XMLHttpRequestJSON的过时浏览器;但是那些浏览器实在是太古老了!避免使用这种技术。

创建之初, XMLHttpRequest 被设计用来将 XML 作为传输数据的格式获取和发送。不过,如今 JSON 已经取代了 XML,而且要常用的多,无论这是不是一件好事。

但是 XML 和 JSON 都不适合对表单数据请求编码。 表单数据(application/x-www-form-urlencoded)由 URL编码的键/值对列表组成。为了传输二进制数据,HTTP请求被重新整合成multipart/form-data形式。

如果您控制前端(在浏览器中执行的代码)和后端(在服务器上执行的代码),则可以发送JSON / XML并根据需要处理它们。

但是,如果你想使用第三方服务,没有那么简单。 有些服务只接受表单数据。 也有使用表单数据更简单的情况。 如果数据是键/值对,或是原始二进制数据,以现有的后端工具不需要额外的代码就可以处理它。

那么如何发送这样的数据呢?

发送表单数据

一共有三种方式来发送表单数据:包括两种传统的方法和一种利用 formData对象的新方法.让我们仔细看一下:

构建 XMLHttpRequest

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

备注:如果想要了解更多关于 XMLHttpRequest 的知识,你可能会对两篇文章感兴趣:An introductory article to AJAX 和更高级点的using XMLHttpRequest.

让我们重建之前的这个例子:

<button type="button" onclick="sendData({test:'ok'})">点击我!</button>

正如你所看到的,HTML实际上没什么改变。 不过,JavaScript变得截然不同了:

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var urlEncodedData = "";
  var urlEncodedDataPairs = [];
  var name;

  // 将数据对象转换为URL编码的键/值对数组。
  for(name in data) {
    urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
  }

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

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

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

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

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

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

在线演示:

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

使用 XMLHttpRequest 和 the FormData object(表单数据对象)

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

FormData 对象可以用来构建用于传输的表单数据,或是获取表单元素中的数据来管理它的发送方式。 请注意,FormData 对象是“只写”的,这意味着您可以更改它们,但不能检索其内容。

使用这个对象在Using FormData Objects中有详细的介绍,不过这里有两个例子:

使用一个独立的 FormData 对象

<button type="button" onclick="sendData({test:'ok'})">点我!</button>

你应该会觉得那个HTML示例很熟悉。

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

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

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

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

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

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

在线演示:

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

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

这段HTML是典型的情况:

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

但是 JavaScript 接管了这个表单:

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

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

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

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

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

    // 发送的数据是由用户在表单中提供的
    XHR.send(FD);
  }
 
  // 我们需要获取表单元素
  var form = document.getElementById("myForm");

  // ...然后接管表单的提交事件
  form.addEventListener("submit", function (event) {
    event.preventDefault();

    sendData();
  });
});

在线演示:

你甚至可以通过使用表单的elements 属性来更多的参与此过程,来得到一个包含表单里所有数据元素的列表,并且逐一手动管理他们。想了解更多,请参阅这里的例子:Accessing the element list's contents in HTMLFormElement.elements

在隐藏的iframe中构建DOM

最古老的异步发送表单数据方法是用 DOM API 构建表单,然后将其数据发送到隐藏的 <iframe>。 要获得提交的结果,请获取<iframe>的内容。

警告:不要使用这项技术。有第三方服务的安全风险,因为它会使你暴露在 脚本注入攻击 中. 如果你使用 HTTPS,它会影响 同源策略, 这可以使 <iframe> 内容无法访问。然而,该方法可能是你需要支持很古老的浏览器的唯一选择。

下面是个简单的例子:

<button onclick="sendData({test:'ok'})">点击我!</button>

所有操作都在下面这段脚本里:

// 首先创建一个用来发送数据的iframe.
var iframe = document.createElement("iframe");
iframe.name = "myTarget";

// 然后,将iframe附加到主文档
window.addEventListener("load", function () {
  iframe.style.display = "none";
  document.body.appendChild(iframe);
});

// 下面这个函数是真正用来发送数据的.
// 它只有一个参数,一个由键值对填充的对象.
function sendData(data) {
  var name,
      form = document.createElement("form"),
      node = document.createElement("input");

  // 定义响应时发生的事件
  iframe.addEventListener("load", function () {
    alert("Yeah! Data sent.");
  });
    
  form.action = "http://www.cs.tut.fi/cgi-bin/run/~jkorpela/echo.cgi";
  form.target = iframe.name;

  for(name in data) {
    node.name  = name;
    node.value = data[name].toString();
    form.appendChild(node.cloneNode());
  }

  // 表单元素需要附加到主文档中,才可以被发送。
  form.style.display = "none";
  document.body.appendChild(form);

  form.submit();

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送。
  document.body.removeChild(form);
}

在线演示:

处理二进制数据

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

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

formData 的帮助下,发送二进制数据非常简单,使用 append() 方法就可以了。如果你必须手动进行,那确实会有一些棘手。

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

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

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

// 因为我们想获取 DOM 节点,
// 我们在页面加载时初始化我们的脚本.
window.addEventListener('load', function () {

  // 这些变量用于存储表单数据
  var text = document.getElementById("i1");
  var file = {
        dom    : document.getElementById("i2"),
        binary : null
      };
 
  // 使用 FileReader API 获取文件内容
  var reader = new FileReader();

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

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

  // 如果没有被选择,一旦用户选择了它,就读取文件。
  file.dom.addEventListener("change", function () {
    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 实例
    var XHR = new XMLHttpRequest();

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

    // 将我们的主体请求存储于一个字符串中
    var 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', function(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);
  }

  // 访问表单…
  var form = document.getElementById("myForm");

  // …接管提交事件
  form.addEventListener('submit', function (event) {
    event.preventDefault();
    sendData();
  });
});

在线演示:

总结

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

  • gist 通过 Web Workers polyfill 了 FormData
  • HTML5-formdata 试图 polyfill FormData 对象, 但是它需要 File API
  • 这个 polyfill 提供了 FormData 所有的大部分新方法(entries, keys, values, 以及对 for...of 的支持)

In this module