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

一个表单并不总是一个表单

随着开放Web应用程序的兴起,使用HTML forms而不是文字表单(literal forms for humans)日益普遍 - 越来越多的开发人员正在控制传输数据。

获得整体界面的控制

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

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

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

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

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

注意: 老的 AJAX 技术可能不依赖 XMLHttpRequest. 例如, JSONP 结合 eval() 函数. 它可以使用, 但是因其严重的安全问题不推荐使用。使用它的唯一原因是老的浏览器不支持 XMLHttpRequestJSON, 但是那些确实是非常古老的浏览器!避免使用这种技术.

创建之初, XMLHttpRequest 被提出是打算将 XML 做为传输数据的格式。不过,JSON已经取代了XML,而且今天已经非常普遍了。

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

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

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

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

发送表单数据

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

在DOM中构建一个隐藏的iframe

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

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

下面是个简单的例子:

<button onclick="sendData({test:'ok'})">Click Me!</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的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  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);
}

在线演示:

手动构建XMLHttpRequest

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

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

还是上一节的这个例子:

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

正如你所看到的,HTML并没有改变。 但是,JavaScript是完全不同的:

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

  // Turn the data object into an array of URL-encoded key/value pairs.
  for(name in data) {
    urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
  }

  // Combine the pairs into a single string and replace all %-encoded spaces to 
  // the '+' character; matches the behaviour of browser form submissions.
  urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');

  // Define what happens on successful data submission
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // Define what happens in case of error
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something goes wrong.');
  });

  // Set up our request
  XHR.open('POST', 'https://example.com/cors.php');

  // Add the required HTTP header for form data POST requests
  XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

  // Finally, send our data.
  XHR.send(urlEncodedData);
}

在线演示:

注: 使用XMLHttpRequest会受到同源策略的影响,如果你需要执行跨域请求,你需要熟悉一下CORS和HTTP访问控制.

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

手动建立一个HTTP请求可能是一个巨大的挑战(can be overwhelming)。 幸运的是,最近的XMLHttpRequest 规范提供了一种方便简单的方法来处理带有FormData对象的表单数据请求。

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

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

向FormData对象中手动添加数据

<button type="button" onclick="sendData({test:'ok'})">Click Me!</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! Data sent and response loaded.');
  });

  // 定义发生错误时执行的操作
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something goes wrong.');
  });

  // 设置请求地址和方法
  XHR.open('POST', 'http://ucommbieber.unl.edu/CORS/cors.php');

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

在线演示:

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

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

这段HTML是典型的情况:

<form id="myForm">
  <label for="myName">Send me your name:</label>
  <input id="myName" name="name" value="John">
  <input type="submit" value="Send Me!">
</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('Oups! Something goes wrong.');
    });

    // 我们设置了我们的请求
    XHR.open("POST", "http://ucommbieber.unl.edu/CORS/cors.php");

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

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

    sendData();
  });
});

在线演示:

发送二进制数据

如果你用来初始化formData对象的那个表单中包含了一个文件输入框(type=file的input元素),则在发送AJAX时,用户在这个文件输入框中选定的文件也会被发送,和正常的表单提交一样.而且即使你没有用表单初始化这个formData对象,你同样可以手动向这个formData对象中添加若干个二进制数据.

二进制数据的来源主要有三种:FileReader API,Canvas API,WebRTC API.不幸的是,在一些旧的浏览器中,我们没有能力访问二进制数据,或者需要一些很繁杂的解决办法才能实现.访问二进制数据已经超出了本文的介绍范围.如果你想知道更多关于FileReader API的知识,你可以阅读:如何在web应用程序中使用文件.

使用formData发送二进制数据非常简单,只需要调用append方法将你需要发送的File对象或者Blob对象添加进去.

在下面的例子中,我们使用了FileReader API来访问二进制数据,然后发送这个请求:

<form id="myForm">
  <p>
    <label for="i1">text data:</label>
    <input id="i1" name="myText" value="Some text data">
  </p>
  <p>
    <label for="i2">file data:</label>
    <input id="i2" name="myFile" type="file">
  </p>
  <button>Send Me!</button>
</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]) {
      // Start a new part in our body's request
      data += "--" + boundary + "\r\n";

      // Describe it as form data
      data += 'content-disposition: form-data; '
      // Define the name of the form data
            + 'name="'         + file.dom.name          + '"; '
      // Provide the real name of the file
            + 'filename="'     + file.dom.files[0].name + '"\r\n';
      // And the MIME type of the file
      data += 'Content-Type: ' + file.dom.files[0].type + '\r\n';

      // There's a blank line between the metadata and the data
      data += '\r\n';
      
      // Append the binary data to our body's request
      data += file.binary + '\r\n';
    }

    // Text data is simpler
    // Start a new part in our body's request
    data += "--" + boundary + "\r\n";

    // Say it's form data, and name it
    data += 'content-disposition: form-data; name="' + text.name + '"\r\n';
    // There's a blank line between the metadata and the data
    data += '\r\n';

    // Append the text data to our body's request
    data += text.value + "\r\n";

    // Once we are done, "close" the body's request
    data += "--" + boundary + "--";

    // Define what happens on successful data submission
    XHR.addEventListener('load', function(event) {
      alert('Yeah! Data sent and response loaded.');
    });

    // Define what happens in case of error
    XHR.addEventListener('error', function(event) {
      alert('Oups! Something went wrong.');
    });

    // Set up our request
    XHR.open('POST', 'https://example.com/cors.php');

    // Add the required HTTP header to handle a multipart form data POST request
    XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary);

    // And finally, send our data.
    XHR.send(data);
  }

  // Access our form...
  var form = document.getElementById("myForm");

  // ...to take over the submit event
  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 的支持)

文档标签和贡献者

最后编辑者: codeofjackie,