HTTPとフォーム

人々がこの設計について理解するのが難しかったのは、URL、HTTP、HTML以外に何もないということでした。Webを「制御する」中央コンピューターはなく、これらのプロトコルが動作する単一のネットワークもなく、「Webを運営する」組織はどこにもありませんでした。Webは、特定の「場所」に存在する物理的な「もの」ではありませんでした。それは情報が存在できる「空間」でした。

ティム・バーナーズ=リー
Illustration showing a web sign-up form on a parchment scroll

第13章で紹介したハイパーテキスト転送プロトコル(HTTP)は、World Wide Web上でデータが要求され、提供されるメカニズムです。この章では、プロトコルをより詳細に説明し、ブラウザの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: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

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

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

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

GET /18_http.html HTTP/1.1

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

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

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

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

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

HTTP/1.1 200 OK

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

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

Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT

これは、レスポンスドキュメントのサイズとタイプを示しています。この場合、87,320バイトのHTMLドキュメントです。また、そのドキュメントが最後に変更された日時も示しています。

クライアントとサーバーは、リクエストまたはレスポンスに含めるヘッダーを自由に決定できます。ただし、正常に動作するには、いくつかは必須です。たとえば、レスポンスに `Content-Type` ヘッダーがないと、ブラウザはドキュメントの表示方法を知ることができません。

ヘッダーの後、リクエストとレスポンスの両方で、空行とそれに続く本文を含めることができます。本文には、実際に送信されるドキュメントが含まれています。 `GET` リクエストと `DELETE` リクエストはデータを送りませんが、 `PUT` リクエストと `POST` リクエストは送ります。エラーレスポンスなど、一部のレスポンスタイプでは本文は必要ありません。

ブラウザとHTTP

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

moderately複雑なWebサイトには、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つのフィールドを持つフォームを表しています。1つは名前を尋ねる小さなフィールド、もう1つはメッセージを書くための大きなフィールドです。「送信」ボタンをクリックすると、フォームが*送信*されます。つまり、フィールドの内容がHTTPリクエストにパックされ、ブラウザはそのリクエストの結果に移動します。

<form>要素の `method` 属性が `GET` (または省略されている)の場合、フォームの情報は*クエリ文字列*として `action` URLの末尾に追加されます。ブラウザはこの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` と呼ばれます。

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でない場合は拒否します。

デフォルトでは、`fetch` は `GET` メソッドを使用してリクエストを行い、リクエストボディを含めません。2番目の引数として追加オプションを持つオブジェクトを渡すことで、異なる設定を行うことができます。たとえば、このリクエストは `example/data.txt` を削除しようとします。

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

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

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

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

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

HTTPサンドボックス

WebページスクリプトでHTTPリクエストを行うと、セキュリティに関する懸念が再び生じます。スクリプトを制御する人は、スクリプトが実行されているコンピューターの所有者と同じ関心を持っているとは限りません。具体的には、私が *themafia.org* にアクセスした場合、そのスクリプトがブラウザからの識別情報を使用して *mybank.com* にリクエストを行い、すべてのお金を振り替えるように指示することを望んでいません。

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

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

Access-Control-Allow-Origin: *

HTTPの理解

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

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

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

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

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

セキュリティとHTTPS

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

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

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

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

フォームフィールド

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

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

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

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

text単一行テキストフィールド
password`text` と同じですが、入力されたテキストを非表示にします
checkboxオン/オフスイッチ
color
dateカレンダーの日付
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="color" value="orange"> (color)</p>
<p><input type="date" value="2023-10-13"> (date)</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ドキュメントのほとんどの要素とは異なり、フォームフィールドは*キーボードフォーカス*を取得できます。クリックしたり、Tabキーで移動したり、他の方法でアクティブ化したりすると、現在アクティブな要素になり、キーボード入力の受信者になります。

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

JavaScriptでは、`focus`メソッドと`blur`メソッドを使用してフォーカスを制御できます。 `focus`は呼び出されたDOM要素にフォーカスを移動し、`blur`はフォーカスを解除します。 `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キーを押して次のフォーカス可能な要素に移動し、Shift + Tabキーを押して前の要素に戻ることで、ドキュメント内を移動できます。デフォルトでは、要素はドキュメントに表示される順序でアクセスされます。 `tabindex`属性を使用してこの順序を変更することができます。次のドキュメント例では、ヘルプリンクを介さずに、テキスト入力からOKボタンにフォーカスをジャンプさせることができます。

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

デフォルトでは、ほとんどの種類のHTML要素はフォーカスできません。任意の要素に`tabindex`属性を追加して、フォーカス可能にすることができます。 `tabindex`が0の場合、フォーカス順序に影響を与えることなく、要素をフォーカス可能にします。

無効化されたフィールド

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

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

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

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

フォーム全体

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

フォームフィールドの`name`属性は、フォームが送信されたときにその値が識別される方法を決定します。また、フォームの`elements`プロパティにアクセスする際のプロパティ名としても使用できます。`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キーを押しても同じ効果があります。

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

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

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

第2王朝の最後のファラオであるカセケムイに関する記事を書いていますが、彼の名前のつづりに苦労しているとします。次のコードは、F2キーを押すと文字列「Khasekhemwy」を挿入するイベントハンドラーを使用して、`<textarea>`タグを接続します。

<textarea></textarea>
<script>
  let textarea = document.querySelector("textarea");
  textarea.addEventListener("keydown", event => {
    if (event.key == "F2") {
      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>`タグでは、ユーザーは1つのオプションだけでなく、任意の数のオプションを選択できます。通常のセレクトフィールドは_ドロップダウン_コントロールとして描画され、アクティブでないオプションは開いたときにのみ表示されますが、`multiple`が有効になっているフィールドは同時に複数のオプションを表示し、ユーザーが個別に有効または無効にすることができます。

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

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

この例では、`multiple`セレクトフィールドから選択された値を抽出し、それらを使用して個々のビットから2進数を構成します。複数のオプションを選択するには、Ctrlキー(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", () => {
    let {selected} = state;
    setState({
      notes: {...state.notes, [selected]: note.value},
      selected
    });
  });
  document.querySelector("button")
    .addEventListener("click", () => {
      let name = prompt("Note name");
      if (name) setState({
        notes: {...state.notes, [name]: ""},
        selected: name
      });
    });
</script>

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

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

この例の ... 構文は、古い state.notes のクローンであるが、1 つのプロパティが追加または上書きされた新しいオブジェクトを作成するために使用されます。スプレッド構文を使用して、最初に古いオブジェクトのプロパティを追加し、次に新しいプロパティを設定します。オブジェクトリテラルの大括弧表記は、名前が動的な値に基づくプロパティを作成するために使用されます。

localStorage に似た別のオブジェクトとして、sessionStorage があります。2 つの違いは、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)}`);
});

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

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

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

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

localStorage オブジェクトと sessionStorage オブジェクトを使用して、ページの再読み込み後も存続する形で情報を保存できます。最初のオブジェクトはデータを永久に(またはユーザーがクリアを決めるまで)保存し、2 番目のオブジェクトはブラウザが閉じられるまでデータを保存します。

演習

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

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

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

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

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

// Your code here.
ヒントを表示...

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

偽のメディアタイプをリクエストすると、コード 406、「受け入れ不可」のレスポンスが返されます。これは、サーバーが 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>
ヒントを表示...

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

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

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

コンウェイのライフゲーム

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

*隣接セル*は、斜めに隣接するセルを含め、隣接するセルとして定義されます。

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

適切と思われるデータ構造を使用して、このゲームを実装します。Math.random を使用して、グリッドにランダムなパターンを初期に設定します。チェックボックスフィールドのグリッドとして表示し、横にボタンを配置して次の世代に進みます。ユーザーがチェックボックスをオンまたはオフにすると、次の世代を計算するときに変更が反映される必要があります。

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

<script>
  // Your code here.
</script>
ヒントを表示...

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

マトリックスの表現は、幅×高さの要素を持つ単一の配列で、値を行ごとに格納することで実現できます。例えば、5行目の3番目の要素は(ゼロベースのインデックスを使用)、位置4×*幅* + 2に格納されます。生きている隣接セルの数は、2つのネストされたループを使用して、両方の次元で隣接する座標をループすることで数えることができます。フィールド外のセルを数えないように、また、隣接セルを数えている中心のセルを無視するように注意してください。

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

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

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