XMLHttpRequest2 中的新技巧

简介

XMLHttpRequest 是 HTML5 世界中一个不为人知的英雄。严格地说,XHR2 并不属于 HTML5。不过,这是浏览器供应商对核心平台不断做出改进的一部分。我之所以将 XHR2 加入我们新的百宝囊中,是因为它在当今复杂的 Web 应用中扮演了不可或缺的一环。

原来,我们的老朋友进行了重大改造, 但许多人都不知道它的新功能。XMLHttpRequest 级别 2 引入了一系列新功能,以终结 Web 应用中的复杂黑客行为,例如跨源请求、上传进度事件以及对上传/下载二进制数据的支持。这些 API 可让 AJAX 与许多先进的 HTML5 API(例如 File System APIWeb Audio API 和 WebGL)协同工作。

本教程重点介绍了 XMLHttpRequest 中的一些新功能,特别是可用于处理文件的功能。

正在提取数据

使用 XHR 提取二进制 blob 形式的文件一直很痛苦。从技术层面来说,这甚至不可能有一种广为人知的技巧就是使用用户定义的字符集替换 MIME 类型,如下所示。

提取图片的旧方式如下:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);

// Hack to pass bytes through unprocessed.
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
    var binStr = this.responseText;
    for (var i = 0, len = binStr.length; i < len; ++i) {
      var c = binStr.charCodeAt(i);
      //String.fromCharCode(c & 0xff);
      var byte = c & 0xff;  // byte at offset i
    }
  }
};

xhr.send();

虽然这种方法行得通,但 responseText 中实际返回的内容并不是二进制 blob。它是表示图片文件的二进制字符串。 我们要诱使服务器不经处理便传回数据。 虽然这个小技巧确实有效,但我会将其称为“黑魔法”,并给出反对建议。无论何时您利用字符代码黑客和字符串操纵手段将数据强制转换为所需的格式,都是个问题。

指定响应格式

在前面的示例中,我们通过替换服务器的 MIME 类型并将响应文本处理为二进制字符串,将图片下载为二进制“文件”。相反,我们可以利用 XMLHttpRequest 的新 responseTyperesponse 属性来告知浏览器我们希望以哪种格式返回数据。

xhr.responseType
在发送请求之前,请根据您的数据需求将 xhr.responseType 设置为“文本”“数组缓冲区”“blob”或“文档”。请注意,设置(或省略)xhr.responseType = '' 会默认将响应设为“text”。
xhr.response
请求成功后,xhr 的响应属性将包含请求的数据作为 DOMStringArrayBufferBlobDocument(具体取决于 responseType 的设置)。

有了这个新的酷炫功能,我们可以修改上一个示例,不过这次,以 Blob(而非字符串)的形式提取图片:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    // Note: .response instead of .responseText
    var blob = new Blob([this.response], {type: 'image/png'});
    ...
  }
};

xhr.send();

好多了!

ArrayBuffer 响应

ArrayBuffer 是用于二进制数据的通用固定长度容器。如果您需要原始数据的通用缓冲区,它们非常方便,但它们真正的优势在于,您可以使用 JavaScript 类型数组创建底层数据的“视图”。实际上,可以通过单个 ArrayBuffer 来源创建多个视图。例如,您可以创建一个 8 位整数数组,与来自相同数据的现有 32 位整数数组共享同一个 ArrayBuffer。底层数据保持不变,我们只是为其创建不同的表示形式。

例如,以下代码会以 ArrayBuffer 形式提取相同的图片,但这次会从该数据缓冲区创建一个无符号 8 位整数数组:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response); // this.response == uInt8Array.buffer
  // var byte3 = uInt8Array[4]; // byte at offset 4
  ...
};

xhr.send();

Blob 响应

如果您想直接使用 Blob 且/或不需要操作文件的任何字节,请使用 xhr.responseType='blob'

window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    img.onload = function(e) {
      window.URL.revokeObjectURL(img.src); // Clean up after yourself.
    };
    img.src = window.URL.createObjectURL(blob);
    document.body.appendChild(img);
    ...
  }
};

xhr.send();

Blob 可用于许多地方,包括将其保存到 indexedDB、将其写入 HTML5 文件系统创建 Blob 网址,如本例所示。

正在发送数据

能够下载不同格式的数据固然很好,但如果我们无法将这些丰富格式的数据发送回本垒(服务器),那就毫无意义了。XMLHttpRequest 已限制我们发送 DOMStringDocument (XML) 数据一段时间。现在不会了。经过改进的 send() 方法已替换为以下任何类型:DOMStringDocumentFormDataBlobFileArrayBuffer。本部分其余部分中的示例演示了如何使用每种类型发送数据。

发送字符串数据:xhr.send(DOMString)

function sendText(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.responseText);
    }
  };

  xhr.send(txt);
}

sendText('test string');
function sendTextNew(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.responseType = 'text';
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.response);
    }
  };
  xhr.send(txt);
}

sendTextNew('test string');

这没有新内容,只是正确的代码段略有不同。它会设置 responseType='text' 以进行比较。同样,省略该行会得到相同的结果。

提交表单:xhr.send(FormData)

许多人可能习惯于使用 jQuery 插件或其他库来处理 AJAX 表单提交。我们可以使用 FormData,这是为 XHR2 构思的另一种新数据类型。FormData 便于在 JavaScript 中实时创建 HTML <form>。然后可以使用 AJAX 提交该表单:

function sendForm() {
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);
}

从本质上讲,我们只是动态地创建一个 <form>,并通过调用附加方法为其附加 <input> 值。

当然,您无需从头开始创建 <form>FormData 对象可通过页面上现有的 HTMLFormElement 进行初始化。例如:

<form id="myform" name="myform" action="/server">
  <input type="text" name="username" value="johndoe">
  <input type="number" name="id" value="123456">
  <input type="submit" onclick="return sendForm(this.form);">
</form>
function sendForm(form) {
  var formData = new FormData(form);

  formData.append('secret_token', '1234567890'); // Append extra data before send.

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);

  return false; // Prevent page from submitting.
}

HTML 表单可以包含文件上传(例如 <input type="file">),而 FormData 也可以处理这种情况。只需附加文件,浏览器就会在调用 send() 时构造 multipart/form-data 请求:

function uploadFiles(url, files) {
  var formData = new FormData();

  for (var i = 0, file; file = files[i]; ++i) {
    formData.append(file.name, file);
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);  // multipart/form-data
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  uploadFiles('/server', this.files);
}, false);

上传文件或 blob:xhr.send(Blob)

我们也可以使用 XHR 发送 FileBlob 数据。请注意,所有 File 都是 Blob,因此两者皆适用于此。

此示例使用 Blob() 构造函数从头开始创建新的文本文件,并将该 Blob 上传到服务器。该代码还会设置处理程序来通知用户上传的进度:

<progress min="0" max="100" value="0">0% complete</progress>
function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  // Listen to the upload progress.
  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
    }
  };

  xhr.send(blobOrFile);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

上传字节:xhr.send(ArrayBuffer)

最后但同样重要的一点是,我们可以将 ArrayBuffer 作为 XHR 的载荷发送。

function sendArrayBuffer() {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  var uInt8Array = new Uint8Array([1, 2, 3]);

  xhr.send(uInt8Array.buffer);
}

跨域资源共享 (CORS)

CORS 允许一个网域中的 Web 应用向另一个网域发出跨网域 AJAX 请求。启用非常简单,只需由服务器发送一个响应标头即可。

启用 CORS 请求

假设您的应用位于 example.com 上,并且您想要从 www.example2.com 提取数据。通常,如果您尝试进行此类 AJAX 调用,请求会失败,浏览器会抛出来源不匹配错误。借助 CORS,www.example2.com 只需添加一个标头,即可选择允许来自 example.com 的请求:

Access-Control-Allow-Origin: http://example.com

可将 Access-Control-Allow-Origin 添加到某个网站或整个网域中的单个资源中。如需允许任何网域向您发出请求,请设置:

Access-Control-Allow-Origin: *

事实上,此网站 (html5rocks.com) 已在其所有网页上启用了 CORS。启动开发者工具,您将在我们的响应中看到 Access-Control-Allow-Origin

html5rocks.com 上的 Access-Control-Allow-Origin 标头
html5rocks.com 上的“Access-Control-Allow-Origin”标头

启用跨源请求非常简单,因此,如果您的数据是公开的,请务必启用 CORS

发出跨网域请求

如果服务器端点已启用 CORS,则发出跨域请求与常规 XMLHttpRequest 请求并无不同。例如,example.com 现在可以向 www.example2.com 发出一个请求:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

实际示例

将文件下载并保存到 HTML5 文件系统

假设您有一个图库,想要提取一些图片,然后使用 HTML5 文件系统将它们保存在本地。实现此目的的方法之一是以 Blob 的形式请求图片,然后使用 FileWriter 将其写出:

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

function onError(e) {
  console.log('Error', e);
}

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {

  window.requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
    fs.root.getFile('image.png', {create: true}, function(fileEntry) {
      fileEntry.createWriter(function(writer) {

        writer.onwrite = function(e) { ... };
        writer.onerror = function(e) { ... };

        var blob = new Blob([xhr.response], {type: 'image/png'});

        writer.write(blob);

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

分割文件并上传每个部分

利用 File API,我们可以最大限度地减少上传大文件的工作量。这种技术是将上传内容分成多个块,为每个部分生成一个 XHR,然后将这两个文件放在服务器上。这类似于 Gmail 快速上传大附件的方式。使用此类技术也可以规避 Google App Engine 的 32 MB http 请求限制。

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };
  xhr.send(blobOrFile);
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  var blob = this.files[0];

  const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {
    upload(blob.slice(start, end));

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

用于在服务器上重建文件的代码并未在此显示。

参考编号