はじめに
XMLHttpRequest
は、HTML5 の世界であまり知られていない優れたオブジェクトの 1 つです。厳密に言うと、XHR2 は HTML5 ではありません。XHR2 は、ブラウザ ベンダーがコア プラットフォームに対して加えている段階的な改良の一部です。私は XHR2 を新しいお楽しみ袋に加えました。XHR2 は今日の複雑なウェブ アプリケーションに関して不可欠な役割を果たすからです。
古い友人が大量の書き換えをしたのですが、多くの人はその新機能について知りません。XMLHttpRequest Level 2 では、ウェブ アプリケーションでの面倒なハック作業に終止符を打つ、cross-origin リクエスト、進捗イベントのアップロード、バイナリ データのアップロード/ダウンロ ドのサポートといった多数の新機能を紹介しています。これらの機能により AJAX は、File System API、Web 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 の応答プロパティに、リクエストしたデータが
DOMString
、ArrayBuffer
、Blob
、またはDocument
として含まれます(responseType
に何を設定したかによる)。
このすばらしい新機能を使用して、前の例を書き換えることができます。今度は画像を文字列ではなく ArrayBuffer
として取得します。バッファを BlobBuilder
API に渡して Blob
を作成します:
BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder; var xhr = new XMLHttpRequest(); xhr.open('GET', '/path/to/image.png', true); xhr.responseType = 'arraybuffer'; xhr.onload = function(e) { if (this.status == 200) { var bb = new BlobBuilder(); bb.append(this.response); // Note: not xhr.responseText var blob = bb.getBlob('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()
メソッドは、次のすべてのタイプを受け入れるように変更されました: DOMString
、Document
、FormData
、Blob
、File
、ArrayBuffer
。このセクションの残りの部分では、それぞれのタイプを使用してデータを送信する例を示します。
文字列データの送信: 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
データを送信することもできます。すべての File
は Blob
であることに注意してください。したがって、ここではどちらを使ってもかまいません。
この例は、BlobBuilder
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); } // Take care of vendor prefixes. BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder; var bb = new BlobBuilder(); bb.append('hello world'); upload(bb.getBlob('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
が含まれています。
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 bb = new BlobBuilder(); bb.append(xhr.response); writer.write(bb.getBlob('image/png')); }, onError); }, onError); }, onError); }; xhr.send();
注: このコードを使用するには、FileSystem API のエクスポートのチュートリアルにあるブラウザ サポートとストレージの制約をご覧ください。
ファイルのスライスと各部分のアップロード
File API を使用すると、最小限の作業で大きいファイルをアップロードすることができます。これは、アップロードを複数のチャンクにスライスしてから部分ごとに XHR を作成し、ファイルにしてサーバーに保存するというテクニックです。これは、GMail で大きい添付ファイルがきわめて高速でアップロードされるしくみに似ています。このようなテクニックを使用して、Google App Engine の 32 MB という http リクエストの上限を回避することができます。
window.BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder; 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); })();
ここでは、サーバー上のファイルを再構成するコードは示していません。
ぜひお試しください。