XMLHttpRequest2 の新しいトリック

はじめに

HTML5 の世界であまり知られていないヒーローの 1 人が XMLHttpRequest です。厳密に言えば、XHR2 は HTML5 ではありません。ただし、これはブラウザ ベンダーがコア プラットフォームに行っている段階的な改善の一環です。XHR2 は、今日の複雑なウェブアプリに不可欠な要素であるため、新しい機能のセットに加えています。

実は、この古い機能は大幅に刷新されていますが、その新機能に気付いていないユーザーが多いようです。XMLHttpRequest Level 2 では、クロスオリジン リクエスト、アップロード プログレス イベント、バイナリ データのアップロード/ダウンロードのサポートなど、ウェブアプリの複雑なハックを排除する新しい機能が多数導入されています。これにより、AJAX は File System APIWeb Audio API、WebGL など、最先端の HTML5 API の多くと連携して動作できるようになります。

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

データを取得しています

XHR では、ファイルをバイナリ ブロブとして取得するのが困難でした。技術的には、不可能でした。よく文書化されているトリックの 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 の response プロパティに、リクエストされたデータが 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 型配列を使用して基盤となるデータの「ビュー」を作成できることです。実際、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)データの送信に制限がありました。今はそうではありません。DOMStringDocumentFormDataBlobFileArrayBuffer のいずれかのタイプを受け付けるように、改良された send() メソッドがオーバーライドされました。このセクションの残りの部分の例では、各タイプを使用してデータを送信する方法を示します。

文字列データの送信: 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 からデータを取得するとします。通常、このタイプの 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.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);

})();

ここに示していないのは、サーバーでファイルを再構築するコードです。

参照