第3版が公開されました。こちらで読んでください!

第17章
HTTP

ウェブの背後にある夢は、情報を共有することでコミュニケーションをとる共通の情報空間です。その普遍性は不可欠です。ハイパーテキストリンクが、個人的なもの、ローカルなもの、グローバルなもの、下書きのもの、高度に洗練されたものなど、何でも指し示せるという事実です。

ティム・バーナーズ=リー、The World Wide Web: A very short personal history

Hypertext Transfer Protocol第12章で既に言及されています)は、ワールドワイドウェブでデータが要求され、提供されるメカニズムです。この章では、プロトコルをより詳細に説明し、ブラウザのJavaScriptがどのようにアクセスできるかを説明します。

プロトコル

ブラウザのアドレスバーにeloquentjavascript.net/17_http.htmlと入力すると、ブラウザはまずeloquentjavascript.netに関連付けられたサーバーのアドレスを検索し、HTTPトラフィックのデフォルトポートであるポート80でTCP接続を開こうとします。サーバーが存在し、接続を受け入れる場合、ブラウザは次のようなものを送信します。

GET /17_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name

その後、サーバーは同じ接続を介して応答します。

HTTP/1.1 200 OK
Content-Length: 65585
Content-Type: text/html
Last-Modified: Wed, 09 Apr 2014 10:48:09 GMT

<!doctype html>
... the rest of the document

ブラウザは、空行の後の応答の部分を取得し、HTMLドキュメントとして表示します。

クライアントによって送信される情報は、リクエストと呼ばれます。これは次の行で始まります。

GET /17_http.html HTTP/1.1

最初の単語はリクエストのメソッドです。GETは、指定されたリソースを取得したいことを意味します。他の一般的なメソッドには、リソースを削除するDELETE、リソースを置き換えるPUT、リソースに情報を送信するPOSTがあります。サーバーは受信したすべてのリクエストを実行する義務がないことに注意してください。ランダムなウェブサイトに近づいて、メインページをDELETEするように指示しても、おそらく拒否されます。

メソッド名の後の部分は、リクエストが適用されるリソースのパスです。最も単純なケースでは、リソースはサーバー上のファイルですが、プロトコルではファイルである必要はありません。リソースは、あたかもファイルであるかのように転送できるものであれば何でもかまいません。多くのサーバーは、生成する応答をその場で生成します。たとえば、twitter.com/marijnjhを開くと、サーバーはmarijnjhという名前のユーザーをデータベースで検索し、見つかった場合は、そのユーザーのプロファイルページを生成します。

リソースパスの後、リクエストの最初の行はHTTP/1.1を指定して、使用しているHTTPプロトコルのバージョンを示しています。

サーバーの応答もバージョンで始まり、次に応答のステータスが、まず3桁のステータスコードとして、次に人間が読める文字列として続きます。

HTTP/1.1 200 OK

2で始まるステータスコードは、リクエストが成功したことを示します。4で始まるコードは、リクエストに何か問題があったことを意味します。404はおそらく最も有名なHTTPステータスコードであり、リクエストされたリソースが見つからなかったことを意味します。5で始まるコードは、サーバーでエラーが発生し、リクエストに責任がないことを意味します。

リクエストまたは応答の最初の行の後には、任意の数のヘッダーが続く場合があります。「name: value」形式の行で、リクエストまたは応答に関する追加情報を指定します。これらのヘッダーは、例として示した応答の一部でした。

Content-Length: 65585
Content-Type: text/html
Last-Modified: Wed, 09 Apr 2014 10:48:09 GMT

これにより、応答ドキュメントのサイズとタイプがわかります。この場合、65,585バイトのHTMLドキュメントです。また、そのドキュメントが最後に変更された日時も示しています。

ほとんどの場合、クライアントまたはサーバーはリクエストまたは応答に含めるヘッダーを決定しますが、いくつかのヘッダーは必須です。たとえば、ホスト名を指定するHostヘッダーは、サーバーが単一のIPアドレスで複数のホスト名をサービスしている可能性があるため、リクエストに含める必要があります。このヘッダーがないと、サーバーはクライアントがどのホストに接続しようとしているのかわかりません。

ヘッダーの後、リクエストと応答の両方には、空行とそれに続く本文を含めることができ、本文には送信されるデータが含まれています。GETDELETEリクエストはデータを送信しませんが、PUTPOSTリクエストは送信します。同様に、エラー応答など、一部の応答タイプには本文は必要ありません。

ブラウザとHTTP

例で見たように、ブラウザはアドレスバーにURLを入力するとリクエストを行います。結果として得られるHTMLページが画像やJavaScriptファイルなどの他のファイルを参照する場合、それらもフェッチされます。

中程度の複雑さを持つウェブサイトには、簡単に10から200のリソースが含まれる可能性があります。それらを迅速にフェッチできるようにするために、ブラウザは一度に複数のリクエストを行い、応答を1つずつ待つのではなく、同時に行います。このようなドキュメントは常にGETリクエストを使用してフェッチされます。

HTMLページにはフォームが含まれている場合があり、ユーザーは情報を記入してサーバーに送信できます。これはフォームの例です。

<form method="GET" action="example/message.html">
  <p>Name: <input type="text" name="name"></p>
  <p>Message:<br><textarea name="message"></textarea></p>
  <p><button type="submit">Send</button></p>
</form>

このコードは、名前を尋ねる小さなフィールドと、メッセージを書き込む大きなフィールドの2つのフィールドを持つフォームを記述しています。「送信」ボタンをクリックすると、これらのフィールドの情報はクエリ文字列にエンコードされます。<form>要素のmethod属性がGETの場合(または省略されている場合)、そのクエリ文字列はactionURLに追加され、ブラウザはそのURLにGETリクエストを行います。

GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

クエリ文字列の先頭は疑問符で示されます。その後、フォームフィールド要素のname属性とそれらの要素の内容に対応する名前と値のペアが続きます。アンパサンド文字(&)はペアを区切るために使用されます。

前のURLにエンコードされた実際のメッセージは「Yes?」ですが、疑問符は奇妙なコードに置き換えられています。クエリ文字列の一部の文字はエスケープする必要があります。%3Fとして表される疑問符はその1つです。すべての形式には独自の文字エスケープ方法が必要という暗黙のルールがあるようです。URLエンコーディングと呼ばれるこの方法は、パーセント記号の後に、文字コードをエンコードする2桁の16進数が続きます。この場合、10進数で63である3Fは、疑問符文字のコードです。JavaScriptは、この形式をエンコードおよびデコードするためにencodeURIComponent関数とdecodeURIComponent関数を提供します。

console.log(encodeURIComponent("Hello & goodbye"));
// → Hello%20%26%20goodbye
console.log(decodeURIComponent("Hello%20%26%20goodbye"));
// → Hello & goodbye

前に見た例でHTMLフォームのmethod属性をPOSTに変更すると、フォームを送信するために実行されるHTTPリクエストはPOSTメソッドを使用し、クエリ文字列をURLに追加するのではなく、リクエストの本文に配置します。

POST /example/message.html HTTP/1.1
Content-length: 24
Content-type: application/x-www-form-urlencoded

name=Jean&message=Yes%3F

慣例により、GETメソッドは、検索など、副作用のないリクエストに使用されます。新しいアカウントの作成やメッセージの投稿など、サーバー上の何かを変更するリクエストは、POSTなどの他のメソッドで表現する必要があります。ブラウザなどのクライアント側のソフトウェアは、POSTリクエストを盲目的に行うべきではないことを知っていますが、多くの場合、暗黙的にGETリクエストを行います。たとえば、ユーザーがすぐに必要になると考えるリソースをプリフェッチする場合などです。

次の章では、フォームに戻り、JavaScriptを使用してどのようにスクリプト化できるかについて説明します。

XMLHttpRequest

ブラウザのJavaScriptがHTTPリクエストを行うためのインターフェースはXMLHttpRequest(大文字と小文字が不規則なことに注意)と呼ばれます。これは、1990年代後半にMicrosoftによってInternet Explorerブラウザ用に設計されました。この間、XMLファイル形式はビジネスソフトウェアの世界で非常に人気がありました。Microsoftが常に中心的な地位を占めていた世界です。実際、それは非常に人気があったため、HTTPのインターフェースの名前の前にXMLという略語が付けられました。これは、XMLとは何の関係もありません。

ただし、名前が完全に無意味というわけではありません。このインターフェースを使用すると、必要に応じて応答ドキュメントをXMLとして解析できます。別々の2つの概念(リクエストの実行と応答の解析)を1つのものにすることは、もちろんひどい設計ですが、そうなのです。

XMLHttpRequestインターフェースがInternet Explorerに追加されたとき、それ以前は非常に困難だったことをJavaScriptで実行できるようになりました。たとえば、ウェブサイトは、ユーザーがテキストフィールドに入力しているときに候補のリストを表示し始めました。スクリプトは、ユーザーが入力している間にテキストをHTTP経由でサーバーに送信します。可能な入力のデータベースを持つサーバーは、部分的な入力に対してデータベースのエントリを照合し、ユーザーに表示する候補の補完を送信します。これは素晴らしいと考えられました。人々は、ウェブサイトとのあらゆるやり取りでページ全体のリロードを待つことに慣れていたからです。

当時、もう1つの重要なブラウザであるMozilla(後のFirefox)は、遅れを取りたくありませんでした。そのブラウザで同様に素晴らしいことをできるようにするために、Mozillaは不正確な名前を含むインターフェースをコピーしました。次世代のブラウザはこの例に続き、今日ではXMLHttpRequestは事実上の標準インターフェースです。

リクエストの送信

単純なリクエストを行うには、XMLHttpRequestコンストラクタを使用してリクエストオブジェクトを作成し、そのopenメソッドとsendメソッドを呼び出します。

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", false);
req.send(null);
console.log(req.responseText);
// → This is the content of data.txt

openメソッドはリクエストを設定します。この例では、example/data.txtファイルに対するGETリクエストを行うように選択しています。プロトコル名(例えばhttp:)で始まらないURLは相対URLです。これは、現在のドキュメントを基準に解釈されることを意味します。スラッシュ(/)で始まる場合、現在のパス(サーバー名の後の部分)を置き換えます。スラッシュで始まらない場合、現在のパスの最後のスラッシュ文字までを含む部分が、相対URLの前に置かれます。

リクエストを開いた後、sendメソッドで送信できます。sendへの引数はリクエストボディです。GETリクエストの場合、nullを渡すことができます。openの第3引数がfalseの場合、sendはリクエストへのレスポンスが受信されるまで返却しません。リクエストオブジェクトのresponseTextプロパティを読み取ることで、レスポンスボディを取得できます。

レスポンスに含まれるその他の情報も、このオブジェクトから抽出できます。ステータスコードはstatusプロパティから、人間が読み取れるステータステキストはstatusTextプロパティからアクセスできます。ヘッダーはgetResponseHeaderで読み取ることができます。

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", false);
req.send(null);
console.log(req.status, req.statusText);
// → 200 OK
console.log(req.getResponseHeader("content-type"));
// → text/plain

ヘッダー名は、大文字と小文字を区別しません。「Content-Type」のように、通常は各単語の先頭が大文字で記述されますが、「content-type」や「cOnTeNt-TyPe」も同一のヘッダーを参照します。

ブラウザは、「Host」など、サーバーがボディのサイズを判断するために必要なリクエストヘッダーを自動的に追加します。しかし、setRequestHeaderメソッドで独自のヘッダーを追加することもできます。これは高度な用途でのみ必要であり、通信しているサーバーとの連携が必要です。サーバーは、処理方法がわからないヘッダーを無視できます。

非同期リクエスト

これまで見てきた例では、sendの呼び出しが返された時点でリクエストが終了します。これは、responseTextなどのプロパティがすぐに使用できるため便利です。しかし、ブラウザとサーバーが通信している間、プログラムが中断されることも意味します。接続が悪い場合、サーバーが遅い場合、またはファイルが大きい場合、かなり時間がかかる可能性があります。さらに悪いことに、プログラムが中断されている間はイベントハンドラが起動できないため、ドキュメント全体が応答しなくなります。

openの第3引数にtrueを渡すと、リクエストは非同期になります。これは、sendを呼び出すと、すぐに実行されるのはリクエストの送信がスケジュールされることだけです。プログラムは続行でき、ブラウザはバックグラウンドでデータの送受信を処理します。

しかし、リクエストが実行されている間は、レスポンスにアクセスできません。データが使用可能になったときに通知するメカニズムが必要です。

そのため、リクエストオブジェクトで"load"イベントをリスンする必要があります。

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", true);
req.addEventListener("load", function() {
  console.log("Done:", req.status);
});
req.send(null);

第15章requestAnimationFrameの使用と同様に、これは非同期プログラミングスタイルを使用することを強制します。リクエスト後に実行する必要があるものを関数でラップし、適切なタイミングで呼び出されるように配置します。後でこれに戻ります。

XMLデータの取得

XMLHttpRequestオブジェクトによって取得されたリソースがXMLドキュメントの場合、オブジェクトのresponseXMLプロパティには、このドキュメントの解析済み表現が格納されます。この表現は第13章で説明したDOMと非常によく似ていますが、styleプロパティのようなHTML固有の機能は備えていません。responseXMLが保持するオブジェクトはdocumentオブジェクトに対応します。そのdocumentElementプロパティは、XMLドキュメントの外部タグを参照します。次のドキュメント(example/fruit.xml)では、それは<fruits>タグになります。

<fruits>
  <fruit name="banana" color="yellow"/>
  <fruit name="lemon" color="yellow"/>
  <fruit name="cherry" color="red"/>
</fruits>

このようなファイルは次のように取得できます。

var req = new XMLHttpRequest();
req.open("GET", "example/fruit.xml", false);
req.send(null);
console.log(req.responseXML.querySelectorAll("fruit").length);
// → 3

XMLドキュメントは、サーバーと構造化された情報を交換するために使用できます。タグが他のタグの中にネストされた形式は、ほとんどの種類のデータの格納に適しています。少なくとも、プレーンテキストファイルよりは優れています。ただし、DOMインターフェースは情報の抽出にはやや扱いにくく、XMLドキュメントは冗長になりがちです。プログラムと人間の両方にとって読み書きが容易なJSONデータを使用して通信する方が、多くの場合良い方法です。

var req = new XMLHttpRequest();
req.open("GET", "example/fruit.json", false);
req.send(null);
console.log(JSON.parse(req.responseText));
// → {banana: "yellow", lemon: "yellow", cherry: "red"}

HTTPサンドボックス化

ウェブページスクリプトでHTTPリクエストを行うことは、セキュリティに関する懸念を再び提起します。スクリプトを制御する人物は、それが実行されているコンピュータの所有者の利害と一致しない可能性があります。具体的には、themafia.orgにアクセスした場合、そのスクリプトがブラウザの識別情報を使用して、私の全財産を何らかのランダムなマフィア口座に送金する指示をmybank.comに送信できるようになりたくありません。

ウェブサイトは、このような攻撃から身を守ることはできますが、そのためには努力が必要です。多くのウェブサイトはそうしていません。このため、ブラウザは、スクリプトが他のドメインthemafia.orgmybank.comなどの名前)へのHTTPリクエストを行うことを禁止することで、私たちを守っています。

これは、正当な理由で複数のドメインにアクセスする必要があるシステムを構築する場合、厄介な問題になる可能性があります。幸い、サーバーはレスポンスに次のようなヘッダーを含めることで、他のドメインからのリクエストを許可しても問題ないことをブラウザに明示的に示すことができます。

Access-Control-Allow-Origin: *

リクエストの抽象化

第10章では、AMDモジュールシステムの実装において、backgroundReadFileという仮の関数を使用しました。これはファイル名と関数を受け取り、フェッチが完了したらファイルの内容をその関数に渡します。この関数の簡単な実装を次に示します。

function backgroundReadFile(url, callback) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.addEventListener("load", function() {
    if (req.status < 400)
      callback(req.responseText);
  });
  req.send(null);
}

この単純な抽象化により、単純なGETリクエストにXMLHttpRequestを使用することが容易になります。HTTPリクエストを行うプログラムを作成する場合は、ヘルパー関数を使用することをお勧めします。そうすることで、コード全体で醜いXMLHttpRequestパターンを繰り返し記述する必要がなくなります。

関数引数の名前であるcallbackは、このような関数を記述するために頻繁に使用される用語です。コールバック関数は、他のコードに渡され、そのコードに「後で呼び戻す」方法を提供します。

アプリケーションが実行している処理に合わせて調整されたHTTPユーティリティ関数を記述することは難しくありません。前の関数はGETリクエストしか行わず、ヘッダーやリクエストボディを制御できません。POSTリクエスト用の別のバリアント、またはさまざまな種類のリクエストをサポートするより汎用的なバリアントを作成できます。多くのJavaScriptライブラリもXMLHttpRequestのラッパーを提供しています。

前のラッパーの主な問題は、エラー処理です。リクエストがエラーを示すステータスコード(400以上)を返す場合、何も行いません。状況によってはこれで問題ない場合もありますが、情報をフェッチしていることを示す「読み込み中」インジケータをページに配置したと想像してください。サーバーがクラッシュした場合、または接続が一時的に中断されたためにリクエストが失敗した場合、ページは単に停止し、誤解を招くように何かをしているように見えます。ユーザーはしばらく待機し、我慢できなくなり、サイトを役に立たない不安定なものと見なすでしょう。

リクエストが失敗したときに適切な処置を取れるように、通知するオプションも用意する必要があります。たとえば、「読み込み中」メッセージを削除し、何か問題が発生したことをユーザーに通知できます。

非同期コードでのエラー処理は、同期コードでのエラー処理よりもさらに困難です。作業の一部を遅延させる必要があることが多いため、コールバック関数に配置すると、tryブロックの範囲が無意味になります。次のコードでは、backgroundReadFileの呼び出しはすぐに返るため、例外はキャッチされません。その後、制御はtryブロックを離れ、渡された関数は後で呼び出されます。

try {
  backgroundReadFile("example/data.txt", function(text) {
    if (text != "expected")
      throw new Error("That was unexpected");
  });
} catch (e) {
  console.log("Hello from the catch block");
}

失敗したリクエストを処理するには、ラッパーに追加の関数を渡して、リクエストが失敗したときにそれを呼び出すことができます。あるいは、リクエストが失敗した場合、問題を説明する追加の引数が通常のコールバック関数に渡されるという規約を使用できます。例を次に示します。

function getURL(url, callback) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.addEventListener("load", function() {
    if (req.status < 400)
      callback(req.responseText);
    else
      callback(null, new Error("Request failed: " +
                               req.statusText));
  });
  req.addEventListener("error", function() {
    callback(null, new Error("Network error"));
  });
  req.send(null);
}

リクエストが完全に失敗した場合にシグナルされる"error"イベントのハンドラを追加しました。また、エラーを示すステータスコードでリクエストが完了した場合、エラー引数を付けてコールバック関数を呼び出します。

getURLを使用するコードは、エラーが渡されたかどうかをチェックし、エラーが見つかった場合はそれを処理する必要があります。

getURL("data/nonsense.txt", function(content, error) {
  if (error != null)
    console.log("Failed to fetch nonsense.txt: " + error);
  else
    console.log("nonsense.txt: " + content);
});

これは、例外に関しては役に立ちません。複数の非同期アクションをチェーン状に連結する場合、チェーンの任意の時点で例外が発生すると(各処理関数を独自のtry/catchブロックでラップしない限り)、最上位レベルに到達し、アクションのチェーンを中断します。

Promise

複雑なプロジェクトでは、プレーンなコールバックスタイルで非同期コードを記述するのは正しく行うのが困難です。エラーをチェックすることを忘れたり、予期しない例外によってプログラムが粗雑な方法で途中で中断されたりすることがあります。さらに、エラーが複数のコールバック関数とcatchブロックを通過する必要がある場合に、正しいエラー処理を配置するのは面倒です。

この問題を解決するために、多くの試みが追加の抽象化によって行われてきました。より成功したものの1つは、Promise(プロミス)と呼ばれています。Promiseは非同期アクションをオブジェクトでラップし、アクションの完了時または失敗時に特定の処理を実行するように指示できます。このインターフェースは、次期JavaScript言語の一部になる予定ですが、すでにライブラリとして使用できます。

Promiseのインターフェースは完全に直感的ではありませんが、強力です。この章では、概要のみを説明します。より詳細な説明はwww.promisejs.orgを参照してください。

Promiseオブジェクトを作成するには、非同期アクションを初期化する関数を与えてPromiseコンストラクタを呼び出します。コンストラクタはその関数を呼び出し、それ自体が関数である2つの引数を渡します。最初の関数はアクションが正常に完了したときに呼び出され、2番目の関数は失敗したときに呼び出されます。

もう一度、GETリクエストのラッパーをここで示します。今回はPromiseを返します。今回は単にgetと呼びます。

function get(url) {
  return new Promise(function(succeed, fail) {
    var req = new XMLHttpRequest();
    req.open("GET", url, true);
    req.addEventListener("load", function() {
      if (req.status < 400)
        succeed(req.responseText);
      else
        fail(new Error("Request failed: " + req.statusText));
    });
    req.addEventListener("error", function() {
      fail(new Error("Network error"));
    });
    req.send(null);
  });
}

関数のインターフェース自体がはるかにシンプルになっていることに注意してください。URLを渡すと、Promiseが返されます。そのPromiseは、リクエストの結果へのハンドルとして機能します。成功と失敗を処理する2つの関数で呼び出すことができるthenメソッドがあります。

get("example/data.txt").then(function(text) {
  console.log("data.txt: " + text);
}, function(error) {
  console.log("Failed to fetch data.txt: " + error);
});

今のところ、これはすでに表現した内容を別の方法で表現しているだけです。アクションを連鎖させる必要がある場合にのみ、Promiseは大きな違いをもたらします。

thenを呼び出すと、新しいPromiseが生成されます。その結果(成功ハンドラに渡される値)は、thenに渡した最初の関数の戻り値によって決まります。この関数は、さらに非同期処理が行われていることを示す別のPromiseを返す場合があります。この場合、thenによって返されるPromiseは、ハンドラ関数によって返されるPromiseを待ち、解決されると同じ値で成功または失敗します。ハンドラ関数がPromise以外の値を返す場合、thenによって返されるPromiseはその値を結果としてすぐに成功します。

つまり、thenを使用してPromiseの結果を変換できます。たとえば、これは、指定されたURLの内容をJSONとして解析した結果であるPromiseを返します。

function getJSON(url) {
  return get(url).then(JSON.parse);
}

最後のthen呼び出しでは、失敗ハンドラを指定していません。これは許容されます。エラーはthenによって返されるPromiseに渡され、まさに私たちが望むものです。getJSONは何かがうまくいかない場合に何をするべきかわかりませんが、呼び出し元は知っているでしょう。

Promiseの使用方法を示す例として、サーバーから複数のJSONファイルを取得し、その間「読み込み中」という単語を表示するプログラムを作成します。JSONファイルには、fathermotherspouseなどのプロパティで他の個人を表すファイルへのリンクを含む、個人に関する情報が含まれています。

example/bert.jsonの配偶者の母親の名前を取得したいと考えています。そして、何か問題が発生した場合は、「読み込み中」テキストを削除し、代わりにエラーメッセージを表示します。Promiseを使用して行う方法を以下に示します。

<script>
  function showMessage(msg) {
    var elt = document.createElement("div");
    elt.textContent = msg;
    return document.body.appendChild(elt);
  }

  var loading = showMessage("Loading...");
  getJSON("example/bert.json").then(function(bert) {
    return getJSON(bert.spouse);
  }).then(function(spouse) {
    return getJSON(spouse.mother);
  }).then(function(mother) {
    showMessage("The name is " + mother.name);
  }).catch(function(error) {
    showMessage(String(error));
  }).then(function() {
    document.body.removeChild(loading);
  });
</script>

結果のプログラムは比較的コンパクトで読みやすいです。catchメソッドはthenに似ていますが、失敗ハンドラのみを受け付け、成功の場合は結果を変更せずに渡す点が異なります。try文のcatch句と同様に、エラーがキャッチされた後も制御は通常どおり続行されます。このようにして、読み込みメッセージを削除する最後のthenは、何か問題が発生した場合でも常に実行されます。

Promiseインターフェースは、非同期制御フローのための独自の言語を実装していると考えることができます。これを実現するために必要な追加のメソッド呼び出しと関数式により、コードはややぎこちなく見えますが、エラー処理をすべて自分で行う場合と比較すれば、はるかにぎこちなくはありません。

HTTPを理解する

ブラウザで実行されるJavaScriptプログラム(クライアント側)とサーバー上のプログラム(サーバー側)間の通信が必要なシステムを構築する場合、この通信をモデル化するにはいくつかの異なる方法があります。

一般的に使用されるモデルは、リモートプロシージャコールです。このモデルでは、通信は通常の関数呼び出しのパターンに従いますが、関数は実際には別のマシンで実行されています。関数を呼び出すには、関数の名前と引数を含むサーバーへのリクエストを行う必要があります。そのリクエストへの応答には、戻り値が含まれています。

リモートプロシージャコールという観点から考えると、HTTPは単なる通信手段であり、ほとんどの場合、それを完全に隠す抽象化レイヤーを作成します。

別の方法として、リソースとHTTPメソッドの概念に基づいて通信を構築する方法があります。addUserというリモートプロシージャの代わりに、/users/larryに対してPUTリクエストを使用します。ユーザーのプロパティを関数引数にエンコードする代わりに、ユーザーを表すドキュメント形式を定義するか、既存の形式を使用します。新しいリソースを作成するためのPUTリクエストの本文は、そのようなドキュメントになります。リソースは、リソースのURL(たとえば、/user/larry)に対してGETリクエストを行うことで取得され、リソースを表すドキュメントが返されます。

この2番目のアプローチでは、リソースのキャッシュ(クライアント側にコピーを保持する)などのHTTPが提供する機能の一部をより簡単に使用できます。また、リソースは関数群よりも推論しやすいので、インターフェースの一貫性を高めるのにも役立ちます。

セキュリティとHTTPS

インターネットを介して送信されるデータは、長く危険な道をたどる傾向があります。目的地に到達するには、コーヒーショップのWi-Fiネットワークから、さまざまな企業や州が管理するネットワークまで、あらゆるものを通過する必要があります。ルートのどの時点でも、検査されたり、変更されたりする可能性があります。

メールアカウントのパスワードなど、秘密にしておく必要があるもの、または銀行のWebサイトから送金する口座番号など、変更せずに目的地に到着する必要があるものについては、プレーンHTTPでは十分ではありません。

URLがhttps://で始まるセキュアなHTTPプロトコルは、HTTPトラフィックをラップして、読み取りと改ざんを困難にします。まず、クライアントは、サーバーがブラウザが認識する認証機関によって発行された暗号化証明書を持っていることを証明することを要求することにより、サーバーが主張するものであることを確認します。次に、接続を介して送られるすべてのデータは、盗聴と改ざんを防ぐ方法で暗号化されます。

したがって、正しく動作する場合、HTTPSは、あなたが話そうとしていたWebサイトになりすましている人物と、あなたの通信を盗聴している人物の両方を防ぎます。完璧ではありません。偽造または盗まれた証明書と壊れたソフトウェアのためにHTTPSが失敗したさまざまな事件がありました。それでも、プレーンHTTPは簡単に改ざんできますが、HTTPSを破るには、国家または高度な犯罪組織だけが実行できるような努力が必要です。

要約

この章では、HTTPはインターネットを介してリソースにアクセスするためのプロトコルであることを学びました。クライアントはリクエストを送信し、そこにはメソッド(通常はGET)とリソースを識別するパスが含まれています。次に、サーバーはリクエストをどのように処理するかを決定し、ステータスコードとレスポンスボディで応答します。リクエストとレスポンスの両方に、追加情報を提供するヘッダーが含まれる場合があります。

ブラウザは、Webページを表示するために必要なリソースを取得するためにGETリクエストを行います。Webページにはフォームが含まれている場合もあり、ユーザーが入力した情報をフォームの送信時に実行されるリクエストに含めることができます。それについては、次の章で詳しく説明します。

ブラウザのJavaScriptがHTTPリクエストを行うためのインターフェースはXMLHttpRequestと呼ばれます。通常は、その名前の「XML」部分は無視できます(ただし、入力する必要があります)。使用方法は2通りあります。同期式は、リクエストが完了するまですべてをブロックし、非同期式は、応答が入ってきたことに気付くためのイベントハンドラが必要です。ほとんどの場合、非同期式の方が好ましいです。リクエストの実行方法は次のとおりです。

var req = new XMLHttpRequest();
req.open("GET", "example/data.txt", true);
req.addEventListener("load", function() {
  console.log(req.status);
});
req.send(null);

非同期プログラミングは難しいです。Promiseは、エラー状態と例外を適切なハンドラにルーティングし、このプログラミングスタイルにおけるより反復的でエラーが発生しやすい要素の一部を抽象化することにより、それを少し簡単にします。

演習

コンテンツネゴシエーション

HTTPが行うことができるものの、この章では説明しなかったものの1つに、コンテンツネゴシエーションがあります。リクエストのAcceptヘッダーを使用して、クライアントが取得したいドキュメントの種類をサーバーに伝えることができます。多くのサーバーはこのヘッダーを無視しますが、サーバーがリソースをエンコードするさまざまな方法を知っている場合、このヘッダーを確認して、クライアントが優先するものを送信できます。

URL eloquentjavascript.net/authorは、クライアントが要求するものに応じて、プレーンテキスト、HTML、またはJSONで応答するように構成されています。これらの形式は、標準化されたメディアタイプtext/plaintext/html、およびapplication/jsonによって識別されます。

このリソースの3つの形式すべてを取得するリクエストを送信します。XMLHttpRequestオブジェクトのsetRequestHeaderメソッドを使用して、Acceptという名前のヘッダーを上記のメディアタイプの1つに設定します。ヘッダーはopenを呼び出した後、sendを呼び出す前に設定してください。

最後に、application/rainbows+unicornsメディアタイプを要求して、何が起こるかを確認してください。

// Your code here.

この章では、リクエストを行う際に関係するメソッド呼び出しの例として、XMLHttpRequest の様々な使用例を参照してください。必要であれば、同期リクエスト(openの第3パラメータをfalseに設定)を使用することもできます。

不正なメディアタイプを要求すると、コード406の「Not Acceptable」というレスポンスが返されます。これは、サーバーがAcceptヘッダーを満たせない場合に返すコードです。

複数のPromiseを待つ

Promiseコンストラクタには、Promiseの配列を受け取り、配列内のすべてのPromiseが完了するまで待機するallメソッドがあります。その後、成功し、結果値の配列を生成します。配列内のいずれかのPromiseが失敗した場合、allによって返されるPromiseも失敗します(失敗したPromiseからの失敗値と共に)。

allという通常の関数として、このようなものを自分で実装してみてください。

Promiseが解決された(成功または失敗した)後、再び成功または失敗することはなく、それを解決する関数のそれ以降の呼び出しは無視されます。これは、Promiseの失敗を処理する方法を簡素化できます。

function all(promises) {
  return new Promise(function(success, fail) {
    // Your code here.
  });
}

// Test code.
all([]).then(function(array) {
  console.log("This should be []:", array);
});
function soon(val) {
  return new Promise(function(success) {
    setTimeout(function() { success(val); },
               Math.random() * 500);
  });
}
all([soon(1), soon(2), soon(3)]).then(function(array) {
  console.log("This should be [1, 2, 3]:", array);
});
function fail() {
  return new Promise(function(success, fail) {
    fail(new Error("boom"));
  });
}
all([soon(1), fail(), soon(3)]).then(function(array) {
  console.log("We should not get here");
}, function(error) {
  if (error.message != "boom")
    console.log("Unexpected failure:", error);
});

Promiseコンストラクタに渡される関数は、与えられた配列内の各Promiseに対してthenを呼び出す必要があります。それらのいずれかが成功すると、2つのことが起こる必要があります。結果値を結果配列の正しい位置に格納する必要があり、これが最後の保留中のPromiseであったかどうかを確認し、それが最後だった場合は独自のPromiseを完了する必要があります。

後者は、入力配列の長さに初期化され、Promiseが成功するたびに1減算されるカウンタを使用して行うことができます。0に達すると、処理が完了します。入力配列が空の場合(したがって、Promiseは決して解決されない)を考慮に入れてください。

失敗の処理には少し考えが必要ですが、非常にシンプルになります。ラップされたPromiseの失敗関数を配列内の各Promiseに渡すだけで、それらのいずれかの失敗が全体のラッパーの失敗をトリガーします。