導入

はじめに

はじめに

読者の皆様へ

本書は、情報デザインとプログラミングを学んできた筆者自身の経験をもとに、読者が自主的に学習・制作を進めるための知識と考え方を伝えることを目的としています。大学の授業だけでは、技術に関する知識が断片的になり、それぞれのつながりが見えにくくなることがあります。また、生成AIを利用すれば容易にプログラムを作成できますが、その仕組みや設計の根拠を理解しないまま制作が進むこともあります。

本書では、ブラウザの「新しいタブページ」を題材に、Webページの設計から実装・検証までを学びます。解説は段階的に進めますが、実際の開発は必ずしも順序どおりではありません。試行錯誤を繰り返すこともあれば、生成AIに実装を任せることもあります。重要なのは、すべてを自分で書くことではなく、なぜその技術や設計を選んだのか、自分の言葉で説明できることです。また、情報の根拠を明確にするため、公式資料や大同大学図書館で利用できる書籍などを参照し作成しました。仕組みの解説には、できる限り図やイラストを用いています。

プログラミングは目的ではなく、望むものを実現するための手段にすぎません。学ぶだけで満足せず、自ら考え、試し、作り上げてください。やがて本書の内容すら超えていくことを期待しています。

第1章

ページを設計する

第1章 作りたいページを設計する

1.1 New Tab:コードを書く前に考えること

本書では、検索とニュースの確認を一つの画面にまとめた「New Tab」というWebアプリを教材にします。New Tabとは、ブラウザで新しいタブを開いたとき、最初に使うことを想定したページです。調べたいことが浮かんだらすぐ検索し、少し時間があるときにはニュースを確認できる、そんな開始画面を目指します。

画面の目的が曖昧なままコードを書き始めると、何を優先して作るべきか、できあがったものが使いやすいかを判断しにくくなります。デザインの制作でラフスケッチを描くように、Web制作でも先に画面の骨格を考えることができます。この段階ではコードを一行も書けなくて構いません。

本章の目標は、「利用場面から必要な情報を選び、複数の設計案を比較して、その選択理由を説明できるようになること」です。

1.2 利用場面から操作を考える

New Tabは繰り返し開くことを想定した画面です。本書では、次の二つの利用場面を出発点にします。

場面A:すぐに検索したい。 レポート中に知らない言葉を検索するため、新しいタブで検索語を入力して実行します。検索欄が見つけやすく、入力までの操作が少ないことが重要です。

場面B:ニュースを確認したい。 作業の合間に記事を探し、必要ならリンクを開きます。見出しに加えて説明や配信元があると、記事を選びやすくなります。

ここで、機能を数える前に「最初に何をしたいか」に注目します。本書では検索を主な入口とし、ニュースは必要なときに表示する方針を採用します。これはWebの仕様で決まっていることではなく、想定した利用場面に合わせた筆者の設計判断です。

1.3 情報の優先順位を決める

画面に置く情報や操作を整理するため、まず「利用者がすること」と「そのために必要なもの」を対応させます。

画面主な操作必要な情報・部品
検索語句を入力して送信する入力欄、検索ボタン
ニュース見出しを見て記事を選ぶ見出し、説明、画像、配信元、記事へのリンク

この表は、完成した機能の一覧というより、設計時の確認表です。たとえば検索欄は、装飾が少なくても「文字を入力する場所」と分かる必要があります。ニュースでは、画像だけでなく見出しが読めること、記事を選ぶための手がかりがあることが重要です。

情報の優先順位とは、画面を使う目的に対して、何を先に見せ、何を後から見せるかという考え方です。

1.4 画面の設計:二つの案を比べる

構成を決めるときには、最初に思いついた案だけで進まず、比べてみます。今回は、検索とニュースを上下に並べる案と、タブで切り替える案を考えます。

検索フォームとニュースを上下に並べる案Aと、検索・ニュースのタブで表示を切り替える案Bのワイヤーフレーム。案Bは検索タブが選択中の状態を描き、ニュースパネルは別状態として示す。

図 V-01-001:検索とニュースを配置する二つの案。いずれも設計用のワイヤーフレームであり、実際に動く画面ではありません。

このような、部品の配置や大きさの関係を簡単な線や箱で描いた図をワイヤーフレームと呼びます。色やフォントを細かく決める前に、どこに何を置くかを確認するための図です。紙とペンでも、図形を描けるアプリでも作れます。

比較する観点案A:上下に並べる案B:タブで切り替える
検索を始めるページ上部の検索欄を使える初期表示を検索にすれば検索欄を使える
ニュースを見つける同じページ内に見えているニュースタブを選ぶ必要がある
一度に見える情報検索とニュースの両方選択した側の情報だけ
画面の縦の長さ記事数によって長くなりやすい検索を表示している間は短くできる
設計・実装で注意すること情報の区切りと見やすさ切り替え操作、選択状態、操作しやすさ

この比較から、本教材では案B:タブで切り替える構成を採用します。検索を主な入口とする利用場面を重視したためです。ただし、ニュースを常に確認したい場合は案Aが適しており、適切な構成は利用目的によって変わります。

1.5 「画面がどう変わるか」も設計する

画面設計では、最初に見える静止画だけでなく、操作の前後も考えます。本教材のタブ構成には、少なくとも次の状態があります。

状態画面に表示する内容利用者が次にできること
最初に開いたとき検索欄と検索ボタン語句を入力し、検索する
ニュースタブを選んだときニュースの表示領域記事を確認する
ニュースの取得中「取得しています…」などの案内処理中であると理解する
記事が表示できないとき記事がない、または通信失敗を知らせる案内状況を把握し、必要なら別の操作を試す

ニュースは外部から取得する予定なので、要求した直後に必ず記事が表示されるとは限りません。通信に時間がかかったり、記事が返らなかったり、接続に失敗したりする場合があります。設計の段階からこうした状態を想定しておくと、後の章で取得中やエラーの表示を作る際に、何を伝えるべきか考えやすくなります。

1.6 画面の骨格を部品に分ける

案Bを採用したら、次は画面を部品に分けてみます。たとえば「検索/ニュースのタブ」「アプリの見出し」「検索の入力欄とボタン」「ニュースを表示する領域」というまとまりです。これらを整理すると、後からHTMLで内容の構造を作りやすくなります。

第2章以降で使うHTMLの考え方を、ここで少しだけ先取りしてみましょう。後の章で実際に登場するニュースタブには、次の記述があります。

<button id="tab-news" type="button" class="filetab"
  role="tab" aria-selected="false" aria-controls="panel-news" tabindex="-1">2 ニュース</button>

これは後続章で使うHTMLの抜粋です。「2 ニュース」タブとpanel-newsが対応している点だけ確認しましょう。

1.7 HTML・CSS・JavaScriptは何を担当するか

本書では、役割を分けながらNew Tabを作っていきます。設計段階で分担を見通しておくと、完成後に調べる場所を考えやすくなります。

技術主な役割New Tabで担当する例
HTML内容と構造を表すタブ、見出し、検索フォーム、ニュース領域
CSS見た目と配置を整える配色、余白、タブの外観、画面幅に応じた配置
ブラウザ側のJavaScript操作や状態の変化を扱うタブ切り替え、検索欄へのフォーカス、ニュースの受信と表示
Node.js側のJavaScriptサーバーで動く処理を担当する外部ニュースAPIへの問い合わせ、APIキーの管理

Node.jsはブラウザ外でJavaScriptを動かす環境、APIは外部サービスの窓口です。ニュース取得に使うAPIキーはブラウザ側へ埋め込まず、サーバー側で管理します。

これはすべてのWebページにNode.jsが必要という意味ではありません。今回のニュース取得という目的に合わせた構成です。第1章では技術の詳細よりも、画面に見えるものと、その裏で動く処理は分けて考えられることを押さえましょう。

1.8 思いつかない場合:既存の画面から学ぶ

どんな画面にすればよいか思いつかないときは、普段使っているWebサイトやアプリを観察してみましょう。 「ネットサーフィンをする」という方法も、観察する目的を持つと制作に結び付きます。

たとえば、検索サイトやニュースサイト、普段使っている学内システムを開き、次の点を注視してみましょう。

  • 最初に目が向かうものは何か。入力欄、見出し、ボタンのどれが目立つか。
  • 操作する前と後で、画面のどこが変わるか。
  • 情報が取得できないとき、どのような案内が表示されるか。
  • キーボードだけで目的の操作を進められるか。

一つのサイトをそのまま模写するのではなく、何を参考にし、なぜ自分の画面に取り入れたいのかを言葉にします。たとえば「検索欄を大きくする」「ニュースは必要なときだけ見せる」のように、見た目ではなく役割として記録するとよいでしょう。参考にしたサイトの名前やURL、確認した日付も残しておけば、後から自分の判断の根拠がぶれることを防げます。

AIは画面案の比較に使えますが、利用者を実際に調査した結果ではありません。採用理由は利用場面と観察をもとに説明しましょう。

1.9 試す:二つのワイヤーフレームを作る

ここでは、紙とペン、または図形ツールを使って設計を実験します。図 V-01-001を参考に、次の順番で進めてください。

  1. 変更前を確認する。 何も配置していない紙に、「新しいタブを開いて検索したい」「ニュースの見出しを読みたい」という二つの目的を書きます。
  2. 結果を予測する。 検索とニュースを上下に並べた場合、どちらの作業がしやすそうか、自分の予想を一文で書きます。
  3. 案Aを描く。 上に検索欄、下にニュースの見出しを数件置くワイヤーフレームを描きます。文字や箱は簡略化して構いません。
  4. 案Bを描く。 上に「検索」「ニュース」のタブを置き、最初は検索欄が見える案を描きます。ニュースタブを選んだ後の状態も、もう一枚描きます。
  5. 操作を確認する。 指やペン先を使って、「新しいタブを開いて検索語を入力する」「ニュースの見出しを読む」という流れをそれぞれたどります。どの操作で迷うか、何回タブを切り替える必要があるかを記録します。
  6. 理由を説明し、比較を終える。 最初の予想と比べて、どちらを選ぶかを記録します。案A・案Bの元の図も残しておきましょう。後から変更した理由を振り返る材料になります。

操作数だけで優劣を決めないことも大切です。案Aではニュースが目に入りやすく、案Bでは検索だけに集中しやすいかもしれません。どちらの利点を優先するかは、最初に想定した利用場面で判断します。

1.10 考える:設計の根拠と技術の仕様を分ける

検索を最初に見せるのはHTMLの決まりではなく、本書の利用場面に基づく設計です。タブの操作方法には別途、参照できる指針があります。

W3CのWAI-ARIA Authoring Practices Guideには、タブと対応するパネルをどのように表し、キーボードで操作できるようにするかを説明した「Tabs Pattern」があります。第1章でその属性名を覚える必要はありませんが、後から自分の設計が技術的に適切か確かめるための資料があることを知っておきましょう。この資料は、筆者が検索を優先した理由を証明するものではなく、タブという仕組みを実装するときの参考資料です。

MDN Web Docsの「ウェブサイトをどんな外見にするか」でも、コードを書く前に目的や内容を整理し、簡単なスケッチを描く方法が紹介されています。こちらは設計作業を進めるための学習資料です。資料に書かれていることと、自分が実際に確認したこと、まだ検証していない予想を分けておくと、AIや他人の提案を採用するときにも判断しやすくなります。

1.11 章末課題:自分の設計案を説明する

実験で作った二つの案のうち、一つを選んで短い設計メモにまとめてください。次の四点を含めます。

  • 利用場面:誰が、いつ、何のために使う画面か。
  • 情報の優先順位:最初に見せるものと、後から見せるものは何か。
  • 採用理由:別の案と比べて、なぜその構成を選んだか。
  • 未確認のこと:実際の利用者に試してもらうまで分からない点は何か。

余裕があれば、学友にワイヤーフレームを見せ、検索を始める場所とニュースへ進む場所を指してもらいましょう。説明せずに伝わったかを観察し、分かりにくかった部分を一か所だけ直します。変更後、同じ操作をもう一度確かめます。

この課題は、デザインの正解を当てるためではありません。「こう使ってほしい」という予想を、他者の反応や自分の操作で確かめ、小さく改善する練習です。この方法は、第7章のデバッグや第8章の改善サイクルにもつながります。

1.12 ふりかえり:設計をHTMLへつなぐ

本章では、New Tabで何をしたいかを整理し、検索とニュースを上下に並べる案とタブで切り替える案を比較しました。検索を最初に見せるという方針は、今回の利用場面から選んだ設計判断です。また、取得中や失敗時のように、通常の画面以外の状態も考えておく必要があることを学びました。

最初の段階で大切なのは、作る目的を言葉にし、画面の部品と操作の関係を描き、採用した理由を説明できることです。設計は一度決めたら変えてはいけないものではなく、作りながら、使いながら見直していくものです。

第2章

HTMLでページの骨格を作る

第2章 HTMLでページの骨格を作る

画面の構造を理解し、要素同士の関係を読み解く

この章で学ぶこと

Webページには、見出しや文章、画像、入力欄、ボタンなどの要素が配置されています。本章では、HTMLの役割を学び、実際のコードと画面上の対応を読み解きます。

本章で扱うindex.htmlには、検索機能とニュース表示領域が用意されています。ただし、現時点ではCSSやJavaScriptを読み込んでいません。そのため、見た目の調整やタブの切り替えはまだ行わず、画面の構造とHTMLが持つ基本的な機能に注目します。

2.1 HTMLとは何か

2.1.1 Webページを構成する3つの技術

本書におけるWebページの制作では、主にHTML、CSS、JavaScriptという3つの技術を使用します。それぞれは別の役割を持っています。

Webページを構成する主な技術とその役割

HTMLは、見出しや文章、入力欄などの構造と意味を定義します。CSSは、それらの大きさや色、余白、配置などを調整します。JavaScriptは、ボタンの操作に応じて画面を切り替えたり、データを取得したりする処理を担当します。

検索フォームでは、HTMLで検索欄とボタンを用意し、CSSで幅や色を整え、JavaScriptで入力に応じた検索候補などを追加できます。

ただし、JavaScriptがなければすべての操作ができないわけではありません。HTML自体にも、リンク先への移動やフォームの送信など、ブラウザが標準で処理できる機能が用意されています。本章の検索フォームは、その一例です。

3つの技術を一度に理解しようとすると、コードのどの部分が画面のどの働きを担当しているのか、分からなくなりやすいものです。そこで本書では、最初にHTMLで画面の構造を理解し、その上にCSSによるデザインとJavaScriptによる動作を段階的に追加していきます。

2.1.2 タグと要素

HTML(.html 等)は、文章やコンテンツに意味を与えブラウザーなどに解釈をさせるためのマークアップ言語です。その基本となるのが、タグと呼ばれる記述です。

<h1>New Tab</h1>
<p>Googleで検索できます。</p>
<button>検索</button>

ブラウザでは、New Tabが見出しとして扱われ、その下に文章とボタンが表示されます。このとき、<h1>や<p>、<button>がタグです。一般的に、開始タグと終了タグ、それらに囲まれた内容を合わせて「要素」と呼びます。 開始タグは要素の始まりを、終了タグは終わりを示します。なお、inputやmetaなど、終了タグを持たない要素も存在します。

HTMLの要素には、それぞれ定められた意味があります。例えば、h1は見出し、pは段落、buttonはボタンを表します。ここで重要なのは、要素は単に見た目を変えるための記述ではないということです。

h1は見出し、buttonは操作できる部品を表します。HTMLは見た目だけでなく、要素の意味をブラウザや支援技術へ伝えます。

HTMLで使用できる標準的な要素や属性は、WHATWGが管理するHTML Living Standardなどで定義されています。制作する人が自由に名前を付けられるものと、仕様で意味が決められているものを区別することが大切です。

2.1.3 属性によって要素に情報を追加する

要素には、属性と呼ばれる追加情報を記述できます。

<button id="tab-search" class="filetab" type="button">
  検索
</button>

buttonという要素に対して、id、class、typeという3つの属性が指定されています。属性は通常、開始タグの中に記述します。id="tab-search"のように、属性名と値を組み合わせて指定する形式が一般的です。

属性役割
id要素を識別するための名前を付ける
class複数の要素を分類し、CSSなどから参照できるようにする

idとclassは似ていますが、使い方には違いがあります。idは、同じ文書の中で一意になるように指定する識別子です。一方、classは同じ値を複数の要素に付けられます。 要素の役割に応じて、ほかの属性を追加することもあります。

本章のHTMLでは、filetabというclassが検索タブとニュースタブの両方に使用されています。それに対して、tab-searchとtab-newsというidは、それぞれのボタンを区別するために使用されています。この違いは、後ほどタブとパネルの関係を読み解く際にも重要になります。

2.2 HTML文書の基本構造

2.2.1 VS CodeでHTMLを作成する

ここでは、Visual Studio Code(以下、VS Code)を使用してHTML文書の基本形を確認します。新しいファイルを作成し、emmet.htmlという名前で保存してください。

ファイル名の末尾にある.htmlは、HTML文書であることを示す拡張子です。VS Codeはこの拡張子からファイルの種類を判断し、HTMLの入力補助や色分け表示などを行います。

ファイルを開いて半角の!を入力すると、Emmetの補完候補が表示されます。候補を選んでEnterキーを押すと、HTML文書の基本構造が挿入されます。Emmetは、短い省略記法からHTMLやCSSのコードを展開する仕組みで、VS Codeに標準で組み込まれています。展開内容は設定によって異なるため、言語設定やタイトルは制作するページに合わせて確認・変更します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

</body>
</html>

図2-2-1 HTML文書の基本的な構造

2.2.2 headとbodyの違い

生成されたコード図2-2-1を見てみましょう。大きく分けるとheadとbodyの2つの領域に別れることがわかります。

headには、文字コードやページタイトルなど、文書を扱うための情報を記述します。charsetは文字コードをUTF-8に指定し、viewportは端末の表示領域を指定します。一方、bodyには、利用者が読む文章や操作するボタンなど、ページの内容を記述します。

2.2.3 Emmet記法を利用して構造を読む

Emmetはコードの入力を補助するだけでなく、HTMLの親子関係を簡潔に表現することにも使えます。

div.shell>main.container>h1

これは、次のHTMLへ展開できます。

<div class="shell">
  <main class="container">
    <h1></h1>
  </main>
</div>

Emmetでは、.はclass、#はidの指定に使用します。また、>は子要素、+は同じ階層に並ぶ要素を表します。

section.panel#panel-search
<section class="panel" id="panel-search"></section>

本書では、HTMLの構造を短く示すためにEmmet形式の表現を使用する場合があります。ただし、Emmetで使う記号と、実際のHTMLで使うタグや属性は別のものです。コードを読む際には混同しないようにしてください。

2.3 HTMLの構造を読み解く

ここからは、実際にNew Tabアプリのindex.htmlを読み解いていきます。本章では、コードを上から一行ずつ説明するのではなく、最初にページ全体の構造を確認し、その後で各領域の役割を調べていきます。これは、デザイン制作で画面全体のレイアウトを考えてから細部の作業を行うのと同様です。

2.3.1 親要素と子要素

HTMLでは、ある要素の内側に別の要素を配置できます。このような関係を、親要素と子要素と呼びます。

divの内側にmainがあり、さらにその内側にh1が配置されています。divから見ればmainは子要素、mainから見ればh1が子要素です。複数の要素が内側に重なる構造を「入れ子」や「ネスト」と呼びます。

<div class="shell">
  <main class="container">
    <h1>New Tab</h1>
  </main>
</div>

HTMLの構造が複雑になると、どこからコードを書き始めればよいのか迷う場合があります。そのようなときには、親要素から順番に考えると整理しやすくなります。

まず、ページ全体をまとめる領域を用意します。次に、その中にメニューやメインコンテンツなどの大きな領域を配置します。最後に、それぞれの領域の中に見出しやボタンを追加するといった順序を考えて見てください。

なお、親子関係は単にコードを見やすくするためだけのものではありません。例えばCSSでは、親要素を基準として子要素の配置を指定したり、一部の親に対するスタイルの指定が子孫要素へと影響を与えることがあります。

2.3.2 ページ全体の構造を確認する

以下は、添付されたindex.htmlから、ページの主要な構造を抜き出したものです。

<div class="shell">
  <nav class="tabs" role="tablist">
    <button id="tab-search" class="filetab" role="tab">検索</button>
    <button id="tab-news" class="filetab" role="tab">ニュース</button>
  </nav>

  <main class="container">
    <section id="panel-search" class="panel" role="tabpanel">
      <!-- 検索フォーム -->
    </section>
    <section id="panel-news" class="panel" role="tabpanel" hidden>
      <!-- ニュース表示領域 -->
    </section>
  </main>
</div>

最も外側にあるのがdiv.shellです。その中に、nav.tabsとmain.containerという2つの領域が配置されています。

navは検索とニュースを選ぶタブのまとまり、mainは検索・ニュース領域を含む主要部分です。

これをテキストで表現するなら以下のようになるでしょう。

shell(ページ全体)
├── nav.tabs
│   ├── button#tab-search(検索)
│   └── button#tab-news(ニュース)
└── main.container
    ├── h1(New Tab)
    ├── p.subtitle(説明文)
    ├── section#panel-search(検索フォーム)
    └── section#panel-news(ニュース領域・初期状態では非表示)

このように、コード全体の構造を見るだけでも、アプリケーションにどのような機能が用意されているかを読み取れる場合があります。逆に、アプリケーションを制作する際には、最初に構造を考えることが重要です。

2.3.3 div、nav、main、sectionを使い分ける

divは、それ自体には特別な意味を持たない汎用な要素です。 主に、CSSによるレイアウトやプログラムからの操作を目的として、複数の要素をまとめる際に使われます。 他の要素で代替可能な場合は極力使用しないようにしましょう。

これに対して、navはナビゲーション、mainは主要コンテンツ、sectionは内容を主題ごとにまとめた区画を表します。

<section id="panel-search">
  <!-- 検索に関する内容 -->
</section>

2.3.4 classやidから構造を予測する

<button id="tab-search" class="filetab">検索</button>
<section id="panel-search" class="panel"></section>

この2つには、searchという共通した名前が含まれています。名前の付け方から、両者が関連していると予想できるでしょう。

idやclassの名前は、制作者が決めることができます。panel-searchという名前はHTMLの仕様で特別な意味が決められているわけではありません。 それでも、名前から役割を推測できるようにすると、コードを読みやすくなります。panel-searchのように役割を示す名前を付けるルールを命名規則と呼びます。

考えてみよう: tab-searchというidをbutton1に変更した場合、ブラウザの処理は同じでも、人間にとっての読みやすさは変わるでしょうか。

2.4 検索フォームの仕組みを理解する

次は、main.containerの内側を詳しく見ていきます。New Tabアプリには、Googleで検索を行うための入力欄と検索ボタンがあります。これらは、HTMLのform要素によってまとめられています。

2.4.1 見出しと検索領域

<main class="container">
  <h1>New Tab</h1>
  <p class="subtitle">「検索」はGoogleに送信されます。</p>

  <section id="panel-search" class="panel"
           role="tabpanel" aria-labelledby="tab-search">
    <!-- 検索フォーム -->
  </section>
</main>

h1はページの見出し、pはその下に表示する説明文です。これらは検索パネルの外側に配置されています。 そのため、検索画面からニュース画面に切り替える仕組みを追加した場合でも、見出しと説明文は共通の領域として扱うことができます。

一方、sectionは検索パネルとして独立しています。このような構造を採用すると、共通して表示したい要素と、切り替えて表示したい要素を区別できます。

制作をする際にこのような拡張可能な構造になっているかは視点として持っておくべきです。

2.4.2 form要素の役割

<form class="search-form" action="https://www.google.com/search"
      method="get" role="search">
  <label class="sr-only" for="search-input">検索キーワード</label>
  <input class="search-input" id="search-input" name="q" type="search"
         placeholder="Googleで検索" autocomplete="off"
         enterkeyhint="search" required>
  <button class="search-button" type="submit">検索</button>
</form>

大きく分けると、入力内容を送信するためのform、文字を入力するためのinput、送信を実行するbuttonが含まれています。 これらをHTMLの標準的な機能として組み合わせることで、JavaScriptを使わずに検索フォームを動かすことができます。

action="https://www.google.com/search"
method="get"

actionはフォームの送信先、methodは送信方法です。GET方式では、フォームのデータを送信します。 例えば、検索欄にHTMLと入力して検索ボタンを押すと、次のようなURLへの移動が行われます。

https://www.google.com/search?q=HTML

?以降がクエリ文字列です。ここで重要なのが、入力欄に指定されているname="q"です。

<input name="q" type="search">

nameは、フォームの送信時に使用されるデータの項目名です。Googleの検索用URLでは、検索語をqという名前のパラメータで受け取ります。そのため、入力欄にname="q"を指定しています。

入力欄に「HTML」と入力
        ↓
input の name="q"
        ↓
form の GET 送信
        ↓
https://www.google.com/search?q=HTML
```*図2-6 HTMLの検索フォームがデータを送る流れ*

### 2.4.3 input要素と属性

```html
<input class="search-input"
       id="search-input"
       name="q"
       type="search"
       placeholder="Googleで検索"
       autocomplete="off"
       enterkeyhint="search"
       required>
属性役割
type="search"検索語の入力欄であることを示す
name="q"送信する値の項目名を指定する
id="search-input"要素を識別する
placeholder入力欄が空のときに補助的な文字を表示する
autocomplete="off"ブラウザによる入力補完を抑制するよう指定する
enterkeyhint="search"対応する端末で、入力用キーの表示を検索向けにするヒントを与える
required入力を必須とする

ここで注目したいのは、requiredです。この属性が指定された入力欄では、値が空の場合、通常のフォーム送信時にブラウザが入力を求めます。

実際に検索欄を空のまま検索ボタンを押してみてください。入力を求めるメッセージなどが表示されるはずです。

この動作も、HTMLとブラウザに用意された標準的な機能です。ただし、requiredは単にブラウザ側の検証であることは留意が必要です。

2.4.4 labelと入力欄の関係

<label class="sr-only" for="search-input">検索キーワード</label>
<input id="search-input" name="q" type="search">

labelは、入力欄などのフォーム部品に説明を付けるための要素です。forには対応する入力要素のidを指定します。for="search-input"とid="search-input"が一致することで、ラベルと入力欄が関連付けられます。

label [for="search-input"]
            ↕
input [id="search-input"]
```*図2-7 labelのfor属性とinputのid属性の関係*

表示されているラベルをクリックした際、対応する入力欄へフォーカスを移せます。また、スクリーンリーダーなどのweb支援技術に、入力欄の名前を伝えることにも役立ちます。


### 2.4.5 button要素の役割

```html
<button class="search-button" type="submit">検索</button>

buttonは、利用者が操作するためのボタンを表す要素です。type="submit"は、所属するフォームを送信するためのボタンであることを示します。

一方、タブに使用されるボタンは次のようになっています。

<button type="button">ニュース</button>

type="button"では、フォームの送信は行いません。また、クリックした際に特定の画面を表示するような動作が自動的に追加されるわけでもありません。そのような独自の処理は、後章で実装します。

2.5 タブとパネルの対応を理解する

New Tabアプリには、検索とニュースを切り替えるための2つのタブが用意されています。しかし、本章のHTMLだけでは、タブをクリックしても表示内容は切り替わりません。それでもタブとパネルを用意するのは、操作する部分と、操作によって表示する部分を、あらかじめ対応付けておくためです。

2.5.1 タブのHTMLを確認する

<nav class="tabs" role="tablist" aria-label="新しいタブのメニュー">
  <button id="tab-search" type="button" class="filetab"
          role="tab" aria-selected="true"
          aria-controls="panel-search" tabindex="0">1 検索</button>
  <button id="tab-news" type="button" class="filetab"
          role="tab" aria-selected="false"
          aria-controls="panel-news" tabindex="-1">2 ニュース</button>
</nav>

roleは、要素がアクセシビリティ上どのような役割を持つのかを示す属性です。role="tablist"は複数のタブのまとまり、role="tab"は個々のタブを示します。

aria-selectedは選択状態を示す属性で、検索側にはtrue、ニュース側にはfalseが指定されています。ただし、aria-selectedを書いただけで、画面の色が変わったり、パネルが切り替わったりするわけではありません。 ARIA属性は主に、要素の役割や状態、関係を支援技術へ伝えるためのものです。

2.5.3 aria-controlsで対応するパネルを示す

検索タブには次の属性があります。

aria-controls="panel-search"

対応するパネルには同じ値のidがあります。

<section id="panel-search" role="tabpanel"
         aria-labelledby="tab-search"></section>

ニュース側も同じ規則です。

tab-search                       tab-news
aria-controls="panel-search"     aria-controls="panel-news"
       │                                │
       ▼                                ▼
panel-search                      panel-news
id="panel-search"                id="panel-news"

図2-9 タブのaria-controlsとパネルのidによる対応関係

aria-controlsは、要素間の関係を表す属性であり、JavaScriptの処理を自動的に実行するものではありません。aria-controls="panel-news"と書いただけでは、ニュースタブをクリックしてもニュースパネルは表示されません。

第4章では、JavaScriptでタブの操作を受け取り、対応するパネルの表示状態を変更します。その際、aria-controlsの値を利用してパネルを探す実装も可能ですが、実際にどの属性を参照するかは実装次第です。

2.5.4 aria-labelledbyと要素の名前

パネル側には、次の属性も指定されています。

aria-labelledby="tab-search"

これは、そのパネルのアクセシブルな名前を、tab-searchというidを持つ要素から取得することを示します。今回の場合、検索パネルは「1 検索」というボタンによって名前付けされています。

2.5.5 tabindexとキーボード操作

tabindex="0"
tabindex="-1"

tabindexは、キーボードによるフォーカスの扱いに関係する属性です。通常、tabindex="0"の要素はTabキーによる順次フォーカス移動の対象になります。一方、tabindex="-1"の要素は通常のTabキー移動の対象から外れますが、JavaScriptなどによってフォーカスを移すことはできます。

本章のHTMLでは検索タブに0、ニュースタブに-1が指定されています。ただし、現時点ではJavaScriptによる矢印キー操作や選択状態の更新は実装されていません。タブの構造と初期状態を用意した段階と考えてください。

2.6 ニュース領域の構造を理解する

<section id="panel-news" class="panel" role="tabpanel"
         aria-labelledby="tab-news" hidden>
  <div class="news-card">
    <div class="news-heading">
      <h2 id="news-title">ニュース</h2>
    </div>
    <p id="news-genre" class="news-genre">
      第4章以降でタブを動かします。
    </p>
    <div id="news" aria-live="polite"></div>
  </div>
</section>

sectionはニュースパネル全体、h2はニュース領域の見出し、pは説明文です。また、news-cardというclassが付いたdivはニュースの内容をまとめるために用意されています。

2.6.1 hidden属性による非表示

<section id="panel-news" hidden>

hiddenは要素を非表示にするHTML属性です。検索パネルにはなく、ニュースパネルにあるため、初期表示は検索画面です。

2.6.2 ニュース表示用の空のdiv

<div id="news" aria-live="polite"></div>

aria-live="polite"は、内容が更新された際に、支援技術が適切なタイミングで変更を通知できるようにするための属性です。ただし、実際の通知方法はブラウザや支援技術の組み合わせなどによって異なります。

2.6.3 将来の機能を意識した構造

ニュースパネルには、パネルそのもの、ニュースの見出し、補助情報、本文などを追加する領域が用意されています。まだ動作していない機能であっても、その機能が必要とする画面構造を先に設計しているわけです。

最初からすべての機能を完成させようとすると、HTML、CSS、JavaScriptを同時に考える必要があります。一方、必要な領域をHTMLで用意しておけば、後から各機能を段階的に追加できます。

2.7 ブラウザで動作を確認する

コードを理解する上で重要なのは、説明を読むことだけではありません。実際に動かしてみて、予想した結果と一致するかを確認することも必要です。

2.7.1 HTMLをブラウザで開く

02-html/index.htmlを開きましょう。CSSがないため標準の見た目で、ニュースタブを押しても切り替わりません。まだJavaScriptによる表示変更がないためです。

2.8 開発者ツールを使って構造を調べる

開発者ツールは、WebページのHTMLやCSSを確認し、動作を調査する機能です。Chromeでは右クリックして「検証」を選ぶか、Ctrl+Shift+Iを押して開きます。ここでの変更は元のHTMLファイルには保存されず、ページを再読み込みすると元に戻ります。

2.8.1 ElementsでHTMLを確認する

開発者ツールで「Elements」またはそれに相当する画面を探してください。そこにはhtml、head、bodyなどが階層構造で表示されます。

これはブラウザが現在扱っているDOMと呼ばれる文書構造です。DOMはHTMLをもとに構築されますが、ソースコードと完全に同じ表現になるとは限りません。

まずdiv.shellを探してください。その内側にnav.tabsとmain.containerがあり、さらに見出しや検索パネル、ニュースパネルが存在することを確認します。

2.8.2 タブとパネルのidを照合する

検索タブのaria-controls="panel-search"と、検索パネルのid="panel-search"を照合します。ニュースタブとニュースパネルについても、同じ手順で確認してください。

属性がどの要素を参照しているのかを追いかけることが大切です。

2.9 AIと参考資料を使って理解を深める

コードの意味が分からなかったり、どのように調べればよいのか迷ったりした場合、生成AIに質問して説明を得ることもできます。

HTMLのaria-controlsはどのような役割を持っていますか。idとの関係を、初学者にも分かるように説明してください。

このような質問は、分からない用語を理解するための手掛かりになります。ただし、AIによる説明が常に正しいとは限りません。

2.9.1 仕様と実装を区別する

例えば、

「タブを作るにはaria-controlsが必要ですか」と質問したとします。

この問いに答える際には、何を根拠に「必要」と判断するのかを区別することが重要です。

HTMLの標準的なbutton要素を単独で使用する場合と、WAI-ARIAのタブパターンに沿ったUIを自作する場合とでは、求められる構造が異なります。

W3CのWAI-ARIA Authoring Practices Guideでは、タブとパネルの対応付けとしてaria-controlsを使用するパターンが示されています。一方、単に2つのボタンを並べただけであれば、それらが必ずタブとして機能するわけではありません。

2.9.2 参考資料を確認する

HTMLの要素や属性について調べる際には、MDN Web Docsが参考になります。タブのアクセシビリティ設計については、W3CのWAI-ARIA Authoring Practices Guideを確認するとよいでしょう。

疑問が生じた場合には、まず何が分からないのかを整理し、AIの説明や参考資料を利用して理解を深めるべきです。

2.9.3 自分で確かめる習慣を身につける

今回の学習では、h1の内容を変更すると見出しが変わり、hiddenを削除するとニュース領域が表示され、name="q"を変更するとフォーム送信時の項目名が変わることを確認しました。一方、aria-controlsを変更しても、ページの見た目は通常変化しません。

これらの結果から、HTMLには、画面に直接現れる情報と、画面上では分かりにくい意味や関係の情報が含まれていることが分かります。

2.10 章末演習

次の問いについて、自分で考えてから解答例を確認してください。分からない場合には、index.htmlを読み直したり、ブラウザで動作を確かめたりしましょう。

  1. headとbodyの違いは何でしょうか。
  2. idとclassにはどのような違いがありますか。
  3. 検索欄のname="q"を変更すると、何が変わりますか。
  4. ニュース領域が最初から表示されないのはなぜですか。
  5. aria-controlsを指定しただけで、タブが切り替わらないのはなぜですか。

発展課題:3つ目のパネルを設計する

余裕がある方は、新しく「メモ」というタブとパネルを追加してみてください。最初から動作させる必要はありません。HTMLの構造だけを用意します。

<button id="tab-memo" type="button" class="filetab" role="tab"
        aria-selected="false" aria-controls="panel-memo"
        tabindex="-1">3 メモ</button>
<section id="panel-memo" class="panel" role="tabpanel"
         aria-labelledby="tab-memo" hidden>
  <h2>メモ</h2>
  <p>ここにメモを表示します。</p>
</section>

既存のタブ一覧とパネル一覧の適切な場所へ追加してください。追加した時点では、メモタブをクリックしてもパネルは切り替わりません。しかし、hiddenを一時的に削除すれば、メモ領域が表示されます。

ここで考えてほしいのは、タブを追加する際に、どのような要素や属性をそろえる必要があるかということです。演習後は、元の2つのタブの構成に戻しておきましょう。

2.11 本章のまとめ

本章では、HTMLの基本的な役割から始まり、New Tabアプリのindex.htmlを題材として、画面の構造と要素同士の関係を確認しました。

HTMLは、単に文字やボタンを表示するためのものではありません。文書の見出しや区画、入力フォーム、操作部品などに意味を与え、ブラウザや支援技術がそれらを適切に扱うための土台となります。

また、HTMLの親子関係を理解することで、複雑なコードも大きな構造から順番に読み解けるようになります。

今回のアプリでは、最初にdiv.shellというまとまりがあり、その内側にタブの領域とメインコンテンツが配置されていました。さらにメインコンテンツの中には、検索パネルとニュースパネルが用意されていました。

検索フォームでは、form、input、buttonの組み合わせによって、JavaScriptを使わずに検索語を送信できることを確認しました。

タブとパネルについては、idやARIA属性を利用して対応関係を表す方法を学びました。ただし、対応関係を記述することと、実際に画面を切り替えることは別の作業です。

第3章

CSSでデザインを整える

第3章 CSSでデザインを整える

HTMLの構造と画面の見た目を結び付け、変更の結果を自分で確かめる

3.1 知る:HTMLの骨格にデザインを加える

CSSはHTMLの要素の見た目を指定します。本章では色・余白・配置の指定が画面にどう反映されるかを見て、一か所変更します。

3.2 知る:CSSの基本的な書き方と読み込み方

3.2.1 セレクターと宣言

CSSは、基本的に「どの要素に対して」指定するのかを決め、その要素の「どの見た目を」「どの値にするか」を記述します。

セレクター {
  プロパティ:プロパティ値;
}

という記述の仕方となります。実例を見てみましょう。

.subtitle {
  color: var(--text-muted);
  font-size: 13px;
}

.subtitleはセレクターと呼ばれ、class="subtitle"の要素を選びます。波括弧内のcolorやfont-sizeはプロパティ、右側の値はプロパティ値です。color: var(--text-muted);のような組を宣言と呼びます。次のHTMLは説明文にsubtitleを指定しています。

<p class="subtitle">「検索」はGoogleに送信されます。</p>

HTMLのclassとCSSのセレクターが対応し、プロパティと値が見た目を指定します。複数の指定が重なる場合の優先関係はカスケードと呼びます。この対応を図V-03-001で確認してください。

3.2.2 CSSファイルをHTMLから読み込む

教材のindex.htmlには、head内に次の記述があります。

<link rel="stylesheet" href="style.css">

link要素は、HTML文書と外部の資料を関連付けます。ここではrel="stylesheet"でスタイルシートであることを示し、href="style.css"で読み込むファイルを指定しています。style.cssは次のように同じフォルダーへ配置すると読み込めます。

03-css/
├── index.html
└── style.css

CSSが読み込まれていなくても、HTMLの見出しや検索欄は通常表示されます。ただし、その見た目にはブラウザの標準スタイルが用いられます。この違いは、後の演習で実際に確認します。

3.3 読み解く:画面全体とCSSの対応をつかむ

対象のHTMLはdiv.shellの中にnav.tabsとmain.containerがあり、後者に見出し・説明文・検索・ニュースの領域が入っています。

HTMLの要素とCSSセレクターの対応図

図3-1 HTMLの構造とCSSセレクターの対応。左側のHTML要素に付いたclassが、右側のレイアウト指定の対象になります。

この関係を整理すると、教材のCSSは次のように分担されうることが想像できます。

CSSの対象対応する画面の部分主に指定していること
body, .shell, .containerページ全体と中央の内容領域背景、最大幅、余白
.tabs, .filetab「1 検索」「2 ニュース」のタブ横並び、形、選択中の外観
h1, .subtitle見出しと説明文文字の大きさ、色、中央揃え
.search-form, .search-input, .search-button検索欄と検索ボタン横並び、幅、背景、境界線
.news-card, .news-itemなどニュース表示領域カードや記事の外観(現段階では非表示)

重要なのは、画面に存在する部分と、今見えている部分は同じではないという点です。ニュースパネルにはHTMLでhidden属性が指定され、初期状態では表示されません。hiddenが解除された際のスタイルを先行して書いていることに留意しましょう。

3.4 読み解く:色に名前を付けて再利用する

3.4.1 カスタムプロパティでデザインの基準をまとめる

共通の色をまとめて管理するため、CSSカスタムプロパティを使います。--pageや--surfaceはCSSの値に付けた名前です。

:root {
  color-scheme: light;
  --page: #f1eee7;
  --surface: #fffdf8;
  --surface-muted: #e9e5dc;
  --text: #252522;
  --text-muted: #77746c;
  --border: rgba(40, 38, 32, .15);
  --focus: #487b61;
  --error: #b3261e;
  --radius: 12px;
}
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --page: #202124;
    --surface: #292a2d;
    --surface-muted: #3c4043;
    --text: #e8eaed;
    --text-muted: #bdc1c6;
    --border: rgba(255, 255, 255, .18);
    --focus: #a4d9bb;
    --error: #ffb4ab;
  }
}

:rootは通常のHTML文書でhtml要素を選びます。ここに--page(背景)や--text(文字色)などを定義し、各所から使います。--radiusは角の丸みです。

カスタムプロパティを使うときは、次のようにvar()で参照します。

body {
  background: var(--page);
  color: var(--text);
}
.search-input {
  background: var(--surface);
  border-radius: var(--radius);
}

3.4.2 ダークモードでは何が切り替わるのか

先ほどのコードにある@media (prefers-color-scheme: dark)は、メディアクエリです。閲覧環境などの条件に応じてCSSの一部を適用する仕組みです。ここでは、OSやブラウザがダーク配色を希望している場合に、その中の指定が適用されます。

ライト時の--pageは#f1eee7、ダーク時は#202124です。同じbackground: var(--page)でも、参照する値に応じて背景が変わります。

同じHTMLのライト配色とダーク配色を比較した画面

図3-2 同じHTMLをライト設定とダーク設定で表示した比較。教材HTMLとCSSをブラウザで描画した例。

color-schemeは入力欄などブラウザが描く部品の配色を知らせます。ページ独自の色を変える--pageとは役割が違います。

3.5 読み解く:大きさと余白からレイアウトを理解する

3.5.1 ページの幅を制限して中央に配置する

ページ全体の配置を担当するコードを見ます。以下は教材の一部を抜粋したものです。

*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  padding: 20px 32px;
  background: var(--page);
  color: var(--text);
  font-family: 'Noto Sans JP', Inter, system-ui, sans-serif;
}
.shell { width: min(700px, 100%); margin: 0 auto; }
.container { padding: 24px; }

ボックスモデルは内容・余白・境界線からなる大きさの考え方です。box-sizing: border-boxでは、指定した幅や高さにpaddingとborderを含めます。

3.5.2 タブを横に並べ、選択状態を見せる

タブ部分では、次の指定が重要です。必要な部分だけを抜粋しています。

.tabs {
  display: inline-flex;
  gap: 4px;
  padding: 0 12px;
  background: rgba(161, 215, 181, .68);
  border-radius: 10px 10px 0 0;
}
.filetab {
  min-width: 64px;
  height: 40px;
  padding: 0 20px;
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: 10px 10px 0 0;
  background: transparent;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.filetab[aria-selected="true"] {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

.tabsのdisplay: inline-flexは、子要素を並べるFlexbox(フレックスボックス)というレイアウトの仕組みを使い、タブ全体を内容に応じた幅のまとまりとして扱います。

.filetab[aria-selected="true"]は、aria-selected="true"のタブを選ぶ属性セレクターです。選択中のタブの色を変えるために使います。

ただし、CSSが属性を読んで色を変えることと、クリックで属性を書き換えることは別です。CSSによる状態の「表現」と、JavaScriptによる状態の「変更」は異なるものであることを留意しましょう。

3.5.3 検索フォームは入力欄の幅を柔軟にする

検索フォームのHTMLは、form.search-formの中に入力欄と検索ボタンを配置しています。CSSでは次のように指定しています。

.search-form {
  display: flex;
  gap: 8px;
  margin: 24px auto;
  max-width: 560px;
}
.search-input {
  flex: 1;
  min-width: 0;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

display: flexで検索欄とボタンを横並びにし、gap: 8pxで間隔を空けます。max-width: 560pxで全体幅を制限し、flex: 1とmin-width: 0で入力欄を伸縮させます。

form.search-form(横並び・最大560px)
┌────────────────────────────────┐ 8px ┌──────────┐
│ input.search-input(flex: 1)   │←gap→│ 検索ボタン│
└────────────────────────────────┘     └──────────┘

図3-3 検索フォームの横並び構造を表した模式図。実際の寸法を正確な縮尺で示したものではありません。

flex: 1を外した場合、入力欄は残りの横幅を同じようには埋めなくなります。このように、一つの宣言を変更した影響を具体的に予想できることが、CSSを理解する第一歩です。

3.6 読み解く:見えるデザインと操作しやすさ

デザインは見た目だけで完結するものではありません。キーボード操作や支援技術からの利用を考える必要もあります。教材には、次のような指定があります。

.search-input:focus {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
.filetab:focus-visible, .search-button:focus-visible,
.refresh-button:focus-visible, .news-item a:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

:focusはフォーカス中、:focus-visibleはフォーカスの目印が必要と判断された状態を選びます。outlineは輪郭線です。クリックとTabキーで違いを確認しましょう。

一方、.sr-onlyは要素を画面上ではほとんど見えない状態にするためのclassです。検索欄のHTMLには<label class="sr-only" for="search-input">検索キーワード</label>があります。ラベルの説明は視覚的には隠しながら、支援技術に伝える情報として残す目的で用いられています。表示しないことと、情報そのものを削除することは異なることを把握しておきましょう。

ニュース領域については、CSSに.news-cardや.news-itemのほか、.refresh-button、.news-notice、.errorなどの指定があります。しかし、今回のHTMLには対応する要素がないものもあります。さらに、#panel-newsにはhiddenが指定されており、CSSでも次のルールを置いています。

[hidden] { display: none !important; }

このルールは、hidden属性を持つ要素を表示しないようにするものです。ニュース用の見た目は、後の章に向けて準備されている部分と考えましょう。

3.7 読み解く:画面幅や利用環境に合わせて調整する

3.7.1 600px以下で余白を小さくする

教材の末尾には、画面幅に応じた調整が書かれています。

@media (max-width: 600px) {
  body { padding: 12px; }
  .container { padding: 16px 4px; }
  .filetab { min-width: 56px; padding: 0 14px; }
  .news-image { width: 90px; height: 60px; flex-basis: 90px; }
  .search-button { padding: 9px 12px; }
}

@media (max-width: 600px)は、表示領域の幅が600 ピクセル以下のときに内側の指定を適用します。このように表示環境に応じて配置やサイズを調整する考え方をレスポンシブデザインと呼びます。

画面幅390pxで描画した検索画面

図3-4 390px幅での表示例。入力欄とボタンが横並びのまま収まるかを観察できます。実際の端末での見え方は別途確認が必要です。

3.7.2 動きを抑える設定にも配慮する

同じく末尾には、次の指定があります。

@media (prefers-reduced-motion: no-preference) {
  .filetab, .search-button, .refresh-button {
    transition: background .18s ease, color .18s ease;
  }
}

transitionは色の変化を滑らかにします。prefers-reduced-motionを使い、動きを減らしたい利用者への配慮も加えています。

3.8 考える:CSSを外すと何が残るか

CSSを外すと見た目は変わりますが、HTMLの見出しやフォームの意味、GET送信の仕組みは残ります。

ただし、CSSが「見た目だけ」だから重要でないという意味ではありません。文字の読みやすさ、操作対象の見分けやすさ、狭い画面での操作性などもCSSの影響を受けます。構造・見た目・動作を分けて理解し、それらが利用者の体験として結び付くことを意識してください。

3.9 試す:一か所ずつ変更して確かめる

ここでは毎回、作業用コピーを編集し、結果を記録してください。複数の箇所を同時に変えると、どの変更が結果を生んだのか分かりにくくなります。

3.9.1 実験A:ページの背景だけを変更する

最初にライト設定で画面を表示し、ページの背景と検索欄の背景が別の色であることを確認します。次に、背景がどう変わるか予測してから、style.cssの最初の:rootにある次の一行だけを変更してください。

--page: #dceaf5; /* 実験用:元は #f1eee7 */

再読み込みして、ページの背景と検索欄の背景を比べます。ページの背景が変わり、入力欄の表面が別の色のままであれば、--pageと--surfaceが異なる用途で使われていると説明できます。ダーク設定では、後に書かれたダーク用の--pageが適用されるため、この変更が画面に現れない場合があります。

3.9.2 実験B:検索欄とボタンの間隔を変える

ライト設定の検索フォームで、入力欄とボタンの間隔を観察します。gapを広げるとどうなるか予測し、.search-formにある一行だけを次のように変更します。

gap: 24px; /* 実験用:元は 8px */

再読み込みして、入力欄とボタンの間隔が広がることを確認します。フォーム全体の幅が変わらない条件では、間隔が広がった分だけ入力欄に使える幅は減るはずです。display: flexとflex: 1の関係を意識して説明してください。

3.9.3 実験C:境界の前後で表示を比較する

開発者ツールで幅を601px→600px→390pxに変え、余白を比べます。600px以下ではbodyのpaddingが12pxになります。はみ出しも確認しましょう。

3.10 根拠を確かめる:開発者ツールと公式資料

開発者ツールは、ブラウザが現在どのHTMLやCSSを使用して表示しているかを調べる機能です。Chromeなどでは画面上で右クリックして「検証」を選び、ElementsとStyles(またはそれに相当する画面)を使います。

開発者ツールで.search-inputを選び、Stylesのbackground: var(--surface)とComputedの色を確認します。変更を保存する場合は元のCSSファイルを編集しましょう。

ダークモードは、OSの設定を変更するほか、Chrome DevToolsのRenderingにあるprefers-color-schemeのエミュレーションでも試せます。表示を切り替えたとき、--pageや--textの計算結果がどう変化するか確認してください。ただし、エミュレーションでの確認を、そのまま実機での検証と同一視してはいけません。

分からないことを生成AIに質問する場合は、

このCSSの.filetab[aria-selected="true"]はどのHTMLを選びますか。属性の値を変更せずにクリックした場合の挙動も説明してください

のように実際のコードを示して質問します。回答のうちCSSの仕様はMDNなどの公式資料で、教材固有の挙動は開発者ツールで検証しましょう。

3.11 章末演習と発展課題

次の問いに、CSSとHTMLを見ながら答えてください。可能な場合は予測を立てましょう。

  1. .subtitleの先頭に.が付く理由は何でしょうか。HTMLのどの要素に対応していますか。
  2. --surfaceの値を変えたとき、検索欄の背景はどうなるでしょうか。ページの背景が必ず同じ色に変わるでしょうか。
  3. .filetab[aria-selected="true"]が選ぶ要素は、初期状態ではどちらのタブでしょうか。ニュースタブをクリックすると自動的に変わりますか。
  4. max-width: 600pxという条件は、600pxちょうどの表示領域にも当てはまるでしょうか。
  5. CSSを読み込まない状態でも、検索フォームが送信できる理由をHTMLの記述から説明してください。

発展課題: --radiusだけを12pxから4pxへ変更し、検索欄と検索ボタンの角がどう変わるか観察してください。次に、.filetabの角は同じように変わるか予想してから確認します。結果が異なる場合は、.filetabがvar(--radius)ではなく、独自のborder-radiusを使っていることをコードから説明できます。

答えを確かめる際は、単に「変わった・変わらなかった」と書くのではなく、変更した宣言→選ばれた要素→画面上の結果を一続きで説明してください。これが、生成AIが提示したCSSを自分で評価するときにも役立ちます。

3.12 ふりかえり:見た目と動作を分けて理解する

本章では、第2章で作ったHTMLにCSSを読み込み、タブや検索フォームの外観がどのように決まるかを調べました。.shellは内容の幅を整え、Flexboxはタブや検索フォームを横並びにし、カスタムプロパティは色や角の丸みを再利用する役割を持っていました。メディアクエリによって、配色の希望や画面幅、動きの軽減設定に応じた指定も用意されていました。

ここで身につけてほしいのは、CSSのプロパティ名を数多く暗記することではありません。画面上の一部分を選び、対応するHTMLとCSSを探し、変更の影響を予測して検証する方法です。今後、他人が書いたコードや生成AIが提案したコードを使うときにも、この手順が判断の根拠になります。

次の第4章ではJavaScriptを使い、タブの操作に応じて表示するパネルを切り替えていきます。本章で学んだ.filetab[aria-selected="true"]のような指定は、JavaScriptによって状態が更新されると、その結果を見た目に反映する役割を担います。HTMLの構造、CSSの表現、JavaScriptの処理がどのように結び付くかを意識しながら進んでください。

第4章

JavaScriptでタブを動かす

第4章 JavaScriptでタブを動かす

4.1 知る:見た目を整えたタブに動作を加える

前章では、HTMLにCSSを適用し、タブや検索フォームの見た目を整えました。しかし、CSSは「どのタブが選ばれたか」をクリックから判断してくれるわけではありません。

本章では、JavaScriptを使って、利用者がタブを選ぶと対応する内容だけが表示されるようにします。完成した「New Tab」のうち、扱うのは「1 検索」と「2 ニュース」の切り替えです。すでにあるHTMLとCSSに、操作に応じた状態の変更を結び付けることが目標です。

4.2 知る:JavaScript、DOM、イベントの役割

HTMLは構造を記述するものでした。またCSSは見た目を指定するものでした。 これに対してJavaScriptは、利用者の操作などに応じて、ページの状態や動作を変えるために使います。

ブラウザは読み込んだHTMLを、JavaScriptから参照・変更できる要素の集まりとして扱います。この仕組みをDOM(Document Object Model)と呼びます。たとえば、HTMLに書かれた<button>は、JavaScriptから取得して属性を読み取ったり、値を変更したりできます。DOMは、HTMLを単なる文字列として探すのではなく、「このボタン」「このパネル」という単位で扱うための仕組みです。

また、クリックやキー入力のような出来事をイベントと呼びます。イベントが起きたときに呼び出す処理を登録する仕組みがイベントリスナーです。今回の基本的な流れは、次のとおりです。

  1. JavaScriptがHTML内のタブとパネルを見つけます。
  2. タブがクリックされたときに実行する処理を登録します。
  3. クリックが発生すると、JavaScriptが選択状態と表示状態を更新します。
  4. ブラウザは更新されたHTMLの状態とCSSに従って画面を表示します。

ここで重要なのは、JavaScriptがタブの色を直接塗り替えているわけではないという点です。JavaScriptは選択状態を表す属性を書き換え、CSSがその属性を条件に見た目を変えています。

4.3 読み解く:HTMLに用意されたタブとパネル

まず、JavaScriptを読む前に、操作の対象となるHTMLを確認します。index.htmlからタブとパネルの開始部分を抜き出すと、次のようになっています。

<nav class="tabs" role="tablist" aria-label="新しいタブのメニュー">
  <button id="tab-search" type="button" class="filetab"
    role="tab" aria-selected="true" aria-controls="panel-search" tabindex="0">1 検索</button>
  <button id="tab-news" type="button" class="filetab"
    role="tab" aria-selected="false" aria-controls="panel-news" tabindex="-1">2 ニュース</button>
</nav>

<section id="panel-search" class="panel" role="tabpanel" aria-labelledby="tab-search">...</section>
<section id="panel-news" class="panel" role="tabpanel"
  aria-labelledby="tab-news" hidden>...</section>

ここでは、二つのタブをrole="tablist"というまとまりに入れ、各ボタンにrole="tab"、対応する表示領域にrole="tabpanel"を指定しています。roleは、要素がインターフェースの中でどのような役割を持つかを、支援技術に伝えるための属性です。また、aria-で始まる属性は、要素の関係や状態を伝えるために用いられます。

画面上の部分タブのidタブのaria-controls対応するパネルのid
1 検索tab-searchpanel-searchpanel-search
2 ニュースtab-newspanel-newspanel-news

aria-controls="panel-news"はニュースタブから対象パネルを指します。aria-labelledby="tab-news"はパネルの名前付けにタブを使う指定です。JavaScriptもこの対応を参照します。

検索タブとニュースタブの対応関係を示し、ニュースタブのクリック前後で表示中のパネルが入れ替わる図。破線は実際には非表示のパネルを説明用に示している。

図 V-04-001:タブ選択前後の表示。破線は非表示のパネルを示す。

最初のHTMLでは、検索タブにaria-selected="true"、ニュースタブにaria-selected="false"が付いています。ニュースパネルにはhiddenがあるため、ページを開いた直後は検索パネルが表示される構成です。まず、この状態を覚えておいてください。

4.4 読み解く:tabs.js全体で行っていること

今回のJavaScriptは、tabs.jsというファイルにまとまっています。HTMLの<head>には次の指定をしてください

<script src="tabs.js" defer></script>

srcは読み込むファイルを指定します。deferは、HTMLの解析が終わってからこのスクリプトを実行するようブラウザーへ伝えるための要素になります。これはすべてのタブの要素が読み込まれる前に動作を行わないようにするためです。

tabs.js全体の処理は、大きく四つに分けられます。

  1. 要素の取得:タブを集め、それぞれに対応するパネルを取得する。
  2. 切り替え処理の定義:activateTabという関数に、選択状態と表示状態の更新をまとめる。
  3. 操作の登録:クリックとAltキーを使った操作からactivateTabを呼び出す。
  4. 初期化:最後にactivateTab(0)を実行し、検索タブを選択状態にする。

ここでいう関数は、名前を付けてひとまとまりにした処理です。関数を呼び出すことで、同じ変更を複数の操作から実行できます。「要素を見つける→操作を待つ→状態を変える」というJavaScriptの流れを意識してください。

ここでconst、let、functionを確認します。 constは再代入できない変数、letは再代入できる変数、functionは処理をまとめる関数です。

4.5 読み解く:タブとパネルを取得する

最初に、tabs.js冒頭の2行を見ます。

const tabs = [...document.querySelectorAll('[role="tab"]')];
const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls')));

documentは、現在のHTML文書をDOMとして扱うための入口です。querySelectorAll('[role="tab"]')は、CSSのセレクターと同じ書き方を使って、role="tab"を持つ要素をすべて取得します。今回は「1 検索」と「2 ニュース」の二つです。

querySelectorAllは要素の集まりであるNodeListを返します。[...]で配列に変えると、mapなどの処理を使えます。

mapで各タブに対応するパネルを探します。getAttribute('aria-controls')で"panel-news"を読み、getElementByIdへ渡します。

配列の対応は次のようになります。

配列の番号tabsに入る要素panelsに入る要素
0検索タブ(tab-search)検索パネル(panel-search)
1ニュースタブ(tab-news)ニュースパネル(panel-news)

tabs[1]とpanels[1]のように、同じ番号で対応するものを扱えることが、この後の処理の基礎になります。なお、指定したidを持つ要素が見つからない場合、getElementByIdはnull(該当する要素がないことを示す値)を返します。HTMLの名前が一致しているかどうかは、単なる見た目の問題ではなく、JavaScriptの動作に関わります。

ここでJavaScriptにおいて混同しやすいNullとUndefined について軽くさらっておきます。 • undefined:値が「未定義」でまだ何も割り当てられていない • null:値が「空」であり意図的に空っぽにしているといった違いがあります。

4.6 読み解く:activateTabが三つの状態を更新する

次は、タブ切り替えを担当する関数です。

function activateTab(index, moveFocus = false) {
  if (index < 0 || index >= tabs.length || !panels[index]) return;
  tabs.forEach((tab, i) => {
    const selected = i === index;
    tab.setAttribute('aria-selected', String(selected));
    tab.tabIndex = selected ? 0 : -1;
    panels[i].hidden = !selected;
  });
  if (moveFocus) tabs[index].focus();
}

activateTab(0)は検索、activateTab(1)はニュースを選びます。冒頭のifは不正な番号やパネル欠落時にreturnで処理を止めます。

forEachで各タブを調べ、i === indexが一致するときのtrueをselectedに入れて状態を更新します。

コード何を変えるか画面や操作への影響
tab.setAttribute('aria-selected', String(selected))タブの選択状態を表す属性支援技術に選択状態を伝え、CSSの選択条件にも使われる
tab.tabIndex = selected ? 0 : -1Tabキーで順番に移動するときの対象選択中のタブだけを通常のTabキー移動の対象にする
panels[i].hidden = !selected対応するパネルの非表示状態選択されていないパネルを隠す

setAttributeで属性を変更し、String(selected)で真偽値を"true"/"false"の文字列にします。aria-selectedはCSSの選択中表示に使われます。

.filetab[aria-selected="true"] {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

tabIndexの0は通常のTab移動の対象、-1は対象外です。-1でもJavaScriptからフォーカスできます。

!selectedは真偽値を反転します。選択中はhidden = falseで表示、それ以外はtrueで非表示にします。

たとえばactivateTab(1)を呼び出した直後の状態を整理すると、次のようになります。

要素aria-selectedtabIndexパネルのhidden
検索タブ/検索パネルfalse-1true(非表示)
ニュースタブ/ニュースパネルtrue0false(表示)

タブの選択状態・Tab移動・表示を同じ関数で更新します。moveFocusは既定でfalseのため、現状の呼び出しではfocus()による移動は起きません。

4.7 読み解く:クリックとAltキーを同じ関数につなぐ

切り替える規則ができたので、次は利用者の操作と結び付けます。クリックを受け取る部分は次のとおりです。

tabs.forEach((tab, index) => {
  tab.addEventListener('click', () => activateTab(index));
});

addEventListener('click', ...)でクリック時の処理を登録し、() => activateTab(index)で選んだタブの番号を渡します。

window.addEventListener('keydown', event => {
  if (!event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) return;
  if (event.key === '1' || event.key === '2') {
    const index = Number(event.key) - 1;
    if (index < tabs.length) {
      event.preventDefault();
      activateTab(index);
    }
  }
});

keydownはキーを押したときのイベントです。eventから修飾キーと1/2を確認し、対象の操作だけを受け付けます。

Number(event.key) - 1は、キーの文字"1"または"2"を数値に変え、配列の番号0または1に合わせる処理です。event.preventDefault()は、ブラウザ側で行われる既定の動作を、そのイベントが取り消し可能な場合に抑えるための呼び出しです。

クリックでもAltキーでも、最後に呼び出すのは同じactivateTabです。「クリック用の切り替え」「キー操作用の切り替え」を別々に作らないことで、選択状態の更新が食い違う可能性を減らせます。なお、このイベントはwindowに登録されているため、ページ内の別の場所でキーを押しても条件が合えば処理されます。

4.8 考える:初期表示と、実装されている範囲

tabs.jsの最後には、次の一行があります。

activateTab(0);

HTMLだけを読み込んだ時点でも、検索タブにはaria-selected="true"、ニュースパネルにはhiddenが指定されています。JavaScriptが実行されると、activateTab(0)があらためて検索タブを選び、タブとパネルの状態をそろえます。初期表示を明示しておくと、クリックを待つ前に、どちらが選択中かをコードから読み取れます。

JavaScriptを無効にするとタブは切り替わりませんが、検索フォームのGET送信はHTMLのactionとmethodで動きます。

4.9 試す:ブラウザと開発者ツールで状態を確かめる

index.html、style.css、tabs.jsを同じフォルダーに置き、index.htmlをブラウザで開きます。検索タブと検索欄が見えることを確認したら「2 ニュース」をクリックし、ニュースパネルの「第6章でニュースを自動取得します。」と検索フォームが隠れる状態を確認します。「1 検索」をクリックすると元に戻ります。

コンソール(Console)では、JavaScriptの式を直接入力して値を確認できます。

document.getElementById('tab-news').getAttribute('aria-selected')
// 期待する結果: "true"

document.getElementById('panel-news').hidden
// 期待する結果: false

"true"は属性から取得した文字列で、falseはプロパティの真偽値です。同じ状態を調べていても、取り出した値の種類が違うことに注目してください。

4.10 試す:一か所だけ変更して結果を予測する

ここからは、小さな変更を加えて検証します。

実験1:最初に表示するタブを変える

  1. 変更前にページを開き、検索パネルが最初に表示されることを確認します。
  2. activateTab(0)をactivateTab(1)にすると、何が表示されるか予測して記録します。
  3. tabs.jsの最後の一行だけを次のように変更します。
   activateTab(1);
  1. 保存してページを再読み込みし、最初からニュースパネルが表示されること、タブの選択状態もニュースに変わることを確認します。
  2. HTMLでは検索タブが選択された状態で書かれているのに、なぜニュースが表示されるのか説明します。手掛かりは、deferによる実行の順序とactivateTabの働きです。
  3. 最後にactivateTab(0)へ戻し、検索パネルから始まることを再確認します。

この実験から、HTMLの初期値とJavaScriptによって更新された状態を区別できます。どちらが実際の画面を決めているかを、再読み込み後に確かめるのがポイントです。

実験2:HTMLの対応関係を一時的に崩す

  1. 変更前にニュースタブで切り替えられることを確認します。
  2. aria-controlsに存在しないidを書いた場合、どこで問題が起きるか予測します。
  3. index.htmlのニュースタブの属性一か所だけを、aria-controls="panel-news-test"に変更します。ニュースパネル側のidは変えません。
  4. 保存して再読み込みし、画面の状態と開発者ツールのConsoleを確認します。今回のコードでは、panels[1]がnullになり、初期化の途中で存在しないパネルのhiddenを操作しようとしてエラーが起きることがあります。実際のエラーメッセージも記録しましょう。
  5. なぜタブとパネルの名前の不一致がJavaScriptのエラーにつながるのか、getElementById、map、panels[i].hiddenを手掛かりに説明します。
  6. 属性を必ずaria-controls="panel-news"に戻し、再読み込みしてクリック操作が復旧したことを確認します。

対応するパネルを一つ欠けさせると、activateTabのループ中にエラーが起こり得ます。現在の存在確認には改善の余地があります。

4.11 考える:フォーカスと操作方法をどう設計するか

moveFocus = falseは引数省略時の既定値です。現状のクリック・Altキー操作では、表示は切り替わりますがフォーカスは移動しません。

発展的な確認として、Altキーの処理にあるactivateTab(index);だけを一時的にactivateTab(index, true);へ変えると、切り替え先のタブへfocus()が呼び出されます。変更前後でConsoleにdocument.activeElement.idと入力し、現在のフォーカス先を比べてみましょう。変更する前に結果を予測し、確認後は元に戻してください。

aria-selectedは選択状態、tabIndexはTab移動の対象、focus()はフォーカス移動を扱います。三つの役割を区別しましょう。

4.13 ふりかえり:構造・状態・見た目をつなげる

本章では、DOMを通じてタブとパネルを取得し、イベントリスナーからactivateTabを呼び出して表示を切り替える仕組みを読み解きました。aria-controlsとidはHTML上の対応関係を示し、JavaScriptはaria-selected、tabIndex、hiddenを更新します。そして、CSSが選択されたタブの見た目を決めます。

大切なのは、画面上の動作を観察し、対応するHTML・CSS・JavaScriptを探して、変更の結果を予測し、実際の状態を確認するという学び方です。この方法を使えば、自分で書いたコードだけでなく、他人や生成AIが作ったコードについても、動作の根拠をもって制作に生かすことができます。

第5章

検索機能を実装する

第5章 検索機能を実装する

5.1 知る:タブで選べる画面を、使える検索画面にする

本章では検索フォームを読み解きます。検索語の送信はHTML、検索タブへ戻って検索欄に移動する処理はJavaScript、入力欄の外観はCSSが担当します。この分担を実際のコードで確かめましょう。

5.2 知る:検索クエリとフォーカスは別のもの

検索クエリとは検索条件として渡す文字列です。HTML CSSを入力して送信すると、その文字列が検索サービスに渡されます。

一方、フォーカスは、現在どの要素がキーボード入力などを受け取る対象になっているかを表す状態です。入力欄にフォーカスがあれば、利用者は改めて欄をクリックしなくても文字を入力できます。「検索欄を表示すること」と「検索欄にフォーカスを移すこと」は、似ていますが同じではありません。

これを踏まえて、検索に関わる処理を見てみます。

HTMLフォームによるGET送信と、JavaScriptによる検索欄へのフォーカス移動を別の経路として示した図

図 V-05-001:検索語を送信するHTMLの経路(上)と、検索欄へ移動するJavaScriptの経路(下)。CSSはフォーカスされた入力欄の見た目を担当する。

フォーム送信はHTMLとブラウザの機能です。/キーで検索欄へ移る動作だけはJavaScriptで追加しています。

5.3 読み解く:三つのファイルと画面の対応を把握する

今回の教材は、05-search/index.html、05-search/style.css、05-search/app.jsの三つを同じフォルダーに置く構成です。添付された実際のHTMLの<head>には、次の読み込み指定があります。

<link rel="stylesheet" href="style.css">
<script src="app.js" defer></script>

app.jsは第4章のタブ切り替えを引き継ぎ、検索欄へフォーカスする処理を加えています。

表示上の主な対応は次のとおりです。

画面上の部分HTMLの要素CSSの主な指定JavaScriptとの関係
「1 検索」タブ#tab-search.filetabactivateTab(0)で選択する
検索パネル#panel-search.panel、[hidden]選択したときに表示する
検索語の入力欄#search-input.search-input、.search-input:focussearchInput.focus()でフォーカスする
「検索」ボタンbutton[type="submit"].search-buttonHTMLフォームの送信を行う

次節からは、この全体像のうち、検索フォームそのもの、見た目、JavaScriptによる操作の順に調べていきます。

5.4 読み解く:検索語を送信するHTMLフォーム

index.htmlの検索パネルからフォームの部分を抜粋します。

<section id="panel-search" class="panel" role="tabpanel" aria-labelledby="tab-search">
  <!-- 検索はJavaScriptなしでもフォームのGET送信で動作します -->
  <form class="search-form" action="https://www.google.com/search" method="get" role="search">
    <label class="sr-only" for="search-input">検索キーワード</label>
    <input class="search-input" id="search-input" name="q" type="search"
      placeholder="Googleで検索" autocomplete="off" enterkeyhint="search" required>
    <button class="search-button" type="submit">検索</button>
  </form>
</section>

formは、入力された値をまとめて送信するためのHTMLの仕組みです。ここでは、入力欄と送信ボタンをひとつのformで囲んでいます。role="search"は、これが検索に使う領域であることを支援技術に伝えるための指定です。

フォームを理解するときに、最初に見るのは次の三つです。

記述この教材での値担当すること
actionhttps://www.google.com/searchフォームの送信先URLを指定する
methodget入力値をURLのクエリ部分に付けて送信する
入力欄のnameq送信される項目名を決める

たとえばHTML CSSと入力して送信した場合、ブラウザは概ね次の形式のURLを使います。

https://www.google.com/search?q=HTML+CSS

URLの?以降がクエリ文字列です。qが項目名、HTML+CSSが値で、+は空白を表します。qは検索先に合わせた名前で、HTMLの固定ルールではありません。

HTMLのフォームはクリックやEnterキーで送信できます。検索語を送るための独自JavaScriptは不要です。秘密情報の送信には使わないでください。

5.5 読み解く:入力欄・ラベル・送信ボタンが持つ意味

type="search"は検索入力欄、placeholderは未入力時のヒントです。placeholderは正式なラベルの代わりにはなりません。

そこで、この教材には次のラベルがあります。

<label class="sr-only" for="search-input">検索キーワード</label>

for="search-input"とid="search-input"でラベルを入力欄に結び付けます。.sr-onlyはラベルを視覚的に隠しつつ、支援技術から使えるようにします。

requiredは入力必須、autocomplete="off"は自動補完を控えるヒント、enterkeyhint="search"は仮想キーボードの表示用ヒントです。

最後の<button type="submit">検索</button>は、フォームを送信するボタンです。type="submit"はボタンの役割をHTMLで明示します。ここでもJavaScriptでクリックイベントを登録する必要はありません。第4章のタブボタンに付いていたtype="button"とは、目的の違う指定です。

5.7 読み解く:app.jsに加わった検索欄の取得とフォーカス

第4章ではJavaScriptをtabs.jsから読み込んでいましたが、本章のHTMLが読み込むのはapp.jsです。app.jsには前章のタブ切り替え処理が引き継がれ、検索欄の取得とショートカット処理が追加されています。

冒頭の要素取得を確認します。実際のコードのうち、タブと検索欄を取得する部分です。

const tabs = [...document.querySelectorAll('[role="tab"]')];
const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls')));
const searchInput = document.getElementById('search-input');

searchInputはid="search-input"の要素です。getElementByIdで取得するため、HTML側のidを変えたらJavaScript側も合わせます。

検索欄へ移る処理の中心は、次の二行です。これは後述のイベント処理から抜粋したものです。

activateTab(0);
searchInput.focus();

activateTab(0)で検索パネルを表示してから、searchInput.focus()で入力欄へ移動します。表示してからフォーカスする順序が重要です。

ここで新しい検索実行関数は定義されていません。app.jsにはform.addEventListener('submit', ...)やfetch()による検索送信もありません。フォームの送信がHTMLに任されていることを思い出してください。

5.8 読み解く:Alt+Dと/をどのように受け取るか

次は、app.jsのキー操作を受け取る部分の抜粋です。

window.addEventListener('keydown', event => {
  if (event.defaultPrevented || event.isComposing || event.ctrlKey
    || event.metaKey || event.shiftKey) return;
  if (event.altKey && (event.key === '1' || event.key === '2')) {
    const index = Number(event.key) - 1;
    if (index < tabs.length) {
      event.preventDefault();
      activateTab(index);
    }
  } else if (event.altKey && event.key.toLowerCase() === 'd') {
    event.preventDefault();
    activateTab(0);
    searchInput.focus();
  } else if (!event.altKey && event.key === '/' &&
    !event.target.closest('input, textarea, select, [contenteditable="true"]')) {
    event.preventDefault();
    activateTab(0);
    searchInput.focus();
  }
});

window.addEventListener('keydown', ...)は、キーが押されたときに実行する処理を登録しています。eventには押されたキーや、Altキーなどの同時押しの情報が入ります。処理を上から一行ずつ暗記するより、どの条件に当てはまると、どの処理が実行されるかを先に見ましょう。

操作と条件実行する処理期待する結果
Alt+1、Alt+2activateTab(index)検索・ニュースのタブを切り替える
Alt+DactivateTab(0)、searchInput.focus()検索パネルを開いて検索欄へ移る
入力欄など以外で/同じ二つの処理検索欄へ移る
入力欄などで/このショートカット処理を行わない/を通常の入力として使える

最初のifは、すでに別の処理が既定の動作を取り消したイベント(defaultPrevented)、日本語入力などの変換中を表すイベント(isComposing)、Ctrl・Meta・Shiftのいずれかを押している場合を除外します。ショートカットを無条件に発動させないための判定です。Alt+Dの比較では、event.key.toLowerCase()で大文字と小文字の違いを吸収しています。

/の条件は、もう少し丁寧に読みます。

!event.target.closest('input, textarea, select, [contenteditable="true"]')

event.targetは、今回のキー入力イベントが発生した対象です。closest(...)は、対象の要素自身とその親要素を調べ、指定したCSSセレクターに一致するものを探します。ここでは、入力欄(input)、複数行の入力欄(textarea)、選択欄(select)、編集可能な領域([contenteditable="true"])を対象にしています。

入力中は/を文字として使えるよう、closest(...)で編集可能な要素を確認します。対象外なら検索欄へフォーカスします。

event.preventDefault()は、取り消し可能なキー入力イベントに対してブラウザの既定の動作を抑えるための処理です。

5.9 試す:フォームの送信とフォーカスを別々に検証する

以下の作業に入る前に、index.html、style.css、app.jsが同じフォルダーにあり、HTMLから読み込めているかを確認してください。

HTML CSSを入力し、クリックとEnterの両方で送信します。URLのqを確認しましょう。検索結果が出なくても、フォームによるURL生成とは分けて考えます。

外部のページへ移動せずにフォームの入力値だけを確かめたい場合は、検索語を入力した状態でConsoleに次のコードを入力します。以下は検証用であり、app.jsに追加するコードではありません。

new FormData(document.querySelector('.search-form')).get('q')
// "HTML CSS"(入力した値に応じて変わる)

続いて、検索欄を空にして次の式を試します。

document.querySelector('.search-form').checkValidity()
// false(requiredがあり、入力が空の場合)

これはHTMLの入力チェックを確認するための操作です。検索ボタンを押しても、入力が空ならブラウザが送信を止めることも確かめましょう。

次に、ニュースタブをクリックしてから/を押します。検索パネルが表示され、検索欄へフォーカスが移るかを確認してください。Consoleでは、次のように現在のフォーカス先を確かめられます。

document.activeElement.id
// "search-input"(検索欄にフォーカスがある場合)

document.activeElementでフォーカス先を確認します。検索欄では/が文字として入力されることも試しましょう。Alt+Dはブラウザが優先する場合があります。

5.10 試す:一か所を変え、予測して、元に戻す

実験1:requiredを取り外す

空欄でcheckValidity()がfalseになることを確認し、requiredだけ外して再実行します。変化を記録し、最後にrequiredを戻してください。

この実験では、入力欄の見た目やJavaScriptのショートカットを変えなくても、HTMLの属性だけで入力チェックを変えられることがわかります。

実験2:送信する項目名を変える

変更前に、検索欄へHTML CSSと入力してnew FormData(document.querySelector('.search-form')).get('q')の結果を確認します。変更後の結果を予測してから、index.htmlのname="q"をname="keyword"だけに変更してください。再読み込みして同じ語を入力し、Consoleで次を試します。

new FormData(document.querySelector('.search-form')).get('keyword')
// "HTML CSS"(入力した値に応じて変わる)

new FormData(document.querySelector('.search-form')).get('q')
// null

この結果は、入力欄のnameが送信項目の名前を決めることを示しています。この状態で送信すると、URLにはkeyword=...という項目が入るため、元のq=...とは異なる要求になります。Google側でどう解釈されるかまでは、HTMLの仕様だけでは保証されません。

実験3:JavaScriptなしでも送信できるか

変更前に/で検索欄へ移動できることと、検索ボタンで送信できることを確かめます。次にindex.htmlのscript要素の読み込み指定という一行だけ選択後ctrl + /で一時的にHTMLコメントにします。

<!-- <script src="app.js" defer></script> -->

再読み込みして、タブのクリックや/による切り替えができなくなることを確かめます。そのうえで、初めから表示されている検索欄へ文字を入力し、検索ボタンやEnterキーによる送信を試してください。送信はフォームの機能なので、JavaScriptを読み込まなくても基本的に動作します。

5.11 考える:操作を便利にするときの設計判断

Webアプリを作るとき、すべてをJavaScriptで実装する必要はありません。フォームには送信、入力欄には値の保持や入力チェック、ボタンには送信という基本的な仕組みが備わっています。その上に、検索欄へすぐ移動するショートカットを追加するのが本教材の考え方です。

/は入力中の文字と競合するため、closest(...)で判定します。Alt+Dはブラウザや日本語入力と競合することがあるので、代替操作も残しましょう。

発展課題として、生成AIに

「フォームをすべてJavaScriptのfetch()で送信する案」と「HTMLフォームのままにする案」の違い

を尋ねてみてください。ただし、その提案をすぐに採用する必要はありません。 本章で使っているaction、method、name、requiredがどこまで担当しているかを確認し、新しいコードを追加する理由が本当にあるかを考えることを課題とします。

5.12 ふりかえり

本章では、検索フォームの送信と、検索欄へ移動する処理を分けて読み解きました。index.htmlはformのGET送信を担当し、app.jsはタブを開いて検索欄にフォーカスする処理を加え、style.cssはフォームの配置やフォーカス時の外観を決めていました。

振り返りとして、次の内容を自分の言葉で説明してみてください。

  1. HTML CSSという入力値が、name="q"とmethod="get"によってURLのどこに表れるか。
  2. なぜactivateTab(0)の後にsearchInput.focus()を呼ぶのか。
  3. なぜ検索欄で入力した/はショートカットとして扱わないのか。
  4. JavaScriptを読み込まなくても、検索フォームの送信が残るのはなぜか。

答えが曖昧なときは、該当するHTML・CSS・JavaScriptの行を見つけ、ブラウザで再確認しましょう。コードの意味を知るだけでなく、動作を根拠付きで説明できることが、今後の制作で役立ちます。

第6章

ニュースを自動取得する

第6章 ニュースを自動取得する

6.1 知る:画面の中に、外部の情報を取り込む

本章では「2 ニュース」タブに外部の記事を表示します。ブラウザのJavaScript、Node.jsのローカルサーバー、GNews APIの連携を学びます。

本章の到達目標は、「何を送り、何を受け取り、画面のどの要素を書き換えているか」を説明し、成功時だけでなく失敗時も根拠を持って観察できるようになることです。 特に、APIキーをブラウザに渡さない理由と、外部通信の結果をそのまま信じない理由を考えます。

6.2 知る:APIと三つの担当者

API(Application Programming Interface)は、別のプログラムが提供する機能やデータを、決められた方法で利用するための窓口です。 GNews APIでは、検索語を指定してニュース記事のデータを取得できます。本教材では、GNewsに直接アクセスするのはブラウザではなく、自分のNode.jsサーバーです。

担当教材のファイル・相手主な仕事
ブラウザindex.html、style.css、app.js操作を受け付け、自分のサーバーに依頼し、受け取った記事を画面へ表示する
ローカルサーバーserver.mjsブラウザからの依頼を確認し、APIキーを使ってGNewsへ問い合わせる
外部サービスGNews Search API検索条件に対応する記事データを返す

ここでいうサーバーは、リクエスト(依頼)を受けてレスポンス(応答)を返すプログラムです。Node.jsは、JavaScriptをブラウザの外でも実行できる環境です。同じJavaScriptでも、ブラウザで実行するapp.jsは画面を操作し、Node.jsで実行するserver.mjsはHTTP通信とファイル配信を担当します。

ブラウザ、ローカルサーバー、GNews APIの三者間のリクエストと応答を示し、APIキーをサーバー側だけに置くことを明示した図。

図 V-06-001:ニュース取得の通信経路。ブラウザは/api/newsへ問い合わせ、ローカルサーバーがX-Api-Keyヘッダーを付けてGNews APIへ問い合わせます。記事データはサーバーを経由してブラウザに戻り、APIキーはブラウザへ返しません。

この構造を採るのは、秘密にすべきAPIキーを、ブラウザから取得できるHTMLやJavaScriptに書き込まないためです。ブラウザ用のコードは利用者が開発者ツールで読むことができます。サーバー側でキーを管理すれば、本教材の通常の応答にキーを含めずに外部APIを利用できます。

6.3 知る:なぜindex.htmlを直接開かないのか

第4章と第5章は、HTMLファイルをブラウザで直接開いて動きを観察できました。しかし本章のapp.jsは、次のようなURLへアクセスします。

fetch('/api/news?keyword=' + encodeURIComponent('テクノロジー'));

先頭が/のURLは、今開いているページと同じオリジン(通信先のスキーム・ホスト・ポートの組み合わせ)を基準にしたパスです。この教材をhttp://127.0.0.1:3000/から開いていれば、問い合わせ先はhttp://127.0.0.1:3000/api/news?...になります。一方、file://でindex.htmlを直接開いても、/api/newsに応答するNode.jsサーバーは起動しません。

そこで本章では、Node.jsサーバーを先に起動し、ブラウザからそのサーバーへ接続します。画面の表示に使うHTML・CSS・JavaScriptも、同じサーバーから配信される構成です。今後、通信の不具合を調べるときは、まず「どのURLからページを開いたか」を確認してください。

6.4 読み解く:HTMLとCSSはニュースの表示場所を用意する

index.htmlでは、ニュースタブに対応するパネルが次のように定義されています。ここでは関係のある部分だけを抜粋しています。

<section id="panel-news" class="panel" role="tabpanel" aria-labelledby="tab-news" hidden>
  <div class="news-card">
    <div class="news-heading">
      <h2 id="news-title">ニュース</h2>
    </div>
    <p id="news-genre" class="news-genre">ニュースタブを開くと、ニュースを取得します。</p>
    <div id="news" aria-live="polite"></div>
  </div>
</section>

panel-newsは切り替え対象、news-genreは取得状況、newsは記事一覧です。aria-live="polite"は内容の変化を支援技術に知らせます。

CSSは記事の配置と外観を担当します。たとえば、教材には次の指定があります。

.news-item { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--border); }
.news-image { width: 120px; height: 80px; flex: 0 0 120px; object-fit: cover; border-radius: 8px; }
.news-content { min-width: 0; }
.error { color: var(--error); }

.news-itemが記事内の画像と文章を横並びにし、.news-imageが画像の大きさをそろえます。.errorは失敗時の文字色です。記事をどこに作るかはHTMLとJavaScript、作られた記事をどう見せるかはCSSという役割分担は、前章までと同じです。

6.5 読み解く:ニュースタブを初めて開いたときだけ取得する

まずブラウザ側のapp.jsを、大きな単位で読みます。第4・5章のタブ切り替えと検索欄へのフォーカス処理は残り、ニュース取得に必要な要素と状態が追加されました。

const newsBox = document.getElementById('news');
const genreBox = document.getElementById('news-genre');
let newsStarted = false;

newsBoxとgenreBoxはHTML要素の参照、newsStartedは取得開始済みかを記録する値です。後で書き換えるためletを使います。

activateTabには、次の処理が追加されています。

if (index === 1 && !newsStarted) {
  newsStarted = true;
  loadNews();
}

ニュースタブ(index === 1)を初めて開いたときだけnewsStartedをtrueにし、loadNews()を呼びます。

このように、画面を開いたときではなく必要になったときに処理を始める方法は、初期表示で不要な通信を避けるための一つの設計です。

6.6 読み解く:fetchとasync/awaitで応答を待つ

ニュースを取得する中心はloadNewsです。次は教材の実際のコードです。

async function loadNews() {
  newsBox.textContent = 'ニュースを取得しています…';
  genreBox.textContent = '取得中';
  try {
    const response = await fetch('/api/news?keyword=' + encodeURIComponent('テクノロジー'));
    const data = await response.json();
    if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
    genreBox.textContent = `ジャンル:${data.genre}`;
    displayNews(data.articles);
  } catch (error) {
    genreBox.textContent = 'ニュースを取得できません';
    newsBox.textContent = error.message;
    newsBox.classList.add('error');
  }
}

fetchはHTTPで通信し、応答を非同期に受け取ります。async関数内のawaitで結果を待てます。

最初の2行で「取得しています…」「取得中」と表示するのは、利用者に通信中であることを知らせるためです。その後、await fetch(...)でサーバーへ依頼し、await response.json()で応答本文をJavaScriptのデータとして読み取ります。JSONは、文字列・数値・配列などを組み合わせてデータを表す形式で、サーバーとブラウザ間などプログラム同士のデータの受け渡しによく利用されます。

encodeURIComponentは文字列をURL用に変換します。fetchは404などでも自動的には例外を投げないため、response.okを確認し、失敗時はthrowとcatchで表示を切り替えます。

成功した場合はdata.genreを表示し、data.articlesをdisplayNewsに渡します。ここで大切なのは、通信してデータを受け取る処理と、画面の要素を組み立てる処理を別の関数にしていることです。表示方法を変えたいとき、まずdisplayNewsを探せます。

6.7 読み解く:サーバーがAPIキーを読み込み、問い合わせを制限する

サーバー側のserver.mjsは、ブラウザとは別のNode.jsプロセスで実行します。まず、通信先の設定とAPIキーの管理を見ます。

// 第6章:APIキーはサーバー側の環境変数から読み込み、ブラウザへ返さない。
// 起動: node --env-file=.env server.mjs (Node.js 20.6以上)
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const folder = dirname(fileURLToPath(import.meta.url));
const port = Number(process.env.PORT ?? '3000');
const apiKey = process.env.GNEWS_API_KEY?.trim();
const keywords = new Set(['テクノロジー', 'AI', '経済', '政治', '科学']);
const ttlMs = 5 * 60 * 1000;
const cache = new Map();

process.envは、Node.jsで環境変数を参照する仕組みです。環境変数を使うと、APIキーなどの設定値をソースコードに直接書かず、実行時に渡せます。本教材では.envを--env-fileで読み込み、GNEWS_API_KEYを取得します。.env.exampleは書式の見本であり、秘密のキーを書き込んで共有してはいけません。 keywordsは受け付ける検索語の集合です。Setのhasで登録済みかを調べ、外部APIに送る入力を限定しています。

6.8 読み解く:ブラウザの依頼からGNewsへのリクエストまで

server.mjsのnews関数は、まず依頼された検索語を読み、値を確認します。以下は実コードの抜粋です。

const keyword = reqUrl.searchParams.get('keyword') || 'テクノロジー';
if (!keywords.has(keyword)) return json(res, 400, { error: '未対応のジャンルです。' });
if (!apiKey) return json(res, 503, { error: 'GNEWS_API_KEY が未設定です。.env を確認してください。' });

許可外のkeywordには400、APIキー未設定には503を返します。数字の意味はHTTPで定義されますが、どの場合に返すかは教材サーバーの実装です。

検査を通ると、GNewsへの問い合わせ内容を組み立てます。

const params = new URLSearchParams({ q: keyword, lang: 'ja', country: 'jp', max: '5' });
try {
  // APIキーをクエリ文字列に含めず、ヘッダーで渡す。
  const response = await fetch(`https://gnews.io/api/v4/search?${params}`, {
    headers: { 'X-Api-Key': apiKey },
    signal: AbortSignal.timeout(9000),
  });

URLSearchParamsは、q、lang、country、maxなどのパラメーターをURL用に組み立てます。qは検索語、lang: 'ja'は日本語の記事の指定、country: 'jp'は掲載国に関する指定、max: '5'は取得件数の上限として要求する値です。実際に返る記事の件数や内容は、GNewsに依存します。

X-Api-KeyヘッダーでGNewsへキーを渡し、URLへの記載を避けています。AbortSignal.timeout(9000)は約9秒で通信を中断します。

GNewsが正常に応答すると、サーバーはdata.articlesが配列であることを確認し、記事からtitle、description、url、image、publishedAt、source.nameなどを取り出してブラウザへ返します。GNewsの応答全体をそのままブラウザへ送り返すのではなく、必要な項目を選んでいます。このレスポンスにはAPIキーを含めていません。

6.9 読み解く:取得結果をニュースカードとして表示する

ブラウザに戻ってきた記事データはdisplayNewsがHTML要素へ変換します。初めにnewsBox.replaceChildren()で以前の内容を取り除き、受け取った記事を一つずつ処理します。次のコードは、記事タイトルをリンクとして作る部分の抜粋です。

const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.textContent = article.title;
content.append(link);

document.createElement('a')でリンク要素を作り、hrefに記事のURLを設定します。target="_blank"はリンク先を新しい閲覧コンテキストで開くため、rel="noopener noreferrer"は開いたページとの関係を制限するための設定です。

外部の文章はinnerHTMLではなくtextContentで表示します。safeWebUrlではhttp:/https:のURLのみを許可します。

記事画像はimgで表示し、loading = 'lazy'で遅延読み込みします。alt = ''はタイトルとの重複を避ける設定です。画像がなくても記事の文字情報は表示できます。

もし有効なURLやタイトルを持つ記事が一つも表示できなければ、newsBoxには「表示できるニュースがありません。」と表示されます。ここまでで、JSONのデータがDOM要素になり、CSSによってニュースカードとして見えるまでの道筋がつながりました。

6.10 考える:失敗したとき、どこで何が起こるか

通信が失敗した原因は一つとは限りません。ブラウザから見たエラーと、サーバーがGNewsから受け取ったエラーも区別する必要があります。

状況教材サーバーの応答ブラウザ側での結果
.envのキーが空503と設定確認のメッセージcatchに進み、エラーメッセージをニュース欄へ表示
許可していないkeywordを指定400と「未対応のジャンルです。」catchに進み、エラーを表示
GNewsが401・403・429などを返すサーバーが状況に応じた文面を選び、ブラウザには502を返すエラーを表示
通信がタイムアウトするなどして失敗サーバーが502と通信失敗のメッセージを返すエラーを表示
記事として表示できるものがない正常な応答の内容次第「表示できるニュースがありません。」と表示する場合がある

上の「502」は、GNewsから返ったステータスをそのまま転送した数字ではありません。GNews側が401でも、この教材サーバーはブラウザへ502を返す構造です。開発者ツールに表示される値と元のサービスの応答を混同しないようにしてください。

また、newsStartedは取得開始時にtrueになり、失敗してもfalseへ戻しません。ニュースタブをいったん離れて開き直しても通信は再試行されません。改めて確認する際は、ページを再読み込みします。

6.11 考える:5分間のキャッシュと公開範囲

server.mjsでは、取得した記事をcacheというMapに保存します。キャッシュとは、一度取得した結果を一時的に保存して、同じ依頼に再利用する仕組みです。教材のttlMs = 5 * 60 * 1000は、保存結果を5分間利用する設定です。

/api/news?keyword=テクノロジーへ短時間に同じ依頼が来た場合、条件が合えばサーバーはGNewsに改めて問い合わせず、保存済みの記事を返します。応答JSONのcacheがtrueなら保存結果から、falseなら新たに取得した結果から返したことを表します。

6.12 試す:APIキーなしでサーバーを起動する

まず、秘密キーを用意せずに確認できる範囲から始めます。以下の操作は、元の教材ファイルを壊さずに進められます。

  1. index.html、style.css、app.js、server.mjs、.env.example、.gitignoreを同じフォルダーに置きます。Node.js 20.6以降が利用できることをnode --versionで確認します。
  2. .env.exampleをコピーし、コピーしたファイル名を.envにします。見本の「ここに新しく発行したキーを設定」という文字列は必ず消し、次のようにキーの値を空にします。見本の文字列を残したままでは、コードは「未設定」と判定しません。
   GNEWS_API_KEY=
   PORT=3000
  1. フォルダーをターミナルで開き、node --env-file=.env server.mjsを実行します。起動メッセージのURLを確認します。ポートが使用中などで起動しない場合は、そのエラーを先に解決します。
  2. ブラウザでhttp://127.0.0.1:3000/を開き、検索タブが表示されていることを確認します。次にニュースタブを押すと、キー未設定を知らせるメッセージが表示されるはずです。
  3. 開発者ツールのNetworkで/api/newsの通信を探し、HTTPステータスとJSONのerrorを記録します。期待するサーバーの応答は503です。
  4. 確認後、ターミナルでCtrl+Cを押してサーバーを止めます。.envは秘密情報を入れる可能性があるため、提出物や公開リポジトリへ含めないでください。

注意: .envのファイル自体が存在しない場合は、Node.jsの--env-fileが起動時にエラーを出すことがあります。「ファイルはあるがキーが空」と「設定ファイルがない」は別の状態です。実験では前者を再現します。

6.13 試す:一か所だけ変え、エラーの意味を確かめる

実験A: キーを空にし、http://127.0.0.1:3000/api/news?keyword=テクノロジーで503を確認します。keywordだけ未登録にすると400になります。コードで検査順序を調べましょう。

実験B: http://127.0.0.1:3000/.envとhttp://127.0.0.1:3000/server.mjsの404を確認します。staticFilesを読んで理由を調べましょう。

実験C: app.jsのencodeURIComponent('テクノロジー')を'AI'に変え、ブラウザを再読み込みします。Networkでkeyword=AIを確認し、実験後に戻します。

有効なAPIキーを取得できる場合は、任意の実験として記事表示の成功も確認できます。GNews APIの利用開始手順で取得方法と利用条件を確認してください。APIキーの設定は必須演習ではありません。外部通信、記事数、画像表示はネットワークや外部サービスに依存するため、キーなしでも本章の主要な学習目標を達成できます。

6.14 根拠を確かめる:Networkと公式資料を照らし合わせる

Networkで/api/newsのRequest URL・Status・Responseを確認します。キー未設定なら503とGNEWS_API_KEYのエラーを記録しましょう。

次にGNews公式資料で、Search Endpointのq、lang、country、maxの意味と、X-Api-Keyによる認証を確認します。Node.js公式資料では--env-fileの利用方法を、MDNではfetchがHTTPエラーステータスだけでは例外を投げないことを確認します。資料に定義された動作と、教材で採用したエラー番号や表示文言を分けて記録してください。

生成AIにこの通信経路を説明させた場合も、「APIキーをHTMLに直接書き込む」「ブラウザからGNewsへキーを送る」といった提案が含まれていないか、実際のserver.mjsと公式資料で検証できます。説明の自然さよりも、コードと根拠が一致しているかを判断材料にしましょう。

6.15 ふりかえり:画面の外で動く処理も追えるようになる

本章では、ニュースタブの操作を起点に、ブラウザのapp.jsがローカルサーバーの/api/newsへ依頼し、server.mjsがGNewsに問い合わせ、結果をdisplayNewsが画面へ表示するまでを追いました。HTMLは表示場所を用意し、CSSはカードの外観を整え、ブラウザのJavaScriptは通信とDOMの更新を担当します。Node.jsサーバーはAPIキーを管理し、外部サービスとの通信を担当します。

確認してほしいのは、次の四つです。

  • ニュースを開いたとき、どの関数がどの条件で呼ばれるか説明できますか。
  • ブラウザとサーバーの通信先を区別し、APIキーの置き場所を説明できますか。
  • fetch、await、response.ok、catchを実際の表示と結び付けられますか。
  • キー未設定による503、未対応ジャンルによる400、配信対象外ファイルによる404を、Networkで確かめられますか。

これらができれば、「動いた/動かない」という結果だけでなく、どの段階で何が起こったかを説明できます。次章の具体的な内容は今回の資料には示されていませんが、本章で身につけた通信の経路と表示の対応を調べる方法は、今後データの種類や機能が増えても使える考え方です。

第7章

エラーを見つけて修正する

第7章 エラーを見つけて修正する

7.1 知る:完成した機能を「確かめられる」ようにする

機能が増えると、一つの症状にも複数の原因が考えられます。本章では、問題を思いつきで直さず、症状を再現し、仮説を立て、確かめてから、必要な部分だけを修正する方法を学びます。原因が分からないまま複数のファイルを変えると、結果が変わった理由を追えません。これがデバッグの基本です。

本章では、Node.jsの自動テストとブラウザの開発者ツールを併用します。学習の目標は次の四つです。

  • HTMLの構造、JavaScriptの構文、HTTP通信のどこに問題があるかを切り分ける。
  • test.mjsが何を検査しており、何を検査していないかを説明する。
  • テストをわざと失敗させ、失敗内容から原因を読み取って復元する。
  • 自動テストの結果と、ブラウザで観察した事実を区別して記録する。

大事なのは、最初からすべてのエラーを解決できることではありません。確認できたことと、まだ確かめていないことを分けられることです。

7.2 知る:症状、仮説、確認を分ける

デバッグの最初の一歩は、「何が起きたか」を具体的にすることです。

ニュースが壊れた

よりも、

ニュースタブを初めて開くと『ニュースを取得できません』と表示され、Networkには503が記録された

のほうが原因を絞り込めます。

調査は、おおむね次の順番で進めます。

症状の再現から仮説を立て、HTML・JavaScript・通信・サーバー内キャッシュのいずれを確認するか選び、小さな修正の後に再検証する流れ。

図 V-07-001:デバッグの基本的な流れと、問題の種類に応じた確認先。キャッシュは本教材ではサーバー内のMapに保持され、ブラウザのApplication画面へ自動的に保存されるわけではありません。

どこを調べるか迷ったときは、次の対応を手掛かりにします。

目に見える症状最初に疑う場所主な確認方法
タブとパネルが対応しないindex.htmlのidとaria-controls構造テスト、Elements
JavaScriptが実行できないapp.jsやserver.mjsの文法node --check、テスト、Console
ニュースを取得できない/api/newsの応答やサーバー設定Network、HTTPテスト、サーバー側コード
一度取得したニュースが再利用されるサーバー内のキャッシュserver.mjsのcacheと時間判定、応答のcache値

これらは調査の入口であって、原因を断定する表ではありません。たとえば、画面が切り替わらない場合にはHTMLの対応の誤りだけでなく、JavaScriptの例外も考えられます。

7.3 読み解く:今回使用するファイルと自動テスト

test.mjsはNode.jsで動くテスト用ファイルです。.mjsはECMAScriptモジュールとして扱う拡張子で、ブラウザ画面を作るものではありません。

テストを実行するには、少なくとも次のファイルが同じ07-debugフォルダーに必要です。

07-debug/
├─ index.html       ← HTMLのタブ・パネルを検査
├─ app.js           ← 構文を検査
├─ style.css        ← ブラウザでの表示に使用
├─ server.mjs       ← 構文とHTTP応答を検査
└─ test.mjs         ← 今回読む自動テスト

index.htmlとapp.jsは第6章で使ったものを引き継ぎます。

node --test test.mjs

--testはNode.jsのテストランナーを使用する指定です。テストランナーは、登録された確認処理を実行し、成功・失敗をまとめて表示します。教材のtest.mjsには三つのテストがあり、それぞれが独立した役割を持っています。

テスト名(要約)確認する内容確認しない内容
タブとパネルの対応特定のidとaria-controlsの個数・文字列の一致実際のクリック動作、表示、重複IDの完全な検出
JavaScriptの構文app.jsとserver.mjsが構文として解釈できるか実行時の例外や画面操作の正しさ
APIキー未設定時の応答/が200、/api/newsが503、指定した秘密ファイルのパスが404GNewsへの実接続、APIキーを設定した場合の成功、すべてのURLの安全性

テストがすべて成功したことと、Webアプリのあらゆる動作が正しいことは同一ではないことを留意しましょう。

7.4 読み解く:テストを書くための基本部品

test.mjsの冒頭には、次の読み込みがあります。

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { spawn, spawnSync } from 'node:child_process';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const folder = dirname(fileURLToPath(import.meta.url));

importで機能を読み込みます。node:testはテスト、node:assert/strictは期待値の検証、readFileはファイルの読み込み、spawn・spawnSyncは別プログラムの起動に使います。

folderはtest.mjsのあるフォルダーを指し、index.htmlなどの参照先を定めます。サーバー起動時にもcwd: folderを指定します。

7.5 読み解く:HTMLの対応関係を自動で調べる

最初のテストは、タブとパネルの対応を調べます。

test('タブ数とパネル数が一致し、aria-controlsが対応する', async () => {
  const html = await readFile(join(folder, 'index.html'), 'utf8');
  const ids = [...html.matchAll(/id="(panel-[^"]+)"/g)].map(m => m[1]);
  const controls = [...html.matchAll(/aria-controls="(panel-[^"]+)"/g)].map(m => m[1]);
  assert.equal(ids.length, controls.length);
  assert.deepEqual(ids.sort(), controls.sort());
});

第4章で学んだように、タブのaria-controls="panel-news"は、id="panel-news"のパネルを指します。もし名前が違えば、JavaScriptが意図するパネルを探せなくなる可能性があります。そこで、このテストはHTMLファイルを文字列として読み込み、二つの名前の集まりを比較します。

matchAllで正規表現に一致するidとaria-controlsを集めます。assert.equalは件数、assert.deepEqualは並び替え後の値を比較します。

たとえば、ニュースタブのaria-controlsだけをpanel-news-typoに変更すれば、パネル側のidにはその名前が存在しないため、比較結果が一致しなくなります。ただし、このテストはHTMLをDOMとして解析していません。特定の書き方を正規表現で調べる簡易テストです。

7.6 読み解く:JavaScriptの構文を確認する

二番目のテストは、JavaScriptの構文エラーを見つけるためのものです。構文エラーとは、括弧が閉じられていないなど、言語の書き方として解釈できない問題です。

test('JavaScriptファイルに構文エラーがない', () => {
  for (const filename of ['app.js', 'server.mjs']) {
    const result = spawnSync(
      process.execPath,
      ['--check', join(folder, filename)],
      { encoding: 'utf8' }
    );
    assert.equal(result.status, 0, result.stderr);
  }
});

for...ofでファイルを順に調べ、spawnSyncでnode --checkを実行します。--checkはプログラムを動かさず構文だけを検査します。

result.statusが0なら、コマンドは正常終了したと判断して次へ進みます。0以外であれば、assert.equalが失敗し、エラーの詳細がテスト結果に表示されます。result.stderrは、コマンドがエラー内容などを書き出すための出力です。

node --checkでわかるのは構文の誤りだけです。正しい動作は実行して別途確かめる必要があります。

7.7 読み解く:サーバーを起動し、HTTP応答を検査する

三番目のテストは、自分のNode.jsサーバーを一時的に起動し、実際にHTTPリクエストを送ります。HTTPは、ブラウザとサーバーが情報をやり取りするときの規則です。サーバーは処理結果を200や503などのステータスコードで返します。

最初に、テストがサーバーを起動する部分を見ます。

test('APIキー未設定ならニュースAPIは503で、秘密ファイルは配信しない', async () => {
  const child = spawn(process.execPath, ['server.mjs'], {
    cwd: folder,
    env: { ...process.env, GNEWS_API_KEY: '', PORT: '0' },
    stdio: ['ignore', 'pipe', 'pipe']
  });
  // 以下にサーバーへの確認と終了処理が続く(省略)
});

spawnでサーバーを起動します。cwdは作業場所、envはテスト用の環境変数、PORT: '0'はOSによる空きポートの選択です。

このテストは、通常のニュース取得が成功するかどうかを調べるものではありません。APIキーが設定されていない場合に、サーバーが想定どおりの失敗を返すかを確認します。server.mjsには次の条件があります。

async function news(reqUrl, res) {
  const keyword = reqUrl.searchParams.get('keyword') || 'テクノロジー';
  if (!keywords.has(keyword)) return json(res, 400, { error: '未対応のジャンルです。' });
  if (!apiKey) return json(res, 503, { error: 'GNEWS_API_KEY が未設定です。.env を確認してください。' });
  // 以下は省略
}

この教材では、キーが空ならGNewsへ問い合わせる前に503を返す設計です。HTTPの一般的な意味として503はサービスがリクエストに対応できない状態を示しますが、キー未設定を503で表現することは教材サーバーの実装上の判断です。すべてのWeb APIが同じ条件で503を返すわけではありません。

テストの応答確認部分では、次の処理が実行されます。

const html = await fetch(url);
assert.equal(html.status, 200);
const news = await fetch(url + '/api/news');
assert.equal(news.status, 503);
assert.match((await news.json()).error, /GNEWS_API_KEY/);
for (const path of ['/server.mjs', '/.env', '/.env.example']) {
  assert.equal((await fetch(url + path)).status, 404);
}

fetchで/の200、ニュースAPIの503、対象パスの404を確認します。assert.matchでエラーメッセージにGNEWS_API_KEYが含まれるか検査します。

server.mjsはstaticFilesの登録対象のみ配信します。/.envと/server.mjsの404を確認しますが、この結果だけで安全性全体は保証できません。

なお、三番目のテストの終わりではfinallyを使い、成功・失敗にかかわらず起動したサーバープロセスにchild.kill()を呼びます。テスト用のサーバーを起動したままにしないための処理です。tryとfinallyは、途中でエラーがあっても後始末を実行するために使います。

7.8 読み解く:テストの成功・失敗を読む

テストランナーが表示する結果では、okが成功、not okが失敗を表します。三つのテストが成功したときは、概ね次のような表示になります。これは表示形式を説明するための例であり、実行時間などは環境によって異なります。

TAP version 13
# Subtest: タブ数とパネル数が一致し、aria-controlsが対応する
ok 1 - タブ数とパネル数が一致し、aria-controlsが対応する
# Subtest: JavaScriptファイルに構文エラーがない
ok 2 - JavaScriptファイルに構文エラーがない
# Subtest: APIキー未設定ならニュースAPIは503で、秘密ファイルは配信しない
ok 3 - APIキー未設定ならニュースAPIは503で、秘密ファイルは配信しない
1..3
# tests 3
# pass 3
# fail 0

TAPはテスト結果の表示形式です。passは成功数、failは失敗数。not okの項目で期待値と実際の値を比べましょう。

7.9 試す:意図的にHTMLを壊し、修正する

まず、自動テストが問題を見つける様子を確かめます。変更対象は一か所だけです。作業前にファイルのコピーを保存してもかまいません。

  1. 変更前を確認する:node --test test.mjsを実行し、三つのテストが成功することを確かめます。
  2. 予測する:ニュースタブのaria-controlsが存在しないパネル名を指したら、何番目のテストが失敗するか記録します。
  3. 一か所だけ変更する:index.htmlのaria-controls="panel-news"をaria-controls="panel-news-typo"へ変更します。パネル側のid="panel-news"は変更しません。
  4. 結果を確認する:再度node --test test.mjsを実行し、not okになったテスト名と、表示された二つの配列の違いを書き留めます。
  5. 理由を説明する:テストが「値の個数」ではなく「値の一致」で失敗した理由を、assert.deepEqualの行と結び付けて説明します。
  6. 元に戻す:aria-controls="panel-news"へ戻し、同じコマンドを実行して成功することを確かめます。

この実験では、HTMLを壊しても構文テストは成功する場合があります。テストが別々の条件を調べているからです。失敗を一つ作っただけで、三つすべてが同時に失敗するわけではない点にも注目してください。

7.10 試す:構文エラーと実行時の問題を区別する

次は、JavaScriptの構文検査が何を見つけるかを確かめます。ここでも変更は一か所に限定します。

  1. 変更前を確認する:node --check app.jsを実行し、正常終了することを確かめます。
  2. 予測する:ファイルの末尾に閉じられていない括弧があったら、何が起こるかを予測します。
  3. 一か所だけ変更する:app.jsの末尾に、次の意図的に不完全な一行を追加します。これは動作するコード例ではありません。
   function broken( {
  1. 結果を確認する:node --check app.jsとnode --test test.mjsを順に実行し、SyntaxErrorなどのメッセージと、失敗するテストの名前を確認します。
  2. 理由を説明する:構文として解釈できないため、タブやニュースの動作を調べる段階まで進めないことを説明します。
  3. 元に戻す:追加した一行を完全に削除し、構文検査と三つのテストが再び成功することを確認します。

構文検査が成功した場合でも、ブラウザ上の動作まで保証されません。たとえばidの書き間違いによるnullの扱いは、実行したときに初めて問題になることがあります。その場合には、次節の開発者ツールを使います。

7.11 試す:開発者ツールのElementsとConsoleを使う

ブラウザの開発者ツールは、表示されているページの要素や、JavaScriptのエラー、通信内容などを確認するための道具です。Chromium系ブラウザでは主にElements、Console、Networkなどの名前で表示されます。ブラウザによって名称や配置は異なります。

Elementsでtab-newsとpanel-newsのaria-selected・hiddenを確認します。状態が正しければ、次はCSSを調べます。

.filetab[aria-selected="true"] {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

この規則は、タブの選択状態を見た目に反映する部分です。選択状態が更新されない問題と、選択状態は変わるのに色が変わらない問題では、調べる場所が異なります。

Consoleでは、JavaScriptのエラーメッセージを確認したり、式を入力したりできます。次の式は説明のための確認用であり、app.jsに追加する必要はありません。

document.querySelectorAll('[role="tab"]').length
// この教材では通常 2

document.getElementById('tab-news').getAttribute('aria-controls')
// 通常 "panel-news"

document.getElementById('panel-news') !== null
// 通常 true

SyntaxErrorが表示されたらコードの書き方を調べます。一方、TypeErrorのような実行中の例外が出た場合は、エラーの前後で取得した要素が存在していたか、操作する値が想定どおりかを調べます。最初に出たエラーを記録するのは、後から発生したエラーが、その影響で起きている可能性があるためです。

7.12 試す:Networkで通信の失敗を読み取る

ニュースの通信を調べるときはローカルサーバーを起動し、http://127.0.0.1:3000/を開きます。file://では/api/newsを正しく試せません。

APIキー未設定時の動作を安全に確認する場合は、教材フォルダーに.envを用意し、少なくとも次のようにキーの値を空欄にします。実際のAPIキーを本文や実験記録に貼り付ける必要はありません。

GNEWS_API_KEY=
PORT=3000

Node.js 20.6以上の環境では、次のコマンドで起動できます。

node --env-file=.env server.mjs

環境変数がすでに別の場所で設定されている場合は、.envの値だけで上書きされるとは限らないため注意してください。キーが確実に空であることを試す自動テストでは、前述のようにテスト用プロセスのenvを明示的に指定しています。

Networkで/api/news?keyword=...の応答を確認します。キーが空なら503とGNEWS_API_KEYを含むエラーになり、画面は取得失敗を表示します。

503という番号だけで原因は決まりません。応答メッセージも確認しましょう。キー未設定時はGNewsへ通信せずにサーバーが応答します。

なお、教材のapp.jsにはnewsStartedという変数があり、ニュースを初めて開いたときだけloadNews()を実行します。一度エラーになっても、タブを往復するだけでは再度取得しません。

7.13 考える:キャッシュはどこに保存されているか

ニュースが前と同じ内容に見えるとき、キャッシュの影響を疑うことがあります。キャッシュとは、取得や計算を繰り返さずに済むよう、以前の結果を一時的に保持する仕組みです。ただし、どこに保存するかは実装によって異なります。

本教材のserver.mjsには、次のコードがあります。

const ttlMs = 5 * 60 * 1000;
const cache = new Map();

ニュースのキャッシュはサーバーのMapにあり、ブラウザのApplicationでは直接見られません。server.mjsとNetworkのcache値で確認します。成功した取得がない場合は検証できません。

また、server.mjsのAPI応答にはCache-Control: no-storeが設定されています。これはブラウザなどに対するHTTPキャッシュの指定であり、サーバーのメモリ内にあるMapとは別の仕組みです。同じ「キャッシュ」という名前でも、場所と目的を確かめる必要があります。

7.14 考える:自動テストで確認できないことを見極める

自動テストは、繰り返し確かめたい条件を一定の方法で検査できる点が強みです。今回のテストは、HTMLの一部の対応、JavaScriptの構文、キー未設定時のサーバー応答という、外部サービスに頼らず確認しやすい条件を選んでいます。この範囲であれば、修正後に同じコマンドを再実行し、以前の条件が維持されているかを調べられます。

新しい機能を追加したときは、まず「何ができれば成功なのか」を具体的に書き出してみてください。自動化するかどうかは、その条件を繰り返し確認する必要があるか、結果をプログラムで判定できるかによって考えます。目で確かめたこと、テストによって判定したこと、外部条件のため未検証のことを、同じ「確認済み」とまとめないことが大切です。

7.15 章末演習:小さなデバッグ記録を作る

最後に、実験9または10のどちらか一方を選び、次の表を埋めてみましょう。原因を最初から知っていても、あえて「予測」と「観察」を別の欄に書きます。

記録する項目記入例(HTMLの対応を変えた場合)
症状構造テストがnot okになった
仮説aria-controlsとパネルのidが一致しない
変更した一か所panel-newsをpanel-news-typoにした
確認方法node --test test.mjs
観察した事実名前の配列が一致しないと報告された
修正と再検証元の値へ戻し、テストの成功を確認した

発展課題: node --check app.jsが成功してもタブが動かない例を考えます。ElementsとConsoleで確かめ、変更した場合は必ず元に戻しましょう。

発展課題: AIの修正案について、必要な理由と確認方法を説明しましょう。変更は一度に一か所とし、動作を検証します。

7.16 ふりかえり:確認の結果を根拠に直す

本章では、エラーを再現し、仮説を確かめ、修正後に再検証する方法を学びました。test.mjs・Elements・Console・Networkは、それぞれ異なる問題を調べる道具です。

これまでの章では、機能を一つずつ作り、その動作を確かめてきました。ここで身につけたデバッグの手順は、今後新しい機能を加えるときや、制作物を見直すときにも役立ちます。「動いたかどうか」だけでなく、「どの条件を、どの根拠で確認できたか」を説明できることが、自分で学び続けるための土台になります。

第8章

完成・検証・拡張

第8章 完成・検証・拡張

知る:完成は、次の制作への出発点

ここまで、私たちは「New Tab」という一つのWebアプリを通して、HTMLで画面の構造を作り、CSSで見た目を整え、JavaScriptで操作を加える方法を学んできました。 はじめは別々に見えていた技術も、一つの作品の中でつながっていることが分かってきたのではないでしょうか。

では、この「New Tab」は完成したのでしょうか。

検索・タブ・ニュースの表示には、さらに改善できる点があります。

作品には、必ずしも「これで完成」という明確な終点があるわけではありません。しかし、いつまでも作り続けなければならないわけでもありません。

何を実現できたら一区切りとするのか。その判断基準を持つことも、制作の大切な一部です。

そこで本章では、これまでの制作を振り返り、動作を確かめ、結果を記録する方法を学びます。そして、その経験を次の作品へどうつなげるかを考えます。

本章の到達目標は、次の三つです。

  1. 「New Tab」の仕組みと動作を振り返り、確認した結果を説明できる。
  2. 制作の成果をREADMEや設計メモとして残せる。
  3. 新しい機能や作品について、目的と確かめる方法を考えられる。

制作を終えることは、学びを終えることではありません。これまでに得た知識や発見を、次の制作でも使える形に整えていきましょう。

読み解く:「New Tab」を支える仕組み

まずは、完成したアプリの全体像を振り返ってみましょう。

「New Tab」では、HTML、CSS、JavaScript、Node.jsサーバーが、それぞれ異なる役割を担っています。

HTML ── 画面の構造 ──┐
CSS ─── 画面の見た目 ─┼── ブラウザの画面
JavaScript ─ 操作 ───┘        │
                              │ ニュースを要求
                              ▼
                       Node.jsサーバー
                              │
                              │ 外部APIと通信
                              ▼
                           GNews API

図:New Tabを構成する技術の関係(概念図)。ニュースの取得では、JavaScriptが自分のサーバーに要求を送り、サーバーがGNews APIから得た情報をブラウザへ返す。

画面に表示されるものだけでなく、その裏側でどのような処理が行われているかも、少しずつ理解してきました。

仕組み主な役割確認に使うもの
HTML検索フォーム、タブ、表示領域などを構成するElements
CSS色、余白、配置、フォーカス時の見た目を整える画面、Styles
JavaScript操作を受け取り、タブの切り替えやニュースの表示を制御するConsole、Elements、Network
Node.jsサーバーファイルの配信、APIキーの管理、外部APIとの通信を行う起動ログ、HTTP応答
自動テストあらかじめ決めた条件が満たされているかを検査するテスト結果、エラー

これらは、互いに独立しているわけではありません。

たとえば、タブの見た目が切り替わらないとき、原因がCSSにあるとは限りません。HTMLの属性とJavaScriptの処理が正しく対応していない可能性もあります。

だからこそ、問題が起きたときには、画面に見えている現象と、その現象を生み出している仕組みを分けて考えることが大切です。

動作を一つずつ確かめる

作品を一区切りにする前に、今ある機能を実際に操作してみましょう。

「正しく動く」と思っている機能も、操作方法や実行環境を変えると、思いがけない結果になることがあります。

次の表を使い、確認できたことを記録してください。

確認対象試す操作・条件確かめること
初期表示ページを開く検索タブとフォームが表示されるか
検索検索語を入力して送信する送信先URLとqの値が対応しているか
タブタブをクリックするaria-selectedとhiddenが適切に変わるか
キーボードTab、左右矢印、Home、Endを試すフォーカスと選択状態がどう変わるか
ショートカットAlt+1、Alt+2、/を試す意図した操作ができるか、ブラウザなどと競合しないか
ニュースニュースタブを開く/api/newsへの要求と表示結果が対応しているか
再取得再取得ボタンを押す新しい通信が発生するか
キャッシュ短時間で再びニュースを表示する保存されたデータが使われるか、通信が発生するか
通信失敗APIキーを設定せず起動するHTTP応答とエラー案内がどうなるか
配色・画面幅ダークモードや狭い画面で開く文字やレイアウトが読みやすいか
画像画面外のニュース画像を観察する読み込みのタイミングがどう変わるか

実際の結果は、別の欄やメモに書き加えましょう。ブラウザの開発者ツールを使えば、画面だけでは分からない属性や通信の状態も確認できます。

なお、すべての操作が、どの環境でも同じ結果になるとは限りません。

たとえば、ショートカットキーはOSやブラウザの操作と重なる場合があります。また、画像の遅延読み込みは、ブラウザの判断によって読み込みのタイミングが変わります。

重要なのは、予想どおりに動いたかどうかだけではありません。どの条件で、何が起きたのかを確かめることです。

考える:次にタブを増やすなら

現在の「New Tab」に、もう一つタブを追加するとしたら、どのような機能が欲しいでしょうか。

たとえば、制作中に思いついたことを残せる「メモ」タブがあると便利かもしれません。

ただし、すぐにコードを書き始める必要はありません。まずは、何を作りたいのかを言葉にしてみましょう。

小さな設計メモを作る

次の表は、「メモ」タブを追加する場合の設計例です。

考えること設計メモの例
追加する機能短い文章を入力し、保存したメモを表示する
入力するものメモの文章
保存する場所ブラウザのlocalStorageを利用する案を検討する
変更するファイルタブを定義するHTML、見た目を指定するCSS、操作を処理するJavaScript
確かめる方法タブ切り替え、保存、再表示、キーボード操作を確認する

localStorageは、ブラウザ側にデータを保存するための仕組みの一つです。ページを開き直してもデータを利用できるため、簡単なメモ機能の候補になります。ただし、保存先のブラウザやサイトの条件に依存するため、どこでも同じメモを利用できるわけではありません。

ここでは、保存機能を完成させることが目的ではありません。何が必要で、どこを変更し、どう確認するのかを考えることが目的です。

既存の仕組みとのつながりを考える

タブを追加するときには、見た目だけでなく、HTMLとJavaScriptの関係も考える必要があります。

HTMLでは、新しいタブのidとaria-controls、対応するパネルのidとaria-labelledbyが、正しい要素を指すようにします。

JavaScriptでは、タブを管理する処理が三つ目のタブにも対応できるかを調べます。たとえば、数字のショートカットが二つのタブだけを想定していないか、確認が必要です。

さらに、キーボード操作や自動テストも、新しいタブを含めて確かめられるようにします。

ここで作る成果物は、完成したコードではなくても構いません。

「追加する機能」「変更するファイル」「確かめる方法」の三つを説明できる設計メモを残してください。

変更するファイル名は、実際の「New Tab」の構成を調べて記入しましょう。

生成AIに実装案を提案してもらう場合も、この設計メモが判断の基準になります。提案されたコードに、実際には存在しない関数や属性が含まれていないかを確かめ、採用するかどうかを決めましょう。

根拠を確かめる:予測と実測を分けて記録する

制作中には、「なぜ動かなかったのだろう」「この変更で本当に直ったのだろう」と考える場面があります。

そんなときに役立つのが、予測と実測を分けて記録する方法です。

確認の記録は、次の五つを一組にすると、後から読み返しやすくなります。

記録すること書く内容
① 対象と条件何を、どの環境で、どう操作したか
② 予測操作する前に何が起きると思ったか
③ 実測画面、属性、通信、テスト結果など、実際に確認した事実
④ 判断予測と実測は一致したか。どの資料やコードが根拠になるか
⑤ 次の行動修正するか、元に戻すか、別の条件で試すか

たとえば、ニュースの再取得に失敗した場面を考えてみましょう。

単に「ニュースが表示されなかった」と書くだけでは、後から原因を調べることが難しくなります。

一方、APIキーを設定せずに起動した場合の動作を調べ、実際にHTTPステータス503が返されたのであれば、次のように記録できます。

条件: APIキーを設定せずサーバーを起動し、再取得ボタンを押した。 予測: ニュースを取得できず、エラー案内が表示される。 実測: Networkで/api/newsへの要求と503応答を確認し、画面にエラー案内が表示された。 判断: APIキー未設定時のサーバー処理と、ブラウザ側の表示処理を照合する。 次の行動: APIキーを正しく設定した状態で、もう一度確認する。

※これは記録方法の例です。503という応答が返るかどうかは実際のサーバー実装によります。記録には、自分で観察した値を書いてください。

このように記録しておけば、何を確認し、何を根拠に判断したのかが明確になります。

自動テストは、決めた条件を繰り返し検査するときに役立ちます。一方、画面の読みやすさや実際の操作感は、ブラウザを使って確かめる必要があります。

どちらか一方だけでなく、両方を組み合わせることが大切です。

図 V-08-001:目的と予測から、小さな変更、自動テストとブラウザでの確認、結果の記録、次の改善へと循環する検証サイクル。

図 V-08-001:検証サイクル。目的を決め、小さく変更し、結果を確かめて記録する。その繰り返しが、次の改善につながる。

思いどおりに動かなかったことも、大切な結果です。

失敗を隠すのではなく、「どこまで確かめられたか」「何がまだ分からないか」を書き残しましょう。その記録は、次に問題と向き合うときの手掛かりになります。

残す:未来の自分に作品を引き継ぐ

ここまでで、アプリの構造を振り返り、動作を確かめ、結果を記録しました。

では、しばらく時間が経ってから、もう一度「New Tab」を開く場面を想像してみましょう。

起動するために必要なコマンドは何だったでしょうか。APIキーはどこで設定したでしょうか。まだ直していない問題はあったでしょうか。

制作した直後には覚えていることも、時間が経つと忘れてしまいます。

そこで役立つのが、作品の説明書となるREADME.mdです。

README.md:作品の説明書を作る

README.mdは、作品の概要や使い方などを記録するテキストファイルです。

Markdownという書き方を使えば、#で見出しを付けたり、箇条書きで情報を整理したりできます。

「New Tab」のフォルダーにもREADMEを作り、まずは次のような情報をまとめてみましょう。

# New Tab

## 作品の概要
検索とニュースの閲覧ができるWebアプリです。

## 主な機能
- キーワードを入力して検索する
- タブで表示内容を切り替える
- ニュースを取得して表示する

## 起動方法
1. 必要な実行環境を準備する
2. APIキーを所定の方法で設定する
3. [確認した起動コマンド]を実行する
4. [確認したURL]をブラウザで開く

## 動作確認
- [確認したテストコマンド]
- [確認したブラウザと環境]

## 今後の改善
- [追加したい機能]
- [まだ確認できていないこと]

これは書き方の一例です。角かっこで示した部分は、実際に使ったコマンドや確認した内容に書き換えてください。

READMEには、実際に試していない操作や、存在しないファイル名を書かないようにしましょう。

また、APIキーなどの秘密情報は記載せず、設定方法だけを説明します。

最初から立派な説明書を作る必要はありません。まずは、数週間後の自分が迷わず作品を起動できることを目標にしてください。

読みやすいコードも記録の一つ

読みやすさは、READMEだけでなく、コードの中でも大切です。

たとえば、ニュース記事の一覧を表す変数なら、dataという名前よりも、newsArticlesのように役割が分かる名前のほうが、後から処理を理解しやすくなります。

これは命名の例であり、「New Tab」の完成版に同名の変数があるという意味ではありません。

コメントを書くときも、処理をそのまま言い換えるだけでなく、「なぜこの処理が必要なのか」を補足すると役立ちます。

ただし、説明と実際の動作が食い違っていては、かえって混乱を招きます。コードを変更したときには、コメントやREADMEも見直しましょう。

分かりやすい記録を残すことは、未来の自分を助けることでもあります。

広げる:次の制作に向けて

「New Tab」で学んだのは、一つのアプリの作り方だけではありません。

目的を考え、仕組みを調べ、コードを読み、小さく変更し、結果を確かめる。この進め方は、これから制作する別の作品にも応用できます。

ここからは、その経験をどう広げていくかを考えましょう。

アイデアは、身近な気付きから生まれる

新しい作品のきっかけは、日常の小さな不便や興味の中にあります。

「授業の予定を、もっと簡単に確認したい」

「好きな分野のニュースだけをまとめて読みたい」

「制作中に見つけた参考資料を、忘れずに整理したい」

こうした気付きが、次の制作の出発点になります。

思いついたことは、実現方法が分からなくても、まずメモに残してみてください。

そのときに考えたいのは、「誰が、どのような場面で、何をできるようにしたいのか」ということです。

たとえば、「授業の予定を表示するアプリ」を作るなら、最初から多くの機能を考える必要はありません。

まずは今日の予定を一件表示する。その次に、複数の予定を表示する。必要になったら、日付による切り替えを追加する。

このように、小さな目標から始めれば、作りながら必要な知識を増やしていけます。

設計メモは、その最初の一歩になります。

GitHubやCodePenから学ぶ

自分で作るだけでなく、ほかの人がどのように制作しているかを観察することも、学習の助けになります。

GitHubは、ソースコードや変更履歴を管理・共有できるサービスです。公開された作品のファイル構成を調べたり、自分の制作過程を残したりするときに役立ちます。

CodePenでは、HTML、CSS、JavaScriptを編集し、その結果を画面で確認できます。小さな表現や操作を試したいときに便利です。ただし、Node.jsサーバーを含む「New Tab」全体が、そのまま動くわけではありません。

参考になる作品を見つけたら、コードをすぐにコピーするのではなく、まず動きを観察してみましょう。

どのような目的で作られているのか。どの部分が使いやすいのか。自分の作品に取り入れるとしたら、何を変える必要があるのか。

そう考えることで、単なる模倣ではなく、自分の目的に合わせた工夫につながります。

なお、公開されているコードや画像であっても、自由に再利用できるとは限りません。利用条件やライセンスを確認し、参考にした資料を記録しておきましょう。

自分の作品を公開するときも、APIキーなどの秘密情報が含まれていないかを確認してください。

自分で作ることと、既存の仕組みを使うこと

プログラミングには、「車輪の再発明」という言葉があります。

すでに存在する仕組みを、同じ目的のために一から作り直すことを指します。

既存のライブラリやAPIを活用すれば、すべての機能を自分で用意する必要はありません。

「New Tab」でGNews APIを利用したのも、ニュースの収集機能を一から作る代わりに、外部の仕組みを活用した例です。

一方で、学習のために自分で作ることにも意味があります。

タブ切り替えを自分で実装したからこそ、HTMLの属性とJavaScriptの処理がどのようにつながるのかを、具体的に確かめられました。

大切なのは、すべてを自作することでも、すべてを既存の仕組みに任せることでもありません。

仕組みを学ぶために作るのか。作品の目的を実現するために既存のものを使うのか。

その違いを考え、目的に合った方法を選びましょう。

情報デザインとプログラミングをつなげる

情報デザインは、単に情報を美しく並べることではありません。

誰に、何を、どのように伝え、どのように使ってもらうのかを考えることです。

Webアプリでも、見た目だけでなく、操作したときの反応や、問題が起きたときの案内まで含めて、利用者の体験が形作られます。

「New Tab」では、HTMLで情報の関係を示し、CSSで読みやすさを整え、JavaScriptで操作に反応する仕組みを作りました。さらに、キーボード操作や通信失敗時の表示も確かめました。

これらは、考えたデザインを、実際に使える体験へとつなげる過程です。

情報デザインを学ぶ人が、全員プログラマーになる必要はありません。

それでも、プログラミングの基本を理解していれば、自分のアイデアを小さく試し、技術者と具体的に相談し、生成AIの提案が目的に合っているかを判断しやすくなります。

「何が作れるか」と同じくらい、「誰のために作るのか」「どのように使われるのか」を大切にしてください。

ふりかえり:学びは、ここから続いていく

本書では、完成した「New Tab」を最初からすべて理解しようとはしませんでした。

HTMLで構造を見つけ、CSSで画面との関係を確かめ、JavaScriptの処理を読み解きました。さらに、外部APIとの通信を調べ、問題が起きたときにはテストや開発者ツールを使って原因を探ってきました。

一つひとつの学習は小さなものだったかもしれません。

しかし、その積み重ねによって、画面に表示されたものをただ使うだけでなく、「なぜこのように動くのだろう」と仕組みに目を向ける方法を学んできました。

最終章で大切にしたのは、「動いた」という結果だけで制作を終えないことです。

何を確かめ、どこまで実現できて、何がまだ残っているのか。それらを記録すれば、今回の制作で得た経験を、次の制作へ引き継ぐことができます。

これからの制作では、生成AIや既存のライブラリが、さらに多くの場面で力になってくれるでしょう。

しかし、何を作りたいのかを考え、提案された方法を確かめ、最終的に採用するかを決めるのは、作り手であるあなた自身です。

すべてを理解してから、作り始める必要はありません。

分からないことがあれば、資料を探す。小さく試す。結果を観察する。必要なら作り直す。

その繰り返しの中で、できることは少しずつ増えていきます。

この本は、ここで終わります。

けれども、あなたの制作まで、ここで終える必要はありません。

次に作りたいものが見つかったとき、最初の一行をどう書けばよいか迷ったときには、これまで身につけた方法を思い出してください。

目的を持ち、仕組みを知り、試し、確かめる。

その小さな積み重ねが、まだ形になっていないアイデアを、いつか誰かが使える作品へと変えていきます。

まとめ

まとめ

学習のまとめ

読者の皆様へ

第1〜3章で設計とHTML・CSS、第4〜6章で操作と通信、第7〜8章で検証と完成を扱いました。変更したい機能に対応するファイルを確かめてみましょう。

今回はフレームワークを使わず、素のJavaScriptで実装しました。次は別の方法も調べ、小さな制作で試してみてください。

本書における参考資料