XMLHttpRequest2 中的新技巧

简介

XMLHttpRequest 是 HTML5 世界中鲜为人知的功臣之一。严格来说,XHR2 不是 HTML5。不过,这只是浏览器供应商对核心平台进行的增量改进的一部分。我将 XHR2 纳入到我们的新工具包中,是因为它在当今复杂的 Web 应用中发挥着重要作用。

事实证明,我们的老朋友经过了彻底改造,但许多人并不知道它的新功能。XMLHttpRequest 级别 2 引入了一系列新功能,这些功能可终结 Web 应用中的复杂黑客攻击,例如跨源请求、上传进度事件以及对上传/下载二进制数据的支持。这些功能使 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 设置为“text”“arraybuffer”“blob”或“document”。请注意,设置 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 的 32MB 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);

})();

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

参考