XMLHttpRequest2 に関する新しいヒント

HTML5 Rocks

はじめに

XMLHttpRequest は、HTML5 の世界であまり知られていない優れたオブジェクトの 1 つです。厳密に言うと、XHR2 は HTML5 ではありません。XHR2 は、ブラウザ ベンダーがコア プラットフォームに対して加えている段階的な改良の一部です。私は XHR2 を新しいお楽しみ袋に加えました。XHR2 は今日の複雑なウェブ アプリケーションに関して不可欠な役割を果たすからです。

古い友人が大量の書き換えをしたのですが、多くの人はその新機能について知りません。XMLHttpRequest Level 2 では、ウェブ アプリケーションでの面倒なハック作業に終止符を打つ、cross-origin リクエスト、進捗イベントのアップロード、バイナリ データのアップロード/ダウンロ ドのサポートといった多数の新機能を紹介しています。これらの機能により AJAX は、File System APIWeb Audio API、WebGL などの最新の HTML5 API·の多くと連携して動作するようになりました。

このチュートリアルでは、XMLHttpRequest の新機能の一部、中でもファイルの操作時に使用できる機能に重点を置きます。

データの取得

ファイルをバイナリ blob として取得することは、XHR では容易ではありませんでした。厳密に言えば、可能でさえなかったのです。よく書かれているヒントは、次のようにユーザー定義の charset で 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、または Document として含まれます(responseType に何を設定したかによる)。

このすばらしい新機能を使用して、前の例を書き換えることができます。今度は画像を文字列ではなく ArrayBuffer として取得します。バッファを Blob API に渡して Blob を作成します:

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

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 ソースから複数のビューを作成できます。たとえば、既存の 32 ビット整数配列と同じデータから 8 ビットの整数配列を作成し、同じ 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 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);
}

sendText2('test string');

この例では新しいことは何もありませんが、後のスニペットは少し違います。比較のために responseType='text' を設定しています。この行を省略しても結果は同じです。

フォームの送信: xhr.send(FormData)

多くの皆さんが、jQuery プラグインや他のライブラリを使用して AJAX フォーム送信を処理するのに慣れていると思います。別の方法として、ここでは FormData を使います。これも XHR2 で加わった新しいデータ型です。FormData は、HTML <form> を JavaScript ですばやく作成するのに便利です。そのフォームは、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 API を使用してテキスト ファイルを一から作成し、その 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);
}

Cross Origin Resource Sharing(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 ヘッダー

cross-origin リクエストは簡単に有効化できるので、データを一般公開する場合は、必ず CORS を有効化することを忘れないでください。

cross-domain リクエストの実行

サーバー エンドポイントが CORS を有効化している場合、cross-origin リクエストの実行は、通常の 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 ファイル システムを使用してローカルに保存します。これを実現する 1 つの方法は、画像を ArrayBuffer としてリクエストし、データから Blob をビルドし、FileWriter を使用して blob を書き込みます。

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 = 'arraybuffer';

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();

注: このコードを使用するには、FileSystem API のエクスポートのチュートリアルにあるブラウザ サポートとストレージの制約をご覧ください。

ファイルのスライスと各部分のアップロード

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

    // Note: blob.slice has changed semantics and been prefixed. See http://goo.gl/U9mE5.
    if ('mozSlice' in blob) {
      var chunk = blob.mozSlice(start, end);
    } else {
      var chunk = blob.webkitSlice(start, end);
    }

    upload(chunk);

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

})();

ここでは、サーバー上のファイルを再構成するコードは示していません。

ぜひお試しください。

#bytes/chunk:

参考資料

Comments

0