uhyohyo.net

JavaScript初級者から中級者になろう

十三章第一回 XMLHTTPRequest

今回は、かなりよく使われるXMLHTTPRequestを紹介します。

新しいといってもわりと昔からある技術ですが、最近まで進化を続けているのでここで紹介します。

略してXHRと呼ばれることもあるこの技術は何かというと、JavaScriptでHTTP通信を行うためのAPIです。Ajaxという言葉を聞いたことがあるかもしれませんが、その根幹をなす技術といっていいでしょう。

リクエストを送る

XHRには、レベル1とレベル2があります。使う上でそんなに意識しなくてもいいですが、レベル1がわりと昔からあった古典的なもので、レベル2は新しめのものであるという認識でよいでしょう。

さて、もっとも基本的に、HTTPリクエストを発行するには、XMLHttpRequestオブジェクトのインスタンスを作ります。

var xhr= new XMLHttpRequest();

そして、openメソッドでリクエストを開始します。このとき、URLメソッドを渡してやります。

メソッドというのは、GETとかPOSTとかです。ほかにDELETE,PUT,HEAD,OPTION,TRACE,CONNECTがありますが、XHRから使うのは、GET,POSTのほかにせいぜいがHEADくらいでしょう。また、CONNECTとTRACEはセキュリティの観点から使えないことになっています。

GETというのはサーバーからデータを送ってもらうということであり、POSTというのは逆にサーバーへデータを送ることであるとされています。例えば、POSTは掲示板に投稿したりとかそういう時に使うそうです。しかし現実的には、GETでもデータを送ることができますし(HTMLのform要素ではmethod属性でgetかpostか選択可能です)、POSTでもサーバーは結果としてデータを送り返してきます。まあ、状況に応じて使い分けて下さい。

具体的にはこうです。

var xhr= new XMLHttpRequest();
xhr.open("GET","/index.html");
          

第一引数がメソッドであり、第二引数はURLです。このように「/」から始まるURLはルートからの絶対パスですね。このサイトの場合、「http://uhyohyo.net/index.html」に該当します。

ちなみに今回は省略していますが、第三引数は非同期フラグ(後述)で、また必要な場合、第四引数にユーザー名、第五引数にパスワードを渡すことが可能です。

さて、これだけだとリクエストは送信されません。リクエストを実際に送信するのがsendメソッドです。

sendには引数が一つあり、リクエストの本文です。リクエストの本文というのは、例えばPOSTメソッドの場合、サーバーに送信するデータです。文字列など(後述)を送信することが可能です。GETやHEADの場合は本文は必要ありませんので、省略できます。

var xhr= new XMLHttpRequest();
xhr.open("GET","/index.html");
xhr.send();
          

これでリクエストが送信されました。

結果を受け取る

リクエストが送信されたのはいいですが、サーバーから戻ってくる結果はどうやって受け取るのでしょうか。基本的には、リクエストには時間がかかるので、非同期です。ここのところおなじみの非同期ですが、つまりコールバックで受け取るということです。

XHRにおいてはイベントを用います。ここで、リクエストが完了して結果が戻ってきたときのイベントはloadです。すなわちこうです。


xhr.addEventListener("load",function(ev){
});
          

addEventListenerの第三引数は省略できます。

また、いちいちaddEventListenerを使わなくてもよい省略形もあります。

xhr.onload=function(ev){
};

こちらのほうが古典的な方法です。onがついていることに注意しましょう。

さて、それではloadイベントが発生したら、リクエストの結果はresponseまたはresponseTextに入っています。これらの違いも後述です。まあTextというくらいですから、今回は結果はテキストです。

ここまでをまとめたサンプルを見てみましょう。ページを開くと、HTMLが表示されたと思います。これは/index.htmlのソースです。すなわち、あのページからHTTP通信によって/index.htmlの内容を取得し、その内容を表示したわけです。

結果の種類

さて、responseとresponseTextの違いですが、それは結果の種類というものが関係しています。

結果をどんな風に受け取りたいかはresponseTypeプロパティに文字列を代入してやることで決定します。デフォルトは空で、その場合基本的には"text"と同様の扱いになります(後述)。いずれの場合でも、結果はresponseプロパティに入っていることになります。

responseTypeには以下の種類の文字列を設定することが可能です。

text
結果をテキストで受け取ります。結果はresponseプロパティのほかresponseTextプロパティでも取得できます。
json
結果はJSONで送られてきて、それを解析したオブジェクトを受け取ります。結果にJSON.parseを適用したものと同じです(詳しくはまた今度)。
arraybuffer
結果をArrayBuffer(十二章第五回)で受け取ります。この方法ならばバイナリデータも受け取れます。
blob
結果をBlob(十二章第五回)で受け取ります。arraybufferとの違いは、Blobのtypeプロパティ(そのデータのMIMEタイプ)が使用できる点です。
document
HTMLまたはXMLのファイルを受け取ります。結果はHTMLDocument(送られてきたHTMLファイルのdocument)またはXMLDocument(六章第一回)で受け取ります。HTMLになるかXMLになるかは、サーバーから送られてきたデータ(のMIMEタイプ)によって自動的に判定されます。HTMLかXMLかに関わらず、responseXMLプロパティでも受け取れます。

なぜresponseプロパティ一つあればいいのにresponseText,responseXMLといった専用プロパティあが用意されているかというと、これはLevel 1時代のなごりです。

responseTypeはLevel 2になって導入されたもので、それ以前はresponseという汎用のプロパティは無く、responseTextとresponseXMLで、使いたいほうを使うという形でした。この名残として、responseTypeが設定されていない(空である)昔のコードでは、textと同じ動作をするので当然ながらresponseTextを使えるほか、responseTypeを"document"にしなくてもresponseXMLは使えます。

リクエストヘッダ

HTTPには、リクエストやレスポンスの付帯状況としてヘッダを送ることができます。例えば、

From: foo@example.com

みたいなやつです。このようなヘッダを設定するには、setRequestHeaderメソッドを使います。ただし、タイミングはopenメソッドのあと、sendする前でなければいけません。

引数は、第一引数にヘッダ名、第二引数に値です。上の例だとこのようになります。

xhr.setRequestHeader("From","foo@example.com");

ちなみに、XHRではわりと多くのヘッダが制限されています。完全に自由にヘッダを操作できるわけではありません。

レスポンスヘッダ

また、HTTPでは、送るとき(リクエスト)以外にも、結果が戻ってくるとき(レスポンス)にもさまざまなヘッダがついています。これを取得するのがgetResponseHeaderです。引数はヘッダ名の一つだけです。

var length=xhr.getResponseHeader("Content-Length");

こういう感じです。

また、全てのヘッダを一度に得るgetAllResponseHeadersメソッド(引数無し)もあります。しかし文字列で次のような感じで帰ってきます。

Content-Type: text/plain;charset=UTF-8
Content-Length: 5

あまり使い所はないかもしれません。

レスポンス

ほかにレスポンスには、あまり使う機会はないと思いますがoverrideMimeTypeというメソッドがあります。これは引数を一つとり、結果のMIMEタイプを書き換えます。

例えば、ただのテキスト(text/plain)で帰ってきたものを

xhr.overrideMimeType("text/html");

のようにすれば、responseTypeを"document"にすることでHTMLとして解析してもらえるでしょう。もっと実用的な例もあるかもしれません。

次に、statusプロパティを紹介します。これは、帰ってきた結果のHTTPステータスコードを数値で得ます。例えば。正常にページが帰ってきたならば大抵は200です。ほかに、403とか404が有名です。注意すべきことは、たとえ404などであっても、サーバーから何かしら結果が帰ってきたならばそれはXMLHttpRequest的には「正常に結果が帰ってきた」ということであるということです。

また、statusTextプロパティは、テキストでの説明です。例えば200なら"OK"とか、404なら"Not Found"です。statusとつなげれば「404 Not Found」みたいな感じにすることもできますね。

readyStateとその他のイベント

さて、上では受信に成功したときとしてloadイベントを紹介しました。ほかには次のようなイベントがあります。

loadstart
リクエストを開始した瞬間。
progress
データを受信しているとき(後述)。
abort
リクエストが中断して失敗しとき。
error
リクエストに失敗したとき。
timeout
タイムアウト(後述)によりリクエストが失敗したとき。
loadend
失敗か成功かにかかわらずリクエストが終了したとき。つまり、loadまたはabort,errorイベントと同じタイミングで発生する。
readystatechange
後述

まとめると、処理完了時点で成功した場合にはloadイベントが発生し、失敗した場合は原因によって、abort(中断)またはtimeout(タイムアウト)またはerror(その他)イベントが発生します。いずれの場合もloadendイベントは発生します。

ちなみに、abortイベントが発生する原因の一つとしては、abortメソッド(引数無し)を呼び出すことで自分から中断させることができます。その場合にも発生します。

404でも成功と判断されるのにリクエストが失敗するとはどういう場合かというと、例えばサーバーが落ちていて接続できないとか、そういう場合です。

成功しても失敗しても何らかの動作をしたい場合には、loadendイベントを利用するのが楽でしょう。ちなみに、loadendイベントにおいて成功か失敗か見分ける方法は、statusプロパティを使う方法があります。成功した場合は何らかのコードが入っていますが、失敗した場合は0になっています。また、まだリクエストしていない場合も0なので注意しましょう。また、失敗した場合にはresponseプロパティがnullになりますが、成功したけれどもnullになる場合も多少あるため(変なJSONが送られてきたとか、サーバーが何も送って来なかった場合など)、statusで判断したほうが確実でしょう。

ちなみに、loadstartイベントが発生するのはsendメソッドが呼ばれたときです。

そして、readystatechangeイベントについてですが、これはreadyStateというプロパティと関連しています。これは「現在の状態」を表す数値で、以下に列挙します。

0 (UNSENT)
まだopenメソッドが呼ばれていない状態。
1 (OPENED)
openメソッドが呼ばれたがsendメソッドは呼ばれていない状態。
2 (HEADER_RECEIVED)
sendが呼ばれた後、HTTPヘッダを全て受信し終えた状態。この状態からstatusプロパティやgetResponseHeaderメソッドなどが利用可能。まだデータ本体は受信していない。
3 (LOADING)
本文のデータを受信中である状態。
4 (DONE)
受信完了したか、リクエストに失敗して終了した状態。

見て分かるように、readyStateが4になるのはloadendイベントと同時といっていいでしょう。

さて、readystatechangeイベントは、その名の通りreadystateが変化したときに発生するイベントです。

実はLevel 1ではこのreadystatechangeイベントしか無かったため、例えばロード完了は、readystatechangeイベントでreadyStateが4になった場合に終了だと判断していました。

今はあえてこのreadystatechangeイベントを使う機会は少ないかもしれません。

最速でstatusを得たい(readyStateが2になったタイミング)とかそういう場合は使い道があるかもしれません。

また、実は、readyStateが変わっていないのにreadystatechangeイベントが発生する場合があります。

たとえば、send()が呼ばれた時点、つまりloadstartと同時にreadystatechangeも起こっています。これは、Level 1からの「歴史的理由」によるそうです。

さて、タイムアウトとは制限時間のようなものです。一定時間のうちに処理が完了しなかった場合サーバーが応答しないと判断され、そこで終了となります。

タイムアウトの設定方法は、timeoutプロパティを使います。これはミリ秒の数値であり、例えば

xhr.timeout=1000;

としたならば1秒以内に処理を完了できないとタイムアウトで失敗するということです。デフォルトは0ですが、0の場合無制限ということになります。

イベントオブジェクト

ところで、イベントですから、イベントオブジェクトが存在します。readystatechangeはただのイベントですが、その他のイベントのイベントオブジェクトはProgressEventといいます。

ちなみに、イベントオブジェクトにはtarget三章第五回)がありますね。この場合、targetはそのXMLHttpRequestオブジェクトになります。

さて、ProgressEventに特有なプロパティは主に3つです。一つ目はloadedです。これは数値で、すでに受信した本文のバイト数です。

次はtotalです。これは、受信すべきデータサイズの全体です。例えば、1024バイトのファイルのうち100バイトをすでに受信したとしたら、loadedは100でtotalは1024ということになります。

ところで、totalのはデータサイズの全体ですが、まだ全部受信していないのにどうしてデータサイズが分かるのでしょう。実は、HTTPヘッダのCotent-Lengthにより、データサイズは先にサーバーから教えてもらっているのです。したがって、readyStateが2以上でないとtotalは使えません。

そこで、totalがすでに判明しているかどうかを表すプロパティとしてlengthComputableがあります。これがtrueならばtotalは利用可能で、falseならばtotalはまだ利用できません(このときtotalは0になっています)。

ProgressEventに特有なプロパティは以上の3つです。

これらが活躍するのは、主に上のprogressイベントでしょう。progressイベントは、0.05秒に1回発生することになっています(例外もたまにあります)。

このprogressイベントを用いてダウンロードの進捗状況を把握すれば、よくある「0%」からはじまって「100%」までいくやつなどが可能でしょう。具体的にはこんな感じです。

var xhr=new XMLHttpRequest();
xhr.open("GET","/index.html");
xhr.addEventListener("progress",function(ev){
  console.log(ev.loaded,ev.total);
});
xhr.send();
          

こんな感じでconsole.logでloadedとtotalを表示してやれば、loadedがだんだん増えていく様子がわかるでしょう。ただ、あまりに小さいファイルだと一瞬で終わってしまって分からないかもしれません。実際には、progress要素などに表示するとよいでしょう。

XMLHttpRequestUpload

さて、イベントと関連して紹介するのがXMLHttpRequestUploadです。これは、XMLHttpRequestオブジェクトとセットになっていて、uploadプロパティに入っています。

このXMLHttpRequestUploadの機能は、イベントを発生することです。発生するイベントの名前はXMLHttpRequestと同じです。

さて、実は、さっきのprogressイベントは、サーバーからこちらへ送られてくるデータを観測するものでした。では、POSTでこちらから巨大なデータを送る場合(後述のFormDataでファイルを送る場合とか)は進捗状況を得ることはできないのでしょうか。

それを可能にするのがこのXMLHttpRequestUploadであり、クライアント(リクエスト元)からサーバーにデータを送る部分に関するイベントを発生してくれます。ただし、sendの引数がないなど送るデータが全くない場合はイベントは一つも発生しないので、注意しましょう。

loadstart, abort, error, timeoutについては、クライアントからサーバーへ送る部分とサーバーからクライアントへデータが送られてくるが細分化されていたりはしないので、通常のXMLHttpRequestのイベントと同じです。つまり、わざわざXMLHttpRequestUploadのほうのイベントを監視することもないでしょう。

また、readystatechangeはありません。残ったload,loadend,progressが、XMLHttpRequestUploadで違う部分です。要するに、XMLHttpRequestUploadの用途は主にprogressイベントを監視することだということです。

それでは、XMLHttpRequestUploadにおけるload,loadend,progressの意味を見て行きましょう。とはいっても、これは単純です。

progressイベントのloaded・totalは、受信済みデータ量・受信する総データ量の代わりに、送信済みデータ量・送信する総データ量に変わっただけです。load・loadendは同じタイミングであり、こちら側から送るデータを全て送った段階で発生します。大きいデータを送信して時間がかかりそうな場合は、これを利用してインターフェースを作ると面白いでしょう。これについても、サンプルを示しておきます。

//変数dataには巨大なデータが入っていると仮定
var xhr=new XMLHttpRequest();
xhr.open("POST","/index.html");
xhr.upload.addEventListener("progress",function(ev){
  console.log(ev.loaded,ev.total);
});
xhr.send(data);
        

クロスオリジンリクエスト

次に、クロスオリジンのリクエストについて解説します。これは、オリジンをまたぐということです。オリジンとは、簡単にいうとドメイン(やポート番号)の概念で、一般にオリジンが同じWebページ(≒同じドメインのWebページ)は所有者が同じであるとみなされます。逆に、JavaScriptのセキュリティにおいては、オリジンが異なるページ(iframeで表示してみたりとか)を操作することは基本的にできません。よそのサイトを自由にいじってパスワードを抜かれるとかしても困るからでしょう。逆に、所有者が同じならば安全だろうということです。

しかし、XHRにおいてはあるオリジンのページから違うオリジンのページへHTTPリクエストを送ることができます。これがクロスオリジンリクエストです。ただし、制限もあります。

具体的にどうすればクロスオリジンのリクエストができるかというと、リクエストを受ける側がHTTPヘッダを追加する必要があります。例えば、

xhr.open("GET","http://example.com/");

という場合には、example.com側が変更を行わないといけないということです。すなわち許可制です。向こう側が許可しているならセキュリティも保たれるだろうという考え方ですね。

具体的には、Access-Control-Allow-Originというヘッダをレスポンスに追加します。これで、XHRを許可するドメインを列挙します。例えば、

Access-Control-Allow-Origin: http://example.com http://uhyohyo.net

のようにします。たとえば、この例だと、example.comとuhyohyo.netにあるWebページからのXHRは許可するということです。また、ドメインを列挙する代わりに「*」とすることもできます。これは、「全てのオリジンからのXHRを許可」ということです。

ここで登場するのが、withCredentialsです。これはクロスオリジンのときに関係するプロパティで、falseまたはtrue、デフォルトはfalseです。

trueになると何が起こるかというと、CookieとかSSL証明書などの情報も一緒に送られるようになります。これらが必要な場合はwithCredentialsをtrueにしましょう。

ただし、注意するのは、このwithCredentialsがtrueの状態で、さらにAccess-Control-Allow-Originが"*"でない場合(ちゃんと列挙してある場合)には、さらに追加のHTTPヘッダが必要になります。

Access-Control-Allow-Credientials: true

このヘッダがないと失敗してしまいます。

さて、クロスオリジンのXHRを試してもらうために、http://uhyohyohyo.sakura.ne.jp/javascript/13_1_test.cgiに、Access-Control-Allow-Origin: *を返すCGIを用意しました。

var xhr=new XMLHttpRequest();
xhr.open("GET","http://uhyohyo.net/javascript/13_1_test.cgi");
xhr.onload=function(){console.log(xhr.response);}
xhr.send();
          

こんな感じのコードで実際に試してみましょう。

匿名リクエスト

XMLHttpRequestの代わりにAnonXMLHttpRequestを使うと「匿名フラグ」が立ちます。

しかし、匿名だからといって身元がバレないとか、そういう機能はありません。あまり使う機会もないことと思います。

何が変わるかというと、まずwithCredentialsはfalse固定で、trueにすることはできなくなります。さらに、リクエスト元のオリジンがリクエスト先に分からないようになります。まあ、あまり使う機会もないでしょう(2014年7月現在、これに対応しているのはOpera12だけでした)。

同期リクエスト

XMLHttpRequestは非同期であるといいました。しかし、同期的なリクエストにすることもできます。

それを決めるのが、openの第三引数です。これはさっき「非同期フラグ」といいました。これがtrueならば非同期になります。今までのは非同期ですから、省略するとtrue扱いということです。わりと珍しいですね。

それではこれをfalseにすると、同期リクエストになります。同期ということはどういうことかというと、リクエストが完了するまでsendメソッドから戻ってこないということになります。この場合イベントはあまり使わないことになるでしょう。

つまり、send()の処理が完了した時点でもう結果が帰ってきているということです。

var xhr=new XMLHttpRequest();
xhr.open("GET","index.html",false);
xhr.send();
console.log(xhr.response);	//←ここでもう結果が手に入る
          

今までのように非同期だと、send()した段階ではまだリクエストを送っただけなので、結果が届いているという保証などありませんから、loadイベントなどを利用する必要があります。

また、同期リクエストだと、通信失敗した際に例外(九章第八回)を投げるので多少やっかいです。まあ、同期だと通信中に何も他の処理ができませんから、普通は非同期を使うことのほうが多いでしょう。

sendで送るデータ

さて、sendでは、POSTの場合などに本文を送ることができました。例えばこうです。

var xhr=new XMLHttpRequest();
xhr.open("POST","sample.cgi");
xhr.send("foo=bar&hoge=piyo");

しかし、実は文字列以外も送ることができます。

sendの引数に、ArrayBufferやBlobを渡すことができます。Blobの場合は、本文のデータのMIMEタイプもしっかり設定して送れます。

他に、HTMLDocumentやXMLのDocumentを渡すこともできます。そうなると、そのソースを送ってくれます。

こうしてみると、JSONはありませんが、上のresponseTypeと似ています。しかし、send用にもう一つ実は渡せるものがあります。それはFormDataです。

実は、HTMLのフォームのデータをPOSTで送る際は、特にファイル送信などが混ざってくると、multipart/form-dataという形式にする必要がありこれは複雑です。

そこで、multipart/form-dataの形式を作ってくれるのがFormDataです。FormDataを使うには、そのインスタンスを作ります。

var data = new FormData();

また、FormDataの第一引数にHTMLFormElement(二章第十二回)を渡すと、その内容を持ったFormDataが得られます。そうでない場合は、最初データは空です。

var data = new FormData(form);

また、こうして作ったFormDataのインスタンスには、さらにappendメソッドでデータを追加することができます。FormDataのデータは基本的にname-value形式です。例えば、

<input name="foo" value="bar">

というinput要素で表されるデータは、名前(name)がfooで値(value)がbarというわけです。これを追加するには、appendメソッドの第一引数に名前、第二引数に値を渡します。つまり上の例の場合こうなります。

data.append("foo","bar");

また、appendメソッドはファイルをデータに追加することができます。これは

<input type="file">

に相当するデータです。この時第一引数は名前で同じですが、第二引数はBlobです(もちろん、Blobを継承しているFileでもいいということに注意しましょう)。そして、第三引数にファイル名を与えることも可能です。すなわちこんな感じです。

//変数blobには何かのBlobが入っている
data.append("foo",blob,"file.txt");
        

このようにして作ったFormDataオブジェクトをsendに渡すと、multipart/form-dataの形式にして送ってくれます。つまり、HTMLのフォーム(enctype属性が"multipart/form-data")を送信したときと同じ形式のデータを送ってくれます。

さて、以上にわたってXMLHttpRequestを解説してきましたが、要するに使い道は、HTMLファイルとは別のデータをロードして使いたいとか、そういうときでしょう。結構使い道はあるので、活用しましょう。