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

第18章HTTPとフォーム

通信はステートレスである必要があります[...] クライアントからサーバーへの各リクエストは、リクエストを理解するために必要なすべての情報を含んでいなければならず、サーバー上に保存されたコンテキストを利用することはできません。

ロイ・フィールディング, Architectural Styles and the Design of Network-based Software Architectures
Picture of a web form on a medieval scroll

既に第13章で触れたハイパーテキスト転送プロトコルは、ワールドワイドウェブ上でデータが要求され、提供されるメカニズムです。この章では、このプロトコルをより詳細に説明し、ブラウザのJavaScriptがどのようにアクセスできるかを説明します。

プロトコル

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

GET /18_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: Mon, 08 Jan 2018 10:29:45 GMT

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

ブラウザは空行後の応答の部分、つまり本文(HTMLの<body>タグと混同しないでください)を取得し、HTMLドキュメントとして表示します。

クライアントが送信する情報はリクエストと呼ばれます。これは次の行から始まります。

GET /18_http.html HTTP/1.1

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

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

リソースパスの後、リクエストの最初の行にはHTTP/1.1が記載されており、使用しているHTTPプロトコルのバージョンを示しています。

実際には、多くのサイトがHTTPバージョン2を使用しており、バージョン1.1と同じ概念をサポートしていますが、より高速にするためにはるかに複雑になっています。ブラウザは、特定のサーバーと通信するときに適切なプロトコルバージョンに自動的に切り替わり、リクエストの結果はどのバージョンを使用しても同じです。バージョン1.1の方が単純で扱いやすいので、ここではそれに焦点を当てます。

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

HTTP/1.1 200 OK

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

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

Content-Length: 65585
Content-Type: text/html
Last-Modified: Thu, 04 Jan 2018 14:05:30 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つのフィールドを持つフォームを記述しています。[送信]ボタンをクリックすると、フォームが送信されます。つまり、フィールドの内容がHTTPリクエストにパックされ、ブラウザはそのリクエストの結果に移動します。

<form>要素のmethod属性がGETの場合(または省略されている場合)、フォームの情報はactionURLの最後にクエリ文字列として追加されます。ブラウザは次のURLにリクエストを送信する場合があります。

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

疑問符はURLのパス部分の終わりとクエリの開始を示しています。その後に、名前と値のペアが続きます。これらは、それぞれフォームフィールド要素のname属性とそれらの要素の内容に対応しています。アンパサンド文字(&)を使用してペアを区切ります。

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

console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?

前に見た例の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からの操作方法については、この章の後半で説明します。

Fetch

ブラウザのJavaScriptがHTTPリクエストを行うためのインターフェースはfetchと呼ばれます。比較的新しいインターフェースなので、都合の良いことにPromiseを使用しています(ブラウザのインターフェースとしては珍しいです)。

fetch("example/data.txt").then(response => {
  console.log(response.status);
  // → 200
  console.log(response.headers.get("Content-Type"));
  // → text/plain
});

fetchを呼び出すと、サーバーの応答に関する情報(ステータスコードやヘッダーなど)を含むResponseオブジェクトに解決されるPromiseが返されます。ヘッダーはMapのようなオブジェクトにラップされており、ヘッダー名はケースインセンシティブであるため、そのキー(ヘッダー名)をケースインセンシティブとして扱います。つまり、headers.get("Content-Type")headers.get("content-TYPE")は同じ値を返します。

fetchによって返されるPromiseは、サーバーがエラーコードを返した場合でも正常に解決することに注意してください。ネットワークエラーが発生した場合、または要求の宛先となるサーバーが見つからない場合にも、拒否される可能性があります

fetchの最初の引数は、要求するURLです。そのURLがプロトコル名(http:など)で始まらない場合、相対的として扱われます。つまり、現在のドキュメントを基準に解釈されます。スラッシュ(/)で始まる場合は、現在のパス(サーバー名以降の部分)が置き換えられます。そうでない場合、現在のパスの最後のスラッシュ文字までを含む部分が、相対URLの前に置かれます。

レスポンスの実際のコンテンツを取得するには、そのtextメソッドを使用できます。最初のPromiseはレスポンスのヘッダーが受信された時点で解決され、レスポンスボディの読み込みにはさらに時間がかかる可能性があるため、これもPromiseを返します。

fetch("example/data.txt")
  .then(resp => resp.text())
  .then(text => console.log(text));
// → This is the content of data.txt

同様のメソッドであるjsonは、ボディをJSONとしてパースしたときに得られる値に解決されるPromiseを返し、有効なJSONでない場合は拒否します。

デフォルトでは、fetchGETメソッドを使用してリクエストを行い、リクエストボディを含みません。2番目の引数として追加のオプションを含むオブジェクトを渡すことで、これを変更できます。たとえば、このリクエストはexample/data.txtを削除しようとします。

fetch("example/data.txt", {method: "DELETE"}).then(resp => {
  console.log(resp.status);
  // → 405
});

405ステータスコードは「メソッドが許可されていません」を意味し、HTTPサーバーが「それはできません」と言う方法です。

リクエストボディを追加するには、bodyオプションを含めます。ヘッダーを設定するには、headersオプションがあります。たとえば、このリクエストにはRangeヘッダーが含まれており、サーバーにレスポンスの一部のみを返すように指示します。

fetch("example/data.txt", {headers: {Range: "bytes=8-19"}})
  .then(resp => resp.text())
  .then(console.log);
// → the content

ブラウザは、「Host」など、サーバーがボディのサイズを判断するために必要なリクエストヘッダーを自動的に追加します。しかし、認証情報などの情報を追加したり、サーバーに受信したいファイル形式を伝えたりするために、独自のヘッダーを追加することが役立つことがよくあります。

HTTPサンドボックス化

ウェブページのスクリプトでHTTPリクエストを行うことは、再びセキュリティ上の懸念を引き起こします。スクリプトを制御する人は、スクリプトが実行されているコンピュータの所有者と同じ利害関係を持っているとは限りません。具体的には、themafia.orgにアクセスした場合、そのスクリプトがブラウザの識別情報を使用してmybank.comにリクエストを行い、私の全財産をランダムな口座に送金する指示を出すことを望んでいません。

このため、ブラウザは、スクリプトが他のドメイン(themafia.orgmybank.comなどの名前)へのHTTPリクエストを行うことを禁止することで、私たちを保護しています。

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

Access-Control-Allow-Origin: *

HTTPの理解

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

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

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

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

この2番目のアプローチにより、リソースのキャッシング(高速アクセスのためのクライアントへのコピーの保持)などのHTTPが提供する機能の一部を簡単に使用できます。HTTPで使用される概念は、よく設計されており、サーバーインターフェースを設計するための有益な原則を提供できます。

セキュリティとHTTPS

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

メールアカウントのパスワードなど、秘密にしておくことが重要である場合、または銀行のウェブサイトを介して送金する口座番号など、変更せずに目的地に到着させることが重要である場合は、プレーンHTTPでは不十分です。

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

したがって、正しく機能する場合、HTTPSは、通信しようとしているウェブサイトを他のユーザーが偽装したり、通信を盗聴したりするのを防ぎます。完璧ではなく、偽造または盗まれた証明書と壊れたソフトウェアのためにHTTPSが失敗したさまざまなインシデントがありましたが、プレーンHTTPよりもはるかに安全です。

フォームフィールド

フォームは、もともとJavaScript以前のWebのために設計され、ウェブサイトがユーザーが送信した情報をHTTPリクエストで送信できるようにしていました。この設計では、サーバーとのやり取りは常に新しいページに移動することで行われると想定しています。

しかし、その要素はページの他の要素と同様にDOMの一部であり、フォームフィールドを表すDOM要素は、他の要素には存在しない多くのプロパティとイベントをサポートしています。これにより、JavaScriptプログラムを使用してそのような入力フィールドを検査および制御し、フォームに新しい機能を追加したり、フォームとフィールドをJavaScriptアプリケーションの構成要素として使用したりすることが可能になります。

Webフォームは、<form>タグにグループ化された任意の数の入力フィールドで構成されます。HTMLは、単純なオン/オフチェックボックスからドロップダウンメニュー、テキスト入力フィールドまで、いくつかの異なるスタイルのフィールドを許可しています。この本では、すべてのフィールドタイプを網羅的に説明するわけではありませんが、大まかな概要から始めます。

多くのフィールドタイプは<input>タグを使用します。このタグのtype属性は、フィールドのスタイルを選択するために使用されます。これらは一般的に使用される<input>の種類です。

text1行のテキストフィールド
passwordtextと同じですが、入力されたテキストを非表示にします
checkboxオン/オフスイッチ
radio複数選択フィールドの一部
fileユーザーがコンピュータからファイルを選択できるようにします

フォームフィールドは、必ずしも<form>タグに表示される必要はありません。ページのどこにでも配置できます。このようなフォームレスフィールドは送信できません(フォーム全体のみが送信できます)が、JavaScriptで入力に応答する場合、通常はフィールドを通常どおり送信したくありません。

<p><input type="text" value="abc"> (text)</p>
<p><input type="password" value="abc"> (password)</p>
<p><input type="checkbox" checked> (checkbox)</p>
<p><input type="radio" value="A" name="choice">
   <input type="radio" value="B" name="choice" checked>
   <input type="radio" value="C" name="choice"> (radio)</p>
<p><input type="file"> (file)</p>

このような要素のJavaScriptインターフェースは、要素の種類によって異なります。

複数行のテキストフィールドには独自のタグ<textarea>があり、これは主に、複数行の開始値を指定するために属性を使用することが面倒なためです。<textarea>タグには、対応する</textarea>終了タグが必要であり、value属性ではなく、これら2つの間のテキストを開始テキストとして使用します。

<textarea>
one
two
three
</textarea>

最後に、<select>タグは、ユーザーがいくつかの事前に定義されたオプションから選択できるフィールドを作成するために使用されます。

<select>
  <option>Pancakes</option>
  <option>Pudding</option>
  <option>Ice cream</option>
</select>

フォームフィールドの値が変更されるたびに、「change」イベントが発生します。

フォーカス

HTMLドキュメントのほとんどの要素とは異なり、フォームフィールドはキーボードフォーカスを取得できます。クリックしたり、他の方法でアクティブ化すると、現在アクティブな要素になり、キーボード入力の受信者になります。

そのため、テキストフィールドに入力できるのは、フォーカスが当たっている場合のみです。他のフィールドは、キーボードイベントに異なる方法で反応します。例えば、<select> メニューは、ユーザーが入力したテキストを含むオプションに移動しようと試み、矢印キーで選択を上下に移動します。

JavaScriptでは、focusメソッドとblurメソッドを使用してフォーカスを制御できます。前者は呼び出されたDOM要素にフォーカスを移動し、後者はフォーカスを削除します。document.activeElement の値は、現在フォーカスされている要素に対応します。

<input type="text">
<script>
  document.querySelector("input").focus();
  console.log(document.activeElement.tagName);
  // → INPUT
  document.querySelector("input").blur();
  console.log(document.activeElement.tagName);
  // → BODY
</script>

一部のページでは、ユーザーがフォームフィールドをすぐに操作することを期待しています。ドキュメントの読み込み時にこのフィールドにフォーカスさせるためにJavaScriptを使用できますが、HTMLはautofocus属性も提供しており、同じ効果を生み出しながら、ブラウザに私たちが達成しようとしていることを知らせます。これにより、ユーザーが他の場所にフォーカスを置いている場合など、適切でない場合にブラウザが動作を無効にすることができます。

従来、ブラウザでは、Tabキーを押すことで、ドキュメント内をフォーカスを移動することもできます。tabindex属性を使用して、要素がフォーカスを受け取る順序に影響を与えることができます。次の例では、ヘルプリンクを経由せずに、テキスト入力からOKボタンにフォーカスがジャンプします。

<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>

デフォルトでは、ほとんどの種類のHTML要素はフォーカスできません。しかし、フォーカス可能な要素にはtabindex属性を追加できます。tabindexが-1の場合、通常フォーカス可能であっても、タブ移動によって要素をスキップします。

無効化されたフィールド

すべてのフォームフィールドは、disabled属性によって無効化できます。これは、値を指定せずに指定できる属性です。存在するだけで要素が無効になります。

<button>I'm all right</button>
<button disabled>I'm out</button>

無効化されたフィールドはフォーカスしたり変更したりできず、ブラウザではグレーで淡色表示されます。

プログラムが、ボタンまたは他のコントロールによって引き起こされたアクションを処理中で、サーバーとの通信が必要となり時間がかかる可能性がある場合、アクションが完了するまでコントロールを無効化することが良い考えです。そうすることで、ユーザーが焦って再度クリックしても、誤ってアクションを繰り返すことがなくなります。

フォーム全体

フィールドが<form>要素に含まれている場合、そのDOM要素には、フォームのDOM要素にリンクするformプロパティがあります。<form>要素には、内部のフィールドを含む配列のようなコレクションであるelementsというプロパティがあります。

フォームフィールドのname属性は、フォームが送信されたときにその値がどのように識別されるかを決定します。また、フォームのelementsプロパティにアクセスする際のプロパティ名としても使用でき、配列のようなオブジェクト(数値でアクセス可能)とマップ(名前でアクセス可能)の両方として機能します。

<form action="example/submit.html">
  Name: <input type="text" name="name"><br>
  Password: <input type="password" name="password"><br>
  <button type="submit">Log in</button>
</form>
<script>
  let form = document.querySelector("form");
  console.log(form.elements[1].type);
  // → password
  console.log(form.elements.password.type);
  // → password
  console.log(form.elements.name.form == form);
  // → true
</script>

type属性がsubmitであるボタンは、押すとフォームが送信されます。フォームフィールドにフォーカスが当たっている状態でEnterキーを押すと、同じ効果があります。

通常、フォームを送信することは、ブラウザがフォームのaction属性で指定されたページに、GETまたはPOSTリクエストを使用して移動することを意味します。しかし、それの前に"submit"イベントが発生します。このイベントをJavaScriptで処理し、イベントオブジェクトでpreventDefaultを呼び出すことで、このデフォルトの動作を阻止できます。

<form action="example/submit.html">
  Value: <input type="text" name="value">
  <button type="submit">Save</button>
</form>
<script>
  let form = document.querySelector("form");
  form.addEventListener("submit", event => {
    console.log("Saving value", form.elements.value.value);
    event.preventDefault();
  });
</script>

JavaScriptで"submit"イベントをインターセプトすることには、さまざまな用途があります。ユーザーが入力した値が妥当であることを確認し、フォームを送信する代わりにエラーメッセージをすぐに表示するコードを書くことができます。あるいは、例のように、フォームの通常の送信方法を完全に無効にし、プログラムで入力を処理し、ページをリロードせずにfetchを使用してサーバーに送信することもできます。

テキストフィールド

<textarea>タグ、またはtypetextまたはpasswordである<input>タグによって作成されたフィールドは、共通のインターフェースを共有します。それらのDOM要素には、現在のコンテンツを文字列値として保持するvalueプロパティがあります。このプロパティを別の文字列に設定すると、フィールドのコンテンツが変わります。

テキストフィールドのselectionStartプロパティとselectionEndプロパティは、テキスト内のカーソルと選択に関する情報を提供します。何も選択されていない場合、これらの2つのプロパティは同じ数値を保持し、カーソルの位置を示します。例えば、0はテキストの先頭を示し、10はカーソルが10番目の文字の後にあります。フィールドの一部が選択されている場合、2つのプロパティは異なり、選択されたテキストの開始と終了を示します。valueと同様に、これらのプロパティも書き込むことができます。

カセケムイに関する記事を書いていて、名前のスペルに苦労していると想像してください。次のコードは、F2キーを押すと「カセケムイ」という文字列を挿入するイベントハンドラを備えた<textarea>タグを接続します。

<textarea></textarea>
<script>
  let textarea = document.querySelector("textarea");
  textarea.addEventListener("keydown", event => {
    // The key code for F2 happens to be 113
    if (event.keyCode == 113) {
      replaceSelection(textarea, "Khasekhemwy");
      event.preventDefault();
    }
  });
  function replaceSelection(field, word) {
    let from = field.selectionStart, to = field.selectionEnd;
    field.value = field.value.slice(0, from) + word +
                  field.value.slice(to);
    // Put the cursor after the word
    field.selectionStart = from + word.length;
    field.selectionEnd = from + word.length;
  }
</script>

replaceSelection関数は、テキストフィールドのコンテンツで現在選択されている部分を指定された単語で置き換え、その後、ユーザーがタイプを続けることができるようにカーソルをその単語の後に移動します。

テキストフィールドの"change"イベントは、何かがタイプされるたびに発生するわけではありません。むしろ、コンテンツが変更された後にフィールドがフォーカスを失ったときに発生します。テキストフィールドの変更に即座に反応するには、代わりに"input"イベントのハンドラを登録する必要があります。これは、ユーザーが文字を入力したり、テキストを削除したり、フィールドのコンテンツを操作するたびに発生します。

次の例は、テキストフィールドと、フィールド内のテキストの現在の長さを表示するカウンターを示しています。

<input type="text"> length: <span id="length">0</span>
<script>
  let text = document.querySelector("input");
  let output = document.querySelector("#length");
  text.addEventListener("input", () => {
    output.textContent = text.value.length;
  });
</script>

チェックボックスとラジオボタン

チェックボックスフィールドはバイナリトグルです。その値は、ブール値を保持するcheckedプロパティを使用して抽出または変更できます。

<label>
  <input type="checkbox" id="purple"> Make this page purple
</label>
<script>
  let checkbox = document.querySelector("#purple");
  checkbox.addEventListener("change", () => {
    document.body.style.background =
      checkbox.checked ? "mediumpurple" : "";
  });
</script>

<label>タグは、ドキュメントの一部を入力フィールドに関連付けます。ラベルのどこをクリックしても、フィールドがアクティブになり、チェックボックスまたはラジオボタンの場合はフォーカスされ、値が切り替わります。

ラジオボタンはチェックボックスに似ていますが、同じname属性を持つ他のラジオボタンに暗黙的にリンクされているため、一度にアクティブにできるのは1つだけです。

Color:
<label>
  <input type="radio" name="color" value="orange"> Orange
</label>
<label>
  <input type="radio" name="color" value="lightgreen"> Green
</label>
<label>
  <input type="radio" name="color" value="lightblue"> Blue
</label>
<script>
  let buttons = document.querySelectorAll("[name=color]");
  for (let button of Array.from(buttons)) {
    button.addEventListener("change", () => {
      document.body.style.background = button.value;
    });
  }
</script>

querySelectorAllに与えられたCSSクエリにある角括弧は、属性を一致させるために使用されます。これは、name属性が"color"である要素を選択します。

セレクトフィールド

セレクトフィールドは概念的にはラジオボタンに似ています。ユーザーはオプションのセットから選択することもできます。しかし、ラジオボタンではオプションのレイアウトを制御できるのに対し、<select>タグの外観はブラウザによって決定されます。

セレクトフィールドには、ラジオボックスではなくチェックボックスのリストにより近いバリアントもあります。multiple属性が指定されている場合、<select>タグは、単一のオプションだけでなく、任意の数のオプションを選択できます。これは、ほとんどのブラウザで、通常は開いたときのみオプションを表示するドロップダウンコントロールとして描画される通常のセレクトフィールドとは異なる方法で表示されます。

<option>タグには値があります。この値はvalue属性で定義できます。それが指定されていない場合、オプション内のテキストがその値としてカウントされます。<select>要素のvalueプロパティは、現在選択されているオプションを反映します。ただし、multipleフィールドの場合、このプロパティはあまり意味がありません。現在選択されているオプションの1つの値のみが返されるためです。

<select>フィールドの<option>タグは、フィールドのoptionsプロパティを使用して配列のようなオブジェクトとしてアクセスできます。各オプションにはselectedというプロパティがあり、そのオプションが現在選択されているかどうかを示します。このプロパティは、オプションの選択または選択解除のために書き込むこともできます。

この例では、multipleセレクトフィールドから選択された値を抽出し、それらを使用して個々のビットから2進数を構成します。Controlキー(Macの場合はCommandキー)を押しながら複数のオプションを選択してください。

<select multiple>
  <option value="1">0001</option>
  <option value="2">0010</option>
  <option value="4">0100</option>
  <option value="8">1000</option>
</select> = <span id="output">0</span>
<script>
  let select = document.querySelector("select");
  let output = document.querySelector("#output");
  select.addEventListener("change", () => {
    let number = 0;
    for (let option of Array.from(select.options)) {
      if (option.selected) {
        number += Number(option.value);
      }
    }
    output.textContent = number;
  });
</script>

ファイルフィールド

ファイルフィールドは、元々はフォームを通してユーザーのマシンからファイルをアップロードする方法として設計されました。最新のブラウザでは、JavaScriptプログラムからそのようなファイルを読み取る方法も提供しています。フィールドは一種のゲートキーパーとして機能します。スクリプトはユーザーのコンピューターからプライベートファイルを単純に読み始めることはできませんが、ユーザーがこのようなフィールドでファイルを選択した場合、ブラウザはそのアクションを、スクリプトがファイルを読み取ってもよいという意味に解釈します。

ファイルフィールドは通常、「ファイルを選択」または「参照」のようなラベルの付いたボタンのように見え、選択したファイルに関する情報がその隣に表示されます。

<input type="file">
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    if (input.files.length > 0) {
      let file = input.files[0];
      console.log("You chose", file.name);
      if (file.type) console.log("It has type", file.type);
    }
  });
</script>

ファイルフィールド要素のfilesプロパティは、フィールドで選択されたファイルを含む配列のようなオブジェクト(ただし、本当の配列ではありません)です。最初は空です。単にfileプロパティがない理由は、ファイルフィールドがmultiple属性もサポートしており、複数のファイルを同時に選択できるためです。

filesオブジェクト内のオブジェクトには、name(ファイル名)、size(ファイルサイズ(バイト単位、8ビットのチャンク))、type(ファイルのメディアタイプ、例:text/plainまたはimage/jpeg)などのプロパティがあります。

ただし、ファイルの内容を含むプロパティはありません。ファイルの内容を取得するには、もう少し複雑な処理が必要です。ディスクからのファイル読み込みには時間がかかる可能性があるため、ドキュメントのフリーズを防ぐために、インターフェースは非同期である必要があります。

<input type="file" multiple>
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    for (let file of Array.from(input.files)) {
      let reader = new FileReader();
      reader.addEventListener("load", () => {
        console.log("File", file.name, "starts with",
                    reader.result.slice(0, 20));
      });
      reader.readAsText(file);
    }
  });
</script>

ファイルの読み取りは、FileReaderオブジェクトを作成し、そのオブジェクトに"load"イベントハンドラーを登録し、readAsTextメソッドを呼び出して読み取るファイルを渡すことによって行われます。読み込みが完了すると、リーダーのresultプロパティにファイルの内容が含まれます。

FileReaderは、何らかの理由でファイルの読み取りに失敗した場合にも"error"イベントを発生させます。エラーオブジェクト自体は、リーダーのerrorプロパティに格納されます。このインターフェースは、Promiseが言語の一部になる前に設計されました。次のようにPromiseでラップできます。

function readFileText(file) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.addEventListener(
      "load", () => resolve(reader.result));
    reader.addEventListener(
      "error", () => reject(reader.error));
    reader.readAsText(file);
  });
}

クライアントサイドでのデータの保存

少しのJavaScriptを使用したシンプルなHTMLページは、「ミニアプリケーション」つまり基本的なタスクを自動化する小さなヘルパープログラムにとって優れた形式です。いくつかのフォームフィールドとイベントハンドラーを接続することで、センチメートルとインチの変換から、マスターパスワードとウェブサイト名からパスワードを計算することまで、何でも行うことができます。

このようなアプリケーションがセッション間で何かを記憶する必要がある場合、JavaScriptバインディングを使用することはできません。ページが閉じられるたびに、それらは破棄されます。サーバーをセットアップし、インターネットに接続し、アプリケーションにそこで何かを保存させることができます。第20章でその方法を説明します。しかし、それは多くの余分な作業と複雑さです。時には、ブラウザにデータを保存しておくだけで十分な場合があります。

localStorageオブジェクトを使用して、ページのリロードを乗り越えるデータの保存を行うことができます。このオブジェクトを使用すると、名前に文字列値を保存できます。

localStorage.setItem("username", "marijn");
console.log(localStorage.getItem("username"));
// → marijn
localStorage.removeItem("username");

localStorage内の値は、上書きされるまで、removeItemで削除されるまで、またはユーザーがローカルデータをクリアするまで保持されます。

異なるドメインのサイトは、異なるストレージコンパートメントを取得します。つまり、特定のウェブサイトによってlocalStorageに保存されたデータは、原則として、同じサイトのスクリプトによってのみ読み取り(および上書き)できます。

ブラウザは、サイトがlocalStorageに保存できるデータのサイズに制限を設けています。この制限と、人々のハードドライブをジャンクでいっぱいにすることは実際には利益にならないという事実により、この機能が過剰なスペースを消費することが防止されます。

次のコードは、粗雑なメモ帳アプリケーションを実装しています。これは、名前付きメモのセットを保持し、ユーザーがメモを編集および新規作成できるようにします。

Notes: <select></select> <button>Add</button><br>
<textarea style="width: 100%"></textarea>

<script>
  let list = document.querySelector("select");
  let note = document.querySelector("textarea");

  let state;
  function setState(newState) {
    list.textContent = "";
    for (let name of Object.keys(newState.notes)) {
      let option = document.createElement("option");
      option.textContent = name;
      if (newState.selected == name) option.selected = true;
      list.appendChild(option);
    }
    note.value = newState.notes[newState.selected];

    localStorage.setItem("Notes", JSON.stringify(newState));
    state = newState;
  }
  setState(JSON.parse(localStorage.getItem("Notes")) || {
    notes: {"shopping list": "Carrots\nRaisins"},
    selected: "shopping list"
  });

  list.addEventListener("change", () => {
    setState({notes: state.notes, selected: list.value});
  });
  note.addEventListener("change", () => {
    setState({
      notes: Object.assign({}, state.notes,
                           {[state.selected]: note.value}),
      selected: state.selected
    });
  });
  document.querySelector("button")
    .addEventListener("click", () => {
      let name = prompt("Note name");
      if (name) setState({
        notes: Object.assign({}, state.notes, {[name]: ""}),
        selected: name
      });
    });
</script>

このスクリプトは、localStorageに保存されている"Notes"値から初期状態を取得するか、それが存在しない場合は、ショッピングリストのみを含むサンプル状態を作成します。localStorageから存在しないフィールドを読み取るとnullが返されます。nullJSON.parseに渡すと、文字列"null"が解析され、nullが返されます。したがって、||演算子を使用して、このような状況でデフォルト値を提供できます。

setStateメソッドは、DOMが特定の状態を表示し、新しい状態をlocalStorageに保存することを保証します。イベントハンドラーは、新しい状態に移動するためにこの関数を呼び出します。

この例でのObject.assignの使用は、古いstate.notesのクローンである新しいオブジェクトを作成しますが、1つのプロパティが追加または上書きされます。Object.assignは、最初の引数を受け取り、それ以降の引数のすべてのプロパティを追加します。したがって、空のオブジェクトを渡すと、新しいオブジェクトが作成されます。3番目の引数の角括弧表記は、名前が動的な値に基づいて作成されるプロパティを作成するために使用されます。

localStorageに似ている別のオブジェクトsessionStorageがあります。両者の違いは、sessionStorageの内容は各セッションの最後に忘れられることです。ほとんどのブラウザでは、ブラウザが閉じられるたびにセッションが終了します。

要約

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

ブラウザのJavaScriptがHTTPリクエストを行うためのインターフェースはfetchと呼ばれます。リクエストを行う方法は次のとおりです。

fetch("/18_http.html").then(r => r.text()).then(text => {
  console.log(`The page starts with ${text.slice(0, 15)}`);
});

ブラウザは、ウェブページを表示するために必要なリソースを取得するためにGETリクエストを行います。ページにはフォームも含まれており、フォームが送信されると、ユーザーが入力した情報が新しいページのリクエストとして送信されます。

HTMLは、テキストフィールド、チェックボックス、複数選択フィールド、ファイルピッカーなど、さまざまなタイプのフォームフィールドを表すことができます。

このようなフィールドは、JavaScriptで検査および操作できます。変更されると"change"イベントが発生し、テキストが入力されると"input"イベントが発生し、キーボードフォーカスがある場合にキーボードイベントを受け取ります。value(テキストフィールドとセレクトフィールドの場合)またはchecked(チェックボックスとラジオボタンの場合)などのプロパティを使用して、フィールドの内容を読み書きします。

フォームが送信されると、"submit"イベントが発生します。JavaScriptハンドラーはそのイベントでpreventDefaultを呼び出して、ブラウザのデフォルトの動作を無効にできます。フォームフィールド要素は、フォームタグの外にも配置できます。

ユーザーがファイルピッカーフィールドでローカルファイルシステムからファイルを選択すると、FileReaderインターフェースを使用して、JavaScriptプログラムからこのファイルの内容にアクセスできます。

localStoragesessionStorageオブジェクトを使用して、ページのリロードを乗り越える情報を保存できます。最初のオブジェクトはデータを永久に保存し(ユーザーがクリアするまで)、2番目のオブジェクトはブラウザが閉じられるまで保存します。

練習問題

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

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

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

このリソースの3つの形式すべてを取得するリクエストを送信します。fetchに渡されるオプションオブジェクトのheadersプロパティを使用して、Acceptという名前のヘッダーを目的のメディアタイプに設定します。

最後に、メディアタイプapplication/rainbows+unicornsを要求してみてください。どのようなステータスコードが生成されるかを確認してください。

// Your code here.

コードは、章の前の部分にあるfetchの例に基づいてください。

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

JavaScriptワークベンチ

ユーザーがJavaScriptコードを入力して実行できるインターフェースを作成します。

<textarea> フィールドの横にボタンを配置します。このボタンを押すと、第10章で見た`Function`コンストラクタを使用してテキストを関数にラップし、それを呼び出します。関数の戻り値、または発生したエラーを文字列に変換し、テキストフィールドの下に表示します。

<textarea id="code">return "hi";</textarea>
<button id="button">Run</button>
<pre id="output"></pre>

<script>
  // Your code here.
</script>

HTMLで定義された要素にアクセスするには、`document.querySelector`または`document.getElementById`を使用します。ボタンの` "click"`または` "mousedown"`イベントのイベントハンドラは、テキストフィールドの`value`プロパティを取得し、それに`Function`を呼び出すことができます。

`Function`の呼び出しとその結果の呼び出しの両方を`try`ブロックでラップして、発生する例外をキャッチできるようにします。この場合、どのような種類の例外を探しているのかは実際にはわからないため、すべてをキャッチします。

出力要素の`textContent`プロパティを使用して、文字列メッセージを入力できます。または、古いコンテンツを残しておきたい場合は、`document.createTextNode`を使用して新しいテキストノードを作成し、要素に追加します。すべての出力が1行に表示されないように、改行文字を末尾に追加することを忘れないでください。

ライフゲーム

ライフゲームは、グリッド上に人工的な「生命」を作り出す単純なシミュレーションであり、各セルは生きているか死んでいるかのいずれかです。各世代(ターン)で、次のルールが適用されます。

隣接するセルとは、対角線上にあるセルも含めて、隣接するすべてのセルを指します。

これらのルールは、一度にグリッド全体に適用され、1つのマスずつ適用されるわけではないことに注意してください。つまり、隣接セルのカウントは世代の開始時の状況に基づいており、この世代中に隣接セルに発生する変化は、特定のセルの新しい状態に影響を与えません。

適切なデータ構造を使用してこのゲームを実装します。`Math.random`を使用して、最初にグリッドをランダムなパターンで埋めます。チェックボックスのグリッドとして表示し、その横に次の世代に進むボタンを配置します。ユーザーがチェックボックスをオンまたはオフにした場合、その変更は次の世代の計算に含まれます。

<div id="grid"></div>
<button id="next">Next generation</button>

<script>
  // Your code here.
</script>

変更が概念的に同時に発生するという問題を解決するには、世代の計算を、1つのグリッドを受け取って次のターンを表す新しいグリッドを生成する純粋関数として考えてみてください。

行列の表現は、第6章で示されている方法で行うことができます。隣接する座標を両方の次元でループする2つのネストされたループを使用して、生きている隣接セルをカウントできます。フィールド外のセルをカウントしないこと、そして隣接セルのカウントをしている中心のセルを無視することに注意してください。

チェックボックスの変更が次の世代に反映されるようにするには、2つの方法があります。イベントハンドラがこれらの変更を認識し、それらを反映するように現在のグリッドを更新するか、次のターンを計算する前に、チェックボックスの値から新しいグリッドを生成することができます。

イベントハンドラを使用する場合は、各チェックボックスに対応する位置を識別する属性を付けることで、変更するセルを簡単に特定できます。

チェックボックスのグリッドを描画するには、<table>要素を使用するか(第14章を参照)、すべてを同じ要素に配置し、行間に<br>(改行)要素を配置します。