第14章: HTTPリクエスト

第11章で述べたように、ワールドワイドウェブ上での通信はHTTPプロトコルを介して行われます。単純なリクエストは次のようになります。

GET /files/fruit.txt HTTP/1.1
Host: eloquentjavascript.net
User-Agent: The Imaginary Browser

これは、eloquentjavascript.netのサーバーからfiles/fruit.txtファイルを要求しています。さらに、このリクエストがHTTPプロトコルのバージョン1.1を使用していることを指定しています。バージョン1.0もまだ使用されており、動作がわずかに異なります。HostUser-Agent行はパターンに従います。含まれる情報を識別する単語で始まり、コロンと実際の情報が続きます。これらは「ヘッダー」と呼ばれます。User-Agentヘッダーは、サーバーにリクエストを行うために使用されているブラウザ(またはその他の種類のプログラム)を伝えます。クライアントが理解できるドキュメントの種類や、クライアントが優先する言語などを指定するために、他の種類のヘッダーが頻繁に送信されます。

上記の依頼を受けると、サーバーは次のレスポンスを送信することがあります。

HTTP/1.1 200 OK
Last-Modified: Mon, 23 Jul 2007 08:41:56 GMT
Content-Length: 24
Content-Type: text/plain

apples, oranges, bananas

最初の行は、HTTPプロトコルのバージョンを再び示し、その後にリクエストの状態が続きます。この場合、ステータスコードは200で、「OK、異常なことは何も起こりませんでした。ファイルをあなたに送ります」という意味です。その後にいくつかのヘッダーが続き、(この場合)ファイルが最後に変更された時刻、ファイルの長さ、ファイルの種類(プレーンテキスト)を示しています。ヘッダーの後には空行があり、その後にファイル自体が続きます。

クライアントがドキュメントを取得したいだけを示すGETで始まるリクエストとは別に、サーバーが何らかの方法で処理すると予想される情報がリクエストと一緒に送信されることを示すPOSTという単語も使用できます。1


リンクをクリックしたり、フォームを送信したり、その他の方法でブラウザに新しいページに移動するように促したりすると、HTTPリクエストを行い、新しいドキュメントを表示するために古いページをすぐにアンロードします。一般的な状況では、これはまさにあなたが望むことであり、それがウェブの従来の動作方法です。しかし、場合によっては、JavaScriptプログラムがページを再読み込みせずにサーバーと通信する必要がある場合があります。たとえば、コンソールの「ロード」ボタンは、ページを離れることなくファイルをロードできます。

そのようなことができるようにするには、JavaScriptプログラム自体がHTTPリクエストを行う必要があります。最新のブラウザは、これのためのインターフェースを提供しています。新しいウィンドウを開く場合と同様に、このインターフェースにはいくつかの制限があります。スクリプトが危険なことを行うのを防ぐために、現在のページが由来するドメインに対してのみHTTPリクエストを行うことが許可されています。


ほとんどのブラウザでは、HTTPリクエストを行うために使用されるオブジェクトは、new XMLHttpRequest()を実行することで作成できます。当初このオブジェクトを発明したInternet Explorerの旧バージョンでは、new ActiveXObject("Msxml2.XMLHTTP")、またはさらに古いバージョンではnew ActiveXObject("Microsoft.XMLHTTP")を使用する必要があります。ActiveXObjectは、さまざまな種類のブラウザアドオンへのInternet Explorerのインターフェースです。私たちはすでに非互換性ラッパーの記述に慣れているので、もう一度そうしましょう。

function makeHttpObject() {
  try {return new XMLHttpRequest();}
  catch (error) {}
  try {return new ActiveXObject("Msxml2.XMLHTTP");}
  catch (error) {}
  try {return new ActiveXObject("Microsoft.XMLHTTP");}
  catch (error) {}

  throw new Error("Could not create HTTP request object.");
}

show(typeof(makeHttpObject()));

ラッパーは、trycatchを使用して、どのものが失敗したかを検出しながら、3つのすべての手法でオブジェクトの作成を試みます。どの方法も機能しない場合(古いブラウザやセキュリティ設定が厳格なブラウザの場合がある)、エラーが発生します。

では、なぜこのオブジェクトはXML HTTPリクエストと呼ばれるのでしょうか?これは少し誤解を招く名前です。XMLは、テキストデータを格納する方法です。HTMLのようにタグと属性を使用しますが、より構造化され、柔軟性があります。独自のデータの種類を格納するには、独自のXMLタグの種類を定義できます。これらのHTTPリクエストオブジェクトには、取得されたXMLドキュメントを処理するためのいくつかの組み込み機能があるため、名前の中にXMLが含まれています。ただし、他の種類のドキュメントも処理でき、私の経験では、非XMLリクエストにも同様に頻繁に使用されています。


HTTPオブジェクトができたので、それを利用して、上記で示した例のようなリクエストを行うことができます。

var request = makeHttpObject();
request.open("GET", "files/fruit.txt", false);
request.send(null);
print(request.responseText);

openメソッドはリクエストの設定に使用されます。この場合、fruit.txtファイルに対するGETリクエストを行うことを選択します。ここで指定されたURLは相対的であり、http://部分やサーバー名は含まれていません。つまり、現在のドキュメントが由来するサーバー上のファイルを検索します。3番目のパラメーターであるfalseについては、後で説明します。openが呼び出された後、sendメソッドを使用して実際のリクエストを行うことができます。リクエストがPOSTリクエストの場合、サーバーに送信するデータ(文字列として)をこのメソッドに渡すことができます。GETリクエストの場合、nullを渡す必要があります。

リクエストが行われた後、リクエストオブジェクトのresponseTextプロパティには、取得されたドキュメントの内容が含まれます。サーバーから送り返されたヘッダーは、getResponseHeader関数とgetAllResponseHeaders関数を使用して検査できます。前者は特定のヘッダーを検索し、後者はすべてのヘッダーを含む文字列を提供します。これらは、ドキュメントに関する追加情報を取得するために、時々役立つ場合があります。

print(request.getAllResponseHeaders());
show(request.getResponseHeader("Last-Modified"));

何らかの理由で、サーバーに送信されるリクエストにヘッダーを追加する場合は、setRequestHeaderメソッドを使用できます。これは、ヘッダーの名前と値を2つの文字列として引数として受け取ります。

例では200だったレスポンスコードは、statusプロパティにあります。何か問題が発生した場合、この分かりにくいコードがそれを示します。たとえば、404は、要求されたファイルが存在しないことを意味します。statusTextには、ステータスの少し分かりやすい説明が含まれています。

show(request.status);
show(request.statusText);

リクエストが成功したかどうかを確認するには、通常、status200と比較するだけで十分です。理論的には、サーバーは状況によってはコード304を返して、ブラウザが「キャッシュ」に保存しているドキュメントの古いバージョンがまだ最新であることを示す可能性があります。しかし、ブラウザは304の場合でもstatus200に設定することで、これらからユーザーを保護しているようです。また、FTPなど、HTTPプロトコルではないプロトコル2を介してリクエストを行う場合、プロトコルがHTTPステータスコードを使用しないため、statusは使用できません。


上記のようにリクエストを行うと、sendへの呼び出しは、リクエストが完了するまで返されません。これは、sendへの呼び出し後にresponseTextが使用可能になり、すぐに使用を開始できるため便利です。しかし、問題があります。サーバーが遅い場合、またはファイルが大きい場合、リクエストを行うのにかなりの時間がかかる場合があります。これが行われている間、プログラムは待機しており、ブラウザ全体が待機することになります。プログラムが終了するまで、ユーザーは何をすることもできません。ページをスクロールすることもできません。高速で信頼性の高いローカルネットワーク上で動作するページでは、このようなリクエストを行うことができるかもしれません。一方、大きく、信頼性が低いインターネット上のページでは、そうするべきではありません。

openの3番目の引数がtrueの場合、リクエストは「非同期」に設定されます。つまり、リクエストはバックグラウンドで行われるため、sendはすぐに返されます。

request.open("GET", "files/fruit.xml", true);
request.send(null);
show(request.responseText);

しかし、少し待って…

print(request.responseText);

「少し待つ」はsetTimeoutなどで実装できますが、より良い方法があります。リクエストオブジェクトにはreadyStateプロパティがあり、その状態を示します。ドキュメントが完全にロードされると4になり、それまではそれより小さい値になります3。この状態の変化に対応するには、オブジェクトのonreadystatechangeプロパティを関数に設定できます。この関数は、状態が変わるたびに呼び出されます。

request.open("GET", "files/fruit.xml", true);
request.send(null);
request.onreadystatechange = function() {
  if (request.readyState == 4)
    show(request.responseText.length);
};

リクエストオブジェクトによって取得されたファイルがXMLドキュメントの場合、リクエストのresponseXMLプロパティには、このドキュメントの表現が保持されます。第12章で説明したDOMオブジェクトのように動作しますが、styleinnerHTMLなど、HTML固有の機能は備えていません。responseXMLはドキュメントオブジェクトを提供し、そのdocumentElementプロパティはXMLドキュメントの外部タグを参照します。

var catalog = request.responseXML.documentElement;
show(catalog.childNodes.length);

このようなXML文書は、サーバーとの間で構造化された情報を交換するために使用できます。タグが他のタグに含まれているというその形式は、単純なプレーンテキストでは表現が難しいものを格納するのに非常に適していることがよくあります。ただし、DOMインターフェースは情報抽出にはやや不向きであり、XML文書は非常に冗長です。「fruit.xml」文書は長く見えますが、内容は「リンゴは赤く、オレンジはオレンジ色で、バナナは黄色い」だけです。


XMLの代替として、JavaScriptプログラマーはJSONと呼ばれるものを考案しました。これは、JavaScriptの値の基本的な表記法を使用して、「階層的な」情報をよりミニマリストな方法で表現します。JSON文書は、単一のJavaScriptオブジェクトまたは配列を含むファイルであり、その中には任意の数の他のオブジェクト、配列、文字列、数値、ブール値、またはnull値が含まれています。例として、「fruit.json」を参照してください。

request.open("GET", "files/fruit.json", true);
request.send(null);
request.onreadystatechange = function() {
  if (request.readyState == 4)
    print(request.responseText);
};

このようなテキストは、eval関数を使用して通常のJavaScript値に変換できます。JavaScriptがオブジェクト(中括弧で囲まれた)をコードブロックと解釈してエラーを発生させる可能性があるため、evalを呼び出す前に、その周囲に括弧を追加する必要があります。

function evalJSON(json) {
  return eval("(" + json + ")");
}
var fruit = evalJSON(request.responseText);
show(fruit);

テキストに対してevalを実行する際には、テキストが任意のコードを実行することを許可することを念頭に置く必要があります。JavaScriptでは自分のドメインへのリクエストしかできないため、通常はどのようなテキストを取得しているかを正確に把握しており、これは問題になりません。ただし、他の状況では安全でない可能性があります。


例 14.1

JavaScriptの値が与えられた場合に、その値のJSON表現を含む文字列を生成するserializeJSONという関数を記述します。数値やブール値などの単純な値は、String関数に渡して文字列に変換できます。オブジェクトと配列は再帰的に処理できます。

配列の認識は、その型が"object"であるため、困難な場合があります。instanceof Arrayを使用できますが、これは独自のウィンドウで作成された配列にのみ有効です。他のウィンドウのArrayプロトタイプを使用する配列の場合、instanceoffalseを返します。簡単な方法は、constructorプロパティを文字列に変換し、それが"function Array"を含むかどうかを確認することです。

文字列を変換する際には、その中の特殊文字をエスケープする必要があります。文字列を二重引用符で囲む場合、エスケープする文字は\"\\\f\b\n\t\r、および\v4です。

function serializeJSON(value) {
  function isArray(value) {
    return /^\s*function Array/.test(String(value.constructor));
  }

  function serializeArray(value) {
    return "[" + map(serializeJSON, value).join(", ") + "]";
  }
  function serializeObject(value) {
    var properties = [];
    forEachIn(value, function(name, value) {
      properties.push(serializeString(name) + ": " +
                      serializeJSON(value));
    });
    return "{" + properties.join(", ") + "}";
  }
  function serializeString(value) {
    var special =
      {"\"": "\\\"", "\\": "\\\\", "\f": "\\f", "\b": "\\b",
       "\n": "\\n", "\t": "\\t", "\r": "\\r", "\v": "\\v"};
    var escaped = value.replace(/[\"\\\f\b\n\t\r\v]/g,
                                function(c) {return special[c];});
    return "\"" + escaped + "\"";
  }

  var type = typeof value;
  if (type == "object" && isArray(value))
    return serializeArray(value);
  else if (type == "object")
    return serializeObject(value);
  else if (type == "string")
    return serializeString(value);
  else
    return String(value);
}

print(serializeJSON(fruit));

serializeStringで使用されている技法は、第10章escapeHTML関数で見たものと似ています。各文字の正しい置換を検索するためにオブジェクトを使用します。"\\\\"など、結果の文字列の各バックスラッシュに2つのバックスラッシュを配置する必要があるため、非常に奇妙に見えるものもあります。

また、プロパティの名前は文字列として引用符で囲まれていることに注意してください。一部のプロパティでは不要ですが、スペースやその他の特殊文字を含むプロパティ名には必要です。そのため、コードは簡単にすべてのプロパティ名を引用符で囲みます。


多数のリクエストを行う場合、当然のことながら、毎回opensendonreadystatechangeの儀式全体を繰り返したくありません。非常に簡単なラッパーは次のようになります。

function simpleHttpRequest(url, success, failure) {
  var request = makeHttpObject();
  request.open("GET", url, true);
  request.send(null);
  request.onreadystatechange = function() {
    if (request.readyState == 4) {
      if (request.status == 200)
        success(request.responseText);
      else if (failure)
        failure(request.status, request.statusText);
    }
  };
}

simpleHttpRequest("files/fruit.txt", print);

この関数は、指定されたURLを取得し、コンテンツとともに第2引数として指定された関数を呼び出します。第3引数が指定されている場合は、それを使用して失敗(200以外のステータスコード)を示します。

より複雑なリクエストを実行できるように、メソッド(GETまたはPOST)、データとしてポストするオプションの文字列、追加ヘッダーを追加する方法などを指定する追加パラメーターを受け入れるように関数を変更できます。引数が多すぎる場合は、第9章で見たように、引数オブジェクトとして渡すことをお勧めします。


一部のウェブサイトでは、クライアントで実行されているプログラムとサーバーで実行されているプログラムの間で集中的な通信が行われています。このようなシステムでは、一部のHTTPリクエストをサーバーで実行される関数の呼び出しと考えることが実用的です。クライアントは、関数を識別するURLにリクエストを行い、引数をURLパラメーターまたはPOSTデータとして渡します。その後、サーバーは関数を呼び出し、結果をJSONまたはXML文書に入れて返します。便利なサポート関数をいくつか記述すれば、サーバー側の関数の呼び出しは、クライアント側の関数の呼び出しとほぼ同じくらい簡単になります…もちろん、結果がすぐに得られない点を除いては。

  1. これらはリクエストの唯一の種類ではありません。文書のコンテンツではなくヘッダーだけをリクエストするためのHEAD、サーバーに文書を追加するためのPUT、文書を削除するためのDELETEもあります。これらはブラウザでは使用されず、Webサーバーでもサポートされないことが多いですが、サーバー側のプログラムを追加してサポートすれば、役立つ場合があります。
  2. XMLHttpRequestの名前の「XML」部分だけが誤解を招くものではありません。このオブジェクトはHTTP以外のプロトコル経由のリクエストにも使用できるため、Requestだけが意味のある部分です。
  3. 0(「初期化されていない」)は、openが呼び出される前のオブジェクトの状態です。openを呼び出すと1(「開いている」)に移動します。sendを呼び出すと2(「送信済み」)に進みます。サーバーが応答すると3(「受信中」)になります。最後に4は「読み込み済み」を意味します。
  4. 改行を表す\nは既に見てきました。\tはタブ文字、\rは「キャリッジリターン」で、一部のシステムでは改行の前または代わりに、行末を示すために使用されます。\b(バックスペース)、\v(垂直タブ)、\f(フォームフィード)は古いプリンターを使用する際に役立ちますが、インターネットブラウザを扱う際にはあまり役立ちません。