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

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

获得整体界面的控制

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

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

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

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

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

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

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

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

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

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

发送表单数据

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

在DOM中构建一个隐藏的iframe

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

下面是个简单的例子:

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

// 首先创建一个用来发送数据的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进行编码来准备数据,并遵守表单数据请求的具体内容。

这里有一个例子:

正如你所看到的,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 和 the FormData object(表单对象)

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

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

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

向FormData对象中手动添加数据

你应该对那个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 对象到一个

元素上。这会创建一个 FormData ,代表表单中包含的元素。

这段HTML是典型的情况:

但是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来访问二进制数据,然后发送这个请求:

上面是一个普通的表单,包含一个文件输入框,下面是要执行的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使用它。