Please note, this is a STATIC archive of website developer.mozilla.org from November 2016, cach3.com does not collect or store any user information, there is no "phishing" involved.

使用JavaScript发送表单

正如我在上一篇文章中讲到的,用户填写HTML表单并发送的过程,实际上就是用户以一种更直观的方式配置一个HTTP请求并发送的过程.但在一些情况下,我们需要不依赖表单仅用JavaScript来发送这样的HTTP请求.下面讲几种实现该需求的办法,让我们看看.

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

随着开放Web应用程序的兴起,不使用基本表单功能已经变得很普遍了。 越来越多的开发者试着掌控数据传输。

获得整体界面的控制

去掌控原本数据传输的方式的主要原因是保持用户界面(UI)的操作. 正规的HTML表单的使用需要在提交表单后加载页面。也是就是说,整个页面会被刷新。 如今诸多用户界面中,我们需要避免一些事情给用户一个顺畅的体验,比如我们可以移除页面闪烁或者隐藏网络延迟。

为了获得那个目标,当下许多UI使用HTML表单仅仅是为了获得用户数据。一旦用户准备提交数据,程序会在后天异步地发送数据,处理用户界面来改变那些需要改变的部分。

异步地发送任何数据长久以来一直被叫做AJAX, 是 "Asynchronous JavaScript And XML"的首字母缩写。

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

AJAX 技术主要依靠 XMLHttpRequest (XHR) DOM 对象. 那是一个强大的工具让你来构造HTTP请求,发送它,并获得响应结果。

Note: There is other AJAX technics that do not rely on the XMLHttpRequest DOM object. For example, a mix of JSONP and the use of the eval function can be used. It's working but it is not recommended to use such technics because they can lead to serious security issues. They have been elaborate to polyfill the lack of support for XMLHttpRequest or JSON in legacy browsers. You should avoid such technics.

创建之初, XMLHttpRequest 被提出是打算将 XML 做为传输数据的格式. 但是,随着他的广泛应用, JSON 取代了 XML. There is no good or wrong format here. JSON is a lightweight structured format where XML is a more semantic format. On the one hand, JSON will be a better choice if you need to reduce the network footprint; on the other hand, XML will offer you more information about the data themselves and their structure. The choice is up to you.

Still, in both case, such a data structure does not fit the structure of form data requests. In their most basic form, form data requests are a URL encoded list of key/value pair. In case of binary data, it's the whole HTTP request that is reshaped in order to handle this.

So, in many case, it's not a problem because a developer control both the front-end (which is executed in the browser) and the back-end (which is executed on the web server) therefore, it's possible to define the data structure on both side of the application.

Unfortunately, if you want to use a third party service, it's not that easy and sometimes, you have to deal with services that accept data as a form data request only. There are also cases where it's just simpler to deal with form data. For example if data are key/value pair or if you want to send binary data because, in the back-end world, there is a full stack of tools that are already available to handle that sort of data.

So how is it possible to send such data?

发送表单数据

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

DOM和iFrame

实现异步请求最古老的方式就是使用JavaScript在一个隐藏的iframe中构造一个表单并提交的过程.如果你需要获得服务器返回的响应,还得在iframe页面重新加载完成再次访问这个iframe.或者在iframe中使用JavaScript操作它的parent页面中的元素,或者执行某个函数.

警告: 这项技术有很多缺点,你应该避免去使用它.It's a potential security risk with third party services because it's an open door to script injection attacks. If you use HTTPS, it can have effects on the same origin policy, which can make the content of an iFrame unreachable.

下面是个简单的例子:

<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 = "https://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);
}

在线演示:

XHR1 all by hand

Definitely, XMLHttpRequest remain the safest and most reliable way to handle HTTP request. To send form data with XMLHttpRequest, it requires to perform all the data encoding (data must be URL encoded) and to know the specificity of the form data requests.

注: 如果你想要了解更多关于XMLHttpRequest使用的知识,这里有两篇很少的文章:An introductory article to AJAX 和更高级点的using XMLHttpRequest.

还是上一节的这个例子:

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

HTML部分不动,下面是新的sendData函数:

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var urlEncodedData = "";

  // 将对象类型的数据转换成URL字符串
  for(name in data) {
    urlEncodedData += name + "=" + data[name] + "&";
  }

  // 删除掉最后的"&"字符
  urlEncodedData = urlEncodedData.slice(0, -1);

  // 将URL字符串进行百分号编码(UTF-8)
  urlEncodedData = encodeURIComponent(urlEncodedData);

  // encodeURIComponent函数多编码了一些字符,我们需要还原.
  urlEncodedData = urlEncodedData.replace('%20','+').replace('%3D','=');

  // 定义数据成功发送并返回后执行的操作
  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', 'https://ucommbieber.unl.edu/CORS/cors.php');

  // 根据HTTP协议,我们要添加一些POST请求提交表单时需要的请求头
  XHR.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
  XHR.setRequestHeader('Content-Length', urlEncodedData.length);

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

在线演示:

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

XHR2和FormData对象

手动构建HTTP请求时发送的数据的确比较麻烦.幸运的是,最近新的XMLHttpRequest规范中提供了能够更方便更强大的处理表单数据请求的方法,那就是使用formData对象.

formData对象的用法大概有两种:手动添加进一系列要发送的数据之后发送,或者是直接把表单中的数据导入该formData对象中,然后发送.这两种方式还可以结合起来.

需要注意的是,formData对象是"只写"的,也就是说,你可以把数据添加到该对象中,但你不能从该对象中获取到所包含的值.

使用FormData对象一文中也讲了该对象的详细使用方法,这里也有两个简单的例子:

向FormData对象中手动添加数据

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

HTML部分不动,下面是新的sendData函数:

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', 'https://ucommbieber.unl.edu/CORS/cors.php');

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

在线演示:

使用一个表单元素初始化FormData对象

下面是一个普通的表单:

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

使用异步AJAX来发送这个表单.

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", "https://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代码.

// 注册load事件处理函数.
window.addEventListener('load', function () {

  // This variables will be used to store the form data
  var text = document.getElementById("i1");;
  var file = {
        dom    : document.getElementById("i2"),
        binary : null,
      };
 
  // 使用FileReader API来访问文件内容
  var reader = new FileReader();

  // 应为FileReader API是异步的,我们需要把读取到的内容存储下来
  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]);
  });

  // sendData函数是核心函数
  function sendData() {
    // At first, there is a file selected, we have to wait it is read
    // If it is not, we 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 HMLHttpRequest instance
    var XHR      = new XMLHttpRequest();

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

    // And we'll store our body request as a string.
    var data     = "";

    // So, if the user has selected a file
    if (file.dom.files[0]) {
      // We start a new part in our body's request
      data += "--" + boundary + "\r\n";

      // We said it's form data (it could be something else)
      data += 'content-disposition: form-data; '
      // We define the name of the form data
            + 'name="'         + file.dom.name          + '"; '
      // We provide the real name of the file
            + 'filename="'     + file.dom.files[0].name + '"\r\n';
      // We provide the mime type of the file
      data += 'Content-Type: ' + file.dom.files[0].type + '\r\n';

      // There is always a blank line between the meta-data and the data
      data += '\r\n';
      
      // We happen the binary data to our body's request
      data += file.binary + '\r\n';
    }

    // For text data, it's simpler
    // We start a new part in our body's request
    data += "--" + boundary + "\r\n";

    // We said it's form data and give it a name
    data += 'content-disposition: form-data; name="' + text.name + '"\r\n';
    // There is always a blank line between the meta-data and the data
    data += '\r\n';

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

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

    // We define what will happen if the data are successfully sent
    XHR.addEventListener('load', function(event) {
      alert('Yeah! Data sent and response loaded.');
    });

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

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

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

    // And finally, We send our data.
    // Due to Firefox's bug 416178, it's required to use sendAsBinary() instead of send()
    XHR.sendAsBinary(data);
  }

  // 获取表单
  var form   = document.getElementById("myForm");

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

在线演示:

总结

Depending, on the browser you want to target, sending  form data through JavaScript can be easy or really difficult. The formData object is the answer to all your troubles, do not hesitate to polyfill it on legacy browser:

文档标签和贡献者

 此页面的贡献者: ziyunfei, sanxingming, helloguangxue, teoli
 最后编辑者: ziyunfei,