2014/04/07更新

[JS] XHR2の機能を学ぶ

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
今日は、とても便利なXMLHttpRequest version2の内容紹介を、ブログに書きたいと思います。

画像

Special Thanks to https://flic.kr/p/mCxBaM



この記事の目的

この記事は、XHR2の様々な機能を実装することを通して、XHR2について深く学ぶことを目的にしています。 自分はXHR2について便利そうだなぁという印象しか持っておらず、機能面や実装方法は無知でした。 そんな自分に対して、XHR2に対する確かな知識を持ちたいと思い、この記事を執筆することを決めました。



目次




XHR2とは

XHR2とは、XMLHttpRequestのバージョン2です。W3Cではこちらのページで仕様策定が行われています。 XHR2では、XHR1で行えるサーバーとの非同期通信に加え、バイナリーデータを扱えたり、通信の進捗状況を把握できたりと、XHR1よりも機能が強化されています。
それらの有益な機能を、実装可能な形で1つずつご紹介しきます。



XHR2のサポート状況

XHR2は全てのブラウザーで使えるわけではなく、現在はまだ仕様策定段階の機能です。Can I Useのサイトによれば、各ブラウザーのサポート状況は以下の通りです。
画像

引用:http://caniuse.com/#search=xhr

IEとAndroid2.3でのサポート状況が少し微妙ですが、多くのPCブラウザやスマホブラウザで利用することができます。 XHR2サポート対象外のブラウザを少し考慮することで、十分に実務でも利用可能だと思います。



XHR1でのバイナリーデータをダウンロードするハック

XHR1でも画像などのバイナリーデータを、XHRを使って非同期に読み込むことができます。 しかしこの方法は、MimeTypeを上書きして、テキストデータからバイナリーデータを生成するという、かなりトリッキーなコードです。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/labo/xhr2/image1.jpg', true);
// MimeTypeを上書きする
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.onreadystatechange = function (e) {
    if (this.readyState === 4 && this.status === 200) {
        // レスポンスをテキストで受け取る
        var binStr = this.responseText;
        // 1文字ずつバイナリーとして扱う
        var base64 = 'data:image/jpeg,';
        for (var i = 0, len = binStr.length; i < len; i++) {
            var c = binStr.charCodeAt(i);
            // String.fromCharCode(c & 0xff)
            var aByte = c & 0xff;
            base64 += '%' + (aByte <= 0xf ? '0' : '') + aByte.toString(16);
        }
        // 作成したbase64を使って、imgタグを生成する
        var image = document.createElement('img');
        image.src = base64;
        document.body.appendChild(image);
    }
}
xhr.send();
このトリッキーなコードは上手く行く時は良いですが、上手く行かないこともあったりと大変です。 XHR1ではこのようなトリッキーなコードを使いましたが、XHR2ではもっとエレガントにコードを書くことができます。 次節でXHR2を用いてコードを変更してみましょう。



XHR2を用いて画像をダウンロードする

それではXHR2を用いた画像のダウンロードを実装してみましょう。 XHR2ではレスポンスとして受け取るデータ型を色々と指定(仕様書参照)することができます。
画像をダウンロードする場合には、データ型として「ArrayBuffer」または「Blob」を使います。それぞれの方法を試してみましょう。 まずは、ArrayBufferを使った実装です。
// ArrayBufferデータ型で画像データをダウンロードする
var xhr = new XMLHttpRequest();
xhr.open('GET', '/labo/xhr2/image4.jpg', true);
// ここで受け取るデータ型を指定します。
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
    if (this.status === 200) {
        // this.responseにはArrayBufferであるバイト配列が格納されています。
        var uInt8Array = new Uint8Array(this.response);
        // バイト配列からbase64を生成します
        var base64 = 'data:image/jpeg,';
        for (var i = 0, len = uInt8Array.length; i < len; i++) {
            var aByte = uInt8Array[i];
            base64 += '%' + (aByte <= 0xf ? '0' : '') + aByte.toString(16);
        }
        // img要素を作成します
        var image = document.createElement('img');
        image.src = base64;
        document.body.appendChild(image);
    }
};
xhr.send();
実装の途中にUint8Arrayが出てきましたが、これは8ビット符号なし整数値の配列です。 急に出てきてこれどのブラウザでも使えるのと気になったかもしれませんが、XHR2と共に使うことができるようです(参照:Can I Use)。
以上が、ArrayBuffer形式でバイナリーデータを受け取れるという実装でした。

次にBlobデータ型を用いた実装です。こちらもresponseTypeを指定することで利用することができます。
// Blobデータ型で画像データをダウンロードする
var xhr = new XMLHttpRequest();
xhr.open('GET', '/labo/xhr2/image3.jpg', true);
// responseTypeにblobを指定します
xhr.responseType = 'blob';
xhr.onload = function () {
    if (this.status === 200) {
        // responseにはblobデータが格納されています
        var blob = this.response;
        // URLという機能を用いて、blobデータのURL作成、URL有効化を行います。
        var URL = window.URL || window.webkitURL;
        var image = document.createElement('img');
        image.onload = function () {
            URL.revokeObjectURL(this.src);
        }
        image.src = URL.createObjectURL(blob);
        document.body.appendChild(image);
    }
};
xhr.send();
こちらの実装でも新しいURLという機能が出てきました。 URLでは、Blobデータを参照するためのURLを生成したり、そのURLを有効化する機能が提供されています。 ブラウザサポート状況は、XHR2が利用可能なブラウザで同じく利用することができます(参照:Can I Use)。

以上のように、XHR2を使うことで、画像などのバイナリーデータを特別なハックなしに非同期にダウンロードすることができます。
それでは次に、アップロードについて話題を進めたいと思います。XHR2を用いることで、アップロードも自由自在に行うことができます。



XHR2を用いたアップロード(テキスト、フォームデータ)

先ほどはバイナリーデータを扱いましたが、この節ではまずはテキスト文字列やフォームデータをアップロードすることに取り組みます。 他多くのXHR2の記事を読んで自分が疑問に持った点として、アップロードしたデータをサーバー側でどうやって扱っているだろう、ということでした。 その点についても、PHPでの簡易を用いて紹介したいと思います。


テキスト文字列をアップロードする

最も基本となるテキストデータのアップロードを行いましょう。XHR2で実装すると、以下のようになります。
// Client Code
var xhr = new XMLHttpRequest();
xhr.open('POST', '/labo/xhr2/server', true);
xhr.responseType = 'text';
xhr.onload = function () {
    if (this.status === 200) {
        console.log(this.responseText);
    }
}
// 文字列を送信します
xhr.send('From SendTextBtn');
xhr.sendメソッドの引数に、サーバーへ送りたい情報を指定することで、サーバーとの通信を行うことができます。
上記のリクエストを受け取ったサーバーでは、以下のようにテキストデータを受け取ることができます。
// Server Code (PHP)
$contentType = $headers["Content-Type"];
if (strpos($contentType, 'text/plain') !== false) {
    $requestBody = file_get_contents('php://input');
    echo "Thanks you! your post: '$requestBody'";
    return;
}
file_get_contents('php:input')を使って、送られてきたテキストデータを取得しています。 またコードをみると分かりますが、Content-Typetext/plainとなっていることが分かります。 xhr.sendでは、引数の内容に合わせて、自動的にContent-Typeが指定されます。


フォームデータをアップロードする

XHR2を用いるとフォームデータも簡単にアップロードすることができます。以下のように実装します。
// Client Code

// フォームデータを作成します
var formData = new FormData();
formData.append('username', 'yoheim');
formData.append('location', 'japan');

var xhr = new XMLHttpRequest();
xhr.open('POST', '/labo/xhr2/server', true);
xhr.onload = function () {
    if (this.status === 200) {
        console.log(this.responseText);
    }
}
// フォームデータを送ります
xhr.send(formData);
ここではFormDataクラスのインスタンスをxhr.sendの引数に渡すことでフォームデータを送っています。 このように、簡単にフォームデータを送信することができます。 またHTML上のformタグの情報も上記の要領で送信することができます。FormDataのインスタンス生成時に、formを渡します。
// Client Code
var formData = new FormData(document.forms[0]);

このフォームを受け取るサーバーサイドの実装例は以下の通りです。
// Server Code (PHP)
$contentType = $headers["Content-Type"];
if (strpos($contentType, "form-data") !== false) {
    $username = $_POST["username"];
    $location = $_POST["location"];
    $nickname = $_POST['nickname'];
    echo "Thank you for your post. formData[username='$username',location='$location',nickname='$nickname']";
    return;
}
ここではPOSTメソッドで送信しているため、$_POSTから値を取得しています。$_POSTから値を取得する実装は、良くある実装ですね。

さてここまでで、XHR2を使ってテキストデータやフォームデータをサーバーへ送信する方法が分かりました。 次はバイナリーデータを非同期にサーバーへ送信する方法を扱いたいと思います。バイナリーデータとしてファイルを扱います。



バイナリーデータをサーバーへ非同期に送信する

続いては、XHR2を使ってバイナリーデータをサーバーへ送信することに取り組みましょう。ここではバイナリーデータとしてファイルオブジェクトを利用します。
アップロードする方法には、Blobとしてアップロードすると、フォームデータに含めて送るの2種類があります。 それぞれの方法について見ていきましょう。


Blobデータ形式でアップロードする

まずはBlobデータとしてサーバーへ送信する方法です。以下のようなHTMLがあるとします。
<input id="sendFileBtn" type="file" value="ファイルをBlobで非同期にアップロードする"/>
このInput Fileタグでファイルが選択されたら、そのデータをサーバーへ送ってみましょう。
// Client Code
var btn = document.querySelector('#sendFileBtn');
btn.onchange = function () {

    // 選択されたファイルを取得します.
    var file = this.files[0];
    console.log('filename: ', file.name);
    console.log('filesize: ', file.size);
    console.log('filetype: ', file.type);

    // xhrの準備をします.
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/labo/xhr2/server', true);
    xhr.onload = function () {
        if (this.status === 200) {
            console.log('file uploaded: ', this.responseText);
        }
    }

    // Fileを送信します.
    xhr.send(file);
};
xhr.sendには、Fileオブジェクトも指定することができます。 色々なデータ型を受け取ることができて、おぬしやりよるという感じです。 xhr.sendの引数にFileオブジェクトを指定した場合には、リクエストヘッダに付与されるContent-Typeはfile.typeを元に決定されます。
以下のサーバーサイドの実装例は、JPEG画像がアップロードされた場合の実装です。
// Server Code (PHP)

// 今回はJPEGをアップロードしたので、Content-Typeはimage/jpegとなります.
$contentType = $headers["Content-Type"];
if (strpos($contentType, "image/jpeg") !== false) {

    // 保存先ファイルを準備します
    $fileName = 'tmpbox/aaa.jpg';
    if (!file_exists($fileName)) {
        touch($fileName);
    }
    // 「php://input」からアップロードされたバイナリーデータを取得します
    $blob = file_get_contents('php://input');
    $fp = fopen($fileName, "w");
    fwrite($fp, $blob);
    fclose($fp);
    echo "Thank you for your image.";
    return;
}
ただ、Blobデータのみをアップロードする場合には、ファイル名などの必要情報を持っていません。 それらの情報も必要な場合には、次に紹介するフォームデータに含めて送信する方法がよいでしょう。


フォームデータに含めて送る

ここではファイルデータをフォームに含めて、フォームデータをサーバーへ送信する方法です。 この方法を用いることで、ファイル自体のデータ以外にも、ファイル名などのメタ情報も一緒に渡すことができて便利です。
var btn = document.querySelector('#sendFileBtn');
btn.onchange = function () {

    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/labo/xhr2/server', true);
    xhr.onload = function () {
        if (this.status === 200) {
            console.log('file uploaded: ', this.responseText);
        }
    }

    // フォームに含めてFileを送信します.
    var file = this.files[0];
    var formData = new FormData();
    formData.append('filename', file.name);
    formData.append('filetype', file.type);
    formData.append('content', file);
    xhr.send(formData);
};
Fileオブジェクトを、FormDataにappendすることで、バイナリーデータであるファイルを送ることができます。
サーバーでの受け取りは以下のようになります。
// Server Code (PHP)

// フォームデータとして受け取ります.
$contentType = $headers["Content-Type"];
if (strpos($contentType, "form-data") !== false) {

    // 受け取ったファイル名から保存するパスを作成
    $filename = $_POST["filename"];
    $uploadfile = "tmpbox/$filename";

    // アップロードされたファイルの情報は$_FILESに格納されています
    // move_uploaded_fileを使ってアップロードされたファイルを取得します
    $result = move_uploaded_file($_FILES['content']['tmp_name'], $uploadfile);
    if ($result) {
        echo "success";
    } else {
    	# $_FILES[$key]['error']でエラー内容が分かります
        echo "file upload error.";
    }
}
このように、XHR2を使うことで、非同期にファイルなどのバイナリーデータのアップロードを行うことができます。 バイナリーデータを扱うことができる点が、XHR2がXHR1よりも優れている点の1つです。

次の章では、XHR2に関する他の特筆すべき機能についても紹介したいと思います。



その他XHR2の機能

XHR2には、前述のバイナリーデータの扱いの他に、進捗状況の把握タイムアウト通信キャンセルといった機能が存在します。 それらの機能についても、少し触れたいと思います。


進捗状況の把握(ダウンロード)

XHR2では、どれほど処理が進んだのかを把握する機能が存在します。以前紹介した画像をダウンロードする実装を拡張して紹介します。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/labo/xhr2/image3.jpg', true);
xhr.responseType = 'blob';
xhr.onload = function () {
    if (this.status === 200) {
        var blob = this.response;
        // ・・・Blobの処理
    }
};
// ダウンロードの進捗状況を取得します
xhr.onprogress = function (e) {
    // e.loaded: ダウンロード済みのバイトサイズ
    // e.total:  トータルの倍とサイズ
    console.debug('[progress]', e.loaded ,e.total);
}
xhr.send();
xhr.onprogressイベントに監視するための関数を指定することで、進捗状況を把握することが出来ます。 引数のeはProgressEvent(仕様書参照)で、そのプロパティから進捗情報を得ることができます。
小さなサイズのデータであれば、onprogressは1回しか呼び出されませんが、大きなサイズ(2MBとか)になると、 何度か呼び出されて、ダウンロードの進捗状況を把握することができます。


進捗状況の把握(アップロード)

アップロードでもダウンロードと同様に、進捗状況を把握することができます。以前に紹介したフォームデータの送信の実装を拡張してみましょう。
var btn = document.querySelector('#sendFileBtn');
btn.onchange = function () {

    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/labo/xhr2/server', true);
    xhr.onload = function () {
        if (this.status === 200) {
            console.log('file uploaded: ', this.responseText);
        }
    }

    // アップロードの進捗状況を把握
    xhr.upload.onprogress = function (e) {
        console.debug('[uploadProgress]', e.loaded ,e.total);
    };

    // 送信
    var file = this.files[0];
    var formData = new FormData();
    formData.append('filename', file.name);
    formData.append('filetype', file.type);
    formData.append('content', file);
    xhr.send(formData);
};
xhr.upload.onprogressに監視するための関数を指定することで、アップロードの進捗状況を把握することができます。 引数のeは、ダウンロードの時と同じくProgressEventです。
進捗状況を表示できると、ユーザー体験の向上が行うことができ、サイトのクオリティが向上して良いですね!


タイムアウトを設定する

XHR2ではタイムアウトを簡単に設定することができ、またタイムアウトが発生した場合に通知を受け取ることができます。
var xhr = new XMLHttpRequest();
xhr.open('POST', '/labo/xhr2/server' ,true);
xhr.onload = function () {/*成功時の処理*/}

// タイムアウトを設定します
xhr.timeout = 10; // 10ms

// タイムアウト発生時のイベントを受け取ります
xhr.ontimeout = function () {
     alert('timeout!!');
}

// 送信
xhr.send('send message.');
xhr.timeoutにタイムアウト時間(ミリ秒)を指定するだけでタイムアウトを設定することができます。とてもシンプルで強力な機能です。


通信キャンセル

XHR2では、通信のキャンセルを行うことができます。非同期通信をサーバーに送ってみたものの、ユーザー操作により中止したいということも時々存在します。 通信のキャンセルは以下のように行います。
var xhr = new XMLHttpRequest();
xhr.open('POST', '/labo/xhr2/server' ,true);
xhr.onload = function () {/*通信成功時の処理*/}
xhr.send('send message.');

// 通信キャンセルが出来た場合に呼び出されるコールバック
xhr.onabort = function () {
    console.debug('aboted!!');
}

// 通信中断の処理
// ここでは10ms後に通信のキャンセルを行っています。
setTimeout(function () {
    xhr.abort();
}, 10);
xhr.abort()を呼び出すことで、通信処理をキャンセルすることができ、 xhr.onabortに監視するための関数を登録することで、通信キャンセルがされた場合に通知を受けることができます。
この機能で注意すべきことは、処理が既に終わっている通信はキャンセルすることができないという点です。 また、あくまでクライアント上での通信処理のキャンセルのため、サーバー上でなんらかのデータを更新するといった通信の場合には、通信中断には最新の注意を払う必要があります。



参考資料

この記事を書くために、以下の記事を参照しています。

New Tricks in XMLHttpRequest2 - HTML5 Rocks
XMLHttpRequest | W3C
Progress Events | W3C
Can I use... Support tables for HTML5, CSS3, etc
PHP - 画像アップロード処理サンプル集 - Qiita
Blob - Web API Interfaces | MDN
ArrayBuffer - Web API Interfaces | MDN



最後に

今回は、XHR2について詳しく扱いました。 バイナリーデータが扱えること、進捗状況が分かること、などとても便利な機能が多くあることを学ぶことができ、良かったと思っています。 話の途中に出てきたBlobFileは、IndexedDBにも格納することができ、利用用途が多そうな機能だなぁと感じました。

今後も特定の技術を掘り下げて扱う記事をいくつか書ければと思いますし、本職のフロントエンド記事もたくさん書いていきたいと思います。 ぜひ、RSSTwitterへの登録を、よろしくお願いします。 最後までご覧頂きましてありがとうございました。





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。