正如在前面的文章中讲到的,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请求,并获取请求结果。

Note: Older AJAX techniques might not rely on XMLHttpRequest. For example, JSONP combined with the eval() function. It works, but it's not recommended because of serious security issues. The only reason to use this is for legacy browsers that lack support for XMLHttpRequest or JSON, but those are very old browsers indeed! Avoid such techniques.

创建之初, 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>的内容。

警告: 避免使用这项技术. It's a security risk with third-party services because it leaves you open to script injection attacks. If you use HTTPS, it can affect the same origin policy, which can render the content of an <iframe> unreachable. However, this method may be your only option if you need to support very old browsers.

下面是个简单的例子:

<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>

You should be familiar with that HTML sample.

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);
}

在线演示:

Using FormData bound to a form element

You can also bind a FormData object to a <form> element. This creates a FormData that represents the data contained in the form.

The HTML is typical:

<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>

But JavaScript takes over the form:

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

    // We bind the FormData object and the form element
    var FD  = new FormData(form);

    // We define what will happen if the data are successfully sent
    XHR.addEventListener("load", function(event) {
      alert(event.target.responseText);
    });

    // We define what will happen if case of error
    XHR.addEventListener("error", function(event) {
      alert('Oups! Something goes wrong.');
    });

    // We setup our request
    XHR.open("POST", "http://ucommbieber.unl.edu/CORS/cors.php");

    // The data send are the one the user provide in the form
    XHR.send(FD);
  }
 
  // We need to access the form element
  var form = document.getElementById("myForm");

  // to takeover its submit event.
  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代码.

// Because we want to access DOM node,
// we initialize our script at page load.
window.addEventListener('load', function () {

  // These variables are used to store the form data
  var text = document.getElementById("i1");
  var file = {
        dom    : document.getElementById("i2"),
        binary : null
      };
 
  // Use the FileReader API to access file content
  var reader = new FileReader();

  // Because FileReader is asynchronous, store its
  // result when it finishes to read the file
  reader.addEventListener("load", function () {
    file.binary = reader.result;
  });

  // At page load, if a file is already selected, read it.
  if(file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // If not, read the file once the user selects it.
  file.dom.addEventListener("change", function () {
    if(reader.readyState === FileReader.LOADING) {
      reader.abort();
    }
    
    reader.readAsBinaryString(file.dom.files[0]);
  });

  // sendData is our main function
  function sendData() {
    // If there is a selected file, wait it is read
    // If there is not, delay the execution of the function
    if(!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // To construct our multipart form data request,
    // We need an XMLHttpRequest instance
    var XHR = new XMLHttpRequest();

    // We need a separator to define each part of the request
    var boundary = "blob";

    // Store our body request in a string.
    var data = "";

    // So, if the user has selected a file
    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();
  });
});

在线演示:

总结

Depending on the browser, sending form data through JavaScript can be easy or difficult. The FormData object is generally the answer, and don't hesitate to use a polyfill for it on legacy browsers:

文档标签和贡献者

 最后编辑者: yydzxz,