XMLHttpRequest2 の新しいトリック

はじめに

HTML5 の世界であまり知られていないヒーローの 1 人が XMLHttpRequest です。厳密に言えば、XHR2 は HTML5 ではありません。ただし、これはブラウザ ベンダーがコア プラットフォームに加える段階的な改善の一環です。XHR2 は今日の複雑なウェブアプリで非常に重要な役割を果たすため、Google の新しい特典として搭載しています。

旧友のゲームが改装されましたが、多くのユーザーは新機能に気づいていません。XMLHttpRequest レベル 2 では、クロスオリジン リクエスト、進行状況イベントのアップロード、バイナリデータのアップロードとダウンロードのサポートなど、ウェブアプリの複雑なハッキングを根絶するさまざまな新機能が導入されています。これにより、AJAX を File System APIWeb Audio API、WebGL などの最先端の HTML5 API の多くと連携させることができます。

このチュートリアルでは、XMLHttpRequest の新機能の一部、特にファイルの操作に使用できる機能について説明します。

データを取得しています

XHR では、ファイルをバイナリ blob として取得するのは手間がかかります。技術的には不可能でさえあります十分に説明されている手法の 1 つとして、次のように 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 の新しい responseType プロパティと response プロパティを利用して、データを返す形式をブラウザに伝えます。

xhr.responseType
リクエストを送信する前に、データのニーズに応じて xhr.responseType を「text」、「arraybuffer」、「blob」、「document」に設定します。xhr.responseType = '' を設定する(または省略する)と、レスポンスはデフォルトで「text」に設定されます。
xhr.response
リクエストが成功すると、xhr のレスポンス プロパティには、リクエストされたデータが DOMStringArrayBufferBlob、または DocumentresponseType の設定に応じて)として含まれます。

この新たな機能を利用して、前の例を書き換えることができますが、今回は画像を文字列ではなく 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 型配列を使用して基になるデータの「ビュー」を作成できることです。実際、1 つの ArrayBuffer ソースから複数のビューを作成できます。たとえば、同じデータから既存の 32 ビット整数配列と同じ ArrayBuffer を共有する 8 ビット整数配列を作成できます。基盤となるデータは同じままで、異なる表現を作成するだけです。

たとえば、以下では同じ画像を 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 URL の作成など、さまざまな場所で使用できます。

データの送信

さまざまな形式でデータをダウンロードできることは素晴らしいことですが、これらのリッチ形式をホームベース(サーバー)に送信できなければ意味がありません。XMLHttpRequest により、送信できるデータは DOMString または Document(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 フォーム送信を処理する方法に慣れていると考えられます。代わりに、XHR2 用に設計された別の新しいデータ型である FormData を使用できます。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> を動的に作成し、append メソッドを呼び出して <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 を使用して File または Blob データを送信することもできます。すべての FileBlob であるため、どちらでも機能します。

この例では、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 を使用すると、あるドメインのウェブ アプリケーションから別のドメインにクロスドメイン AJAX リクエストを行うことができます。有効にするのはとても簡単で、サーバーから送信されるレスポンス ヘッダーは 1 つだけで済みます。

CORS リクエストの有効化

たとえば、アプリケーションが example.com に存在し、www.example2.com からデータを pull するとします。通常、このタイプの AJAX 呼び出しを行うと、リクエストは失敗し、ブラウザはオリジンの不一致エラーをスローします。CORS では、ヘッダーを追加するだけで、www.example2.comexample.com からのリクエストを許可するかどうかを選択できます。

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

Access-Control-Allow-Origin は、1 つのサイトの 1 つのリソースまたはドメイン全体で追加できます。すべてのドメインからのリクエストを許可するには、次のように設定します。

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.comwww.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 ファイル システムを使用してローカルに保存するとします。これを実現する 1 つの方法は、画像を 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);

})();

ここには、サーバー上でファイルを再構築するためのコードは示されていません。

参照