はじめに
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);
})();
ここでは、サーバー上のファイルを再構成するコードは示していません。
ぜひお試しください。