← コース一覧
Angular
Googleが開発するフルスタックなフロントエンドフレームワークを学びます。環境構築とCLIの使い方から始めて、スタンドアロンコンポーネントとシグナルを中心に、モダンなAngularの書き方を初学者向けに1テーマずつ細かく解説します。テンプレート・ディレクティブ・通信・ルーティング・DIの応用を経て、最新のv22で正式版になったシグナルフォームやゾーンレスまで到達します。
公式ドキュメント: Angular 日本語ドキュメント ↗はじめの一歩
- 1Angularとは — SPAをまるごと作れるフレームワークAngularは、Googleが開発しているWebアプリケーションのフレームワークです。作れるのはSPA(シングルページアプリケーション)と呼ばれる形式のアプリで、サーバーから受け取るHTMLは実質1枚だけ、あとはJavaScriptが画面を書き換えることでページ遷移のような動きを実現します。画面がまるごと再読み込みされないため、デスクトップアプリのような滑らかな操作感になります。
- 2開発環境の準備 — Node.jsとAngular CLIAngularの開発にはNode.js(v20以上)が必要です。まず node --version でバージョンを確認し、足りなければ公式サイトからLTS版をインストールします。次に npm install -g @angular/cli でAngular CLIを入れます。CLIはプロジェクトの作成・開発サーバーの起動・ファイルの雛形生成までを担当する、Angular開発の相棒となるコマンドラインツールです。
- 3プロジェクト構成 — ng newが作るファイルの読み方ng new が生成するファイルは多く見えますが、最初に読むべきものは一握りです。package.json は依存ライブラリとコマンドの一覧、angular.json はビルドや開発サーバーなどCLIの設定です。src/index.html がブラウザに届く唯一のHTML、src/main.ts がアプリを起動する入口のスクリプトです。
- 4起動の流れ — index.htmlからAppが表示されるまでブラウザに画面が出るまでの道筋は一本道です。まずブラウザが src/index.html を読み込みます。bodyには <app-root></app-root> という見慣れないタグが1つあるだけです。次に main.ts が実行され、bootstrapApplication(App, appConfig) がアプリを起動します。すると App コンポーネントが <app-root> の位置に描画され、画面が表示されます。
- 5ng generate component — 部品の雛形を自動生成する新しいコンポーネントは手書きせず、CLIに作らせます。ng generate component user-card と実行すると、src/app/user-card/ フォルダに .ts(クラス)・.html(テンプレート)・.css(スタイル)の3点セットが生成されます。クラスには @Component デコレータと selector が設定済みで、すぐにタグとして使える状態になっています。
- 6開発サイクル — 自動リロードとエラーの読み方Angularの開発は「ファイルを保存 → ng serve が自動で再ビルド → ブラウザが自動リロード」の繰り返しです。保存するたびに変更が数秒で画面に反映されるので、コードと画面を行き来しながら小さく試すのが基本のリズムです。ng serve は開発中ずっと起動したままにしておきます。
コンポーネント基礎
- 7コンポーネント — UIの基本単位Angularの画面は、コンポーネントという部品を組み合わせて作ります。コンポーネントはTypeScriptのクラスに @Component デコレータを付けたもので、selector(HTMLタグ名)と template(表示するHTML)を指定します。selectorに app-hello と書けば、ほかの場所から <app-hello /> というタグで呼び出せます。
- 8テンプレート補間 — {{ }} で値を表示するクラスが持つ値をテンプレートに表示するには、{{ userName }} のような二重波カッコを使います。これをテンプレート補間と呼び、カッコの中にはプロパティ名のほか、{{ price * 1.1 }} のような簡単な式も書けます。式の結果は文字列に変換されてHTMLに埋め込まれます。
- 9プロパティバインディング — [ ] で値を流し込むHTML要素の属性にクラスの値を渡すには、属性名を角カッコで囲んで [src]="imageUrl" のように書きます。これをプロパティバインディングと呼び、右辺の式が評価されて、その結果が要素のプロパティに設定されます。クラス側の値が変われば、要素の状態も自動で更新されます。
- 10イベントバインディング — ( ) で操作を受け取るクリックや入力などのユーザー操作をクラスのメソッドにつなぐには、イベント名を丸カッコで囲んで (click)="increment()" のように書きます。これをイベントバインディングと呼び、イベントが発生するとカッコ内の式が実行されます。$event と書けば、イベントの詳細情報をメソッドに渡すこともできます。
- 11クラスとスタイルのバインディングCSSクラスの付け外しを条件で切り替えるには [class.クラス名]="条件" と書きます。条件が true のときだけそのクラスが付与されます。同じように [style.スタイル名]="値" で、個別のスタイルに値を直接バインドすることもできます。
- 12テンプレートとスタイルの外部ファイル化これまでのレッスンでは template にHTMLを文字列で直接書いてきましたが、実務ではHTMLとCSSをそれぞれ別ファイルに分けるのが一般的です。@Component の template を templateUrl: './xxx.component.html' に、styles を styleUrl: './xxx.component.css' に置き換えると、見た目はファイルに、ロジックはクラスに、と役割ごとにファイルが分かれます。
- 13ng-content — 中身を差し込めるコンポーネントカードや枠線付きパネルのように「外側の見た目は共通だが、中身は使う側で決めたい」部品を作るには <ng-content /> を使います。子のテンプレートに <ng-content /> を置くと、親が <app-card>ここの中身</app-card> のようにタグで囲んだHTMLが、その場所に差し込まれて表示されます。この仕組みをコンテンツ投影と呼びます。
制御フロー
- 14@if — 条件で表示を切り替えるテンプレート内で条件分岐をするには @if を使います。@if (条件) { ... } と書くと、条件が true のときだけ中身が描画されます。@else { ... } や @else if (条件) { ... } を続けて、条件ごとの出し分けもできます。
- 15@for — 配列を繰り返し表示する配列の要素を順に描画するには @for を使います。@for (item of items; track item.id) { ... } と書くと、配列の要素ごとに中身が繰り返し描画されます。track には各要素を一意に識別できる値(IDなど)を必ず指定します。@empty { ... } を続ければ、配列が空のときの表示も書けます。
- 16@switch — 値ごとに表示を分ける1つの値が取りうる複数のパターンで表示を分けるには @switch を使います。@switch (値) { @case (候補) { ... } } と書くと、値に一致した @case の中身だけが描画されます。どの @case にも一致しなかったときの表示は @default { ... } に書きます。
- 17@defer — 重い部分を後から読み込む@defer は、テンプレートの一部の読み込みを必要になるまで遅らせる仕組みです。@defer { <app-heavy-chart /> } と囲むと、その中のコンポーネントのコードは最初のページ表示には含まれず、あとから別途読み込まれます。最初に表示されるJavaScriptが小さくなるため、ページの初期表示が速くなります。
シグナルと状態
- 18実行可signal — リアクティブな状態の入れ物シグナルは「値が変わったことを周りに知らせる入れ物」です。count = signal(0) のように作り、count() と関数のように呼んで値を読み、count.set(5) で更新します。現在値をもとに更新するなら count.update((v) => v + 1) と書きます。
- 19実行可computed — シグナルから作る派生値computed は、ほかのシグナルから自動で計算される読み取り専用のシグナルです。total = computed(() => price() * quantity()) と書くと、price か quantity が変わるたびに total も新しい値になります。読み方は普通のシグナルと同じで total() です。
- 20effect — シグナルの変更に反応するeffect は、シグナルが変わるたびに実行したい処理を登録する仕組みです。effect(() => { ... }) と書くと、その関数の中で読んでいるシグナルが自動で追跡され、どれかが変わるたびに関数が再実行されます。コンポーネントのコンストラクタの中で登録するのが基本です。
- 21input() — 親から値を受け取る親コンポーネントから子へデータを渡すには、子側で input() を宣言します。task = input.required<string>() と書くと、親はテンプレートで [task]="値" の形で値を渡せます。受け取った値はシグナルなので、子のテンプレートでは task() と関数として読みます。
- 22output() — 親へイベントを通知する子コンポーネントから親へ「ボタンが押された」などの出来事を伝えるには、子側で output() を宣言します。deleted = output<number>() と書いて deleted.emit(42) を呼ぶと、親はテンプレートの (deleted)="onDeleted($event)" でそれを受け取れます。$event にはemitした値が入ります。
- 23model() — 親と子で値を双方向に同期する親から受け取った値を子側からも書き換えたいときは、model() を使います。value = model(0) と宣言すると、子は value() で読み、value.set() や value.update() で書き換えられます。親はテンプレートで [(value)]="score" と書くだけで、どちらの変更ももう一方へ自動で反映されます。
サービスと通信
- 24サービスとDI — ロジックの置き場所画面の表示に直接関係しないロジック(データの管理、計算、ログ出力など)は、サービスというクラスに切り出します。@Injectable({ providedIn: 'root' }) を付けて定義すると、Angularがインスタンスを1つだけ作り、必要とするコンポーネントに渡してくれます。この「必要な部品をフレームワークが渡してくれる」仕組みを依存性の注入(DI)と呼びます。
- 25inject() — サービスを受け取るコンポーネントからサービスを使うには、inject() 関数でインスタンスを受け取ります。private cart = inject(CartService) とフィールドの初期化として書くだけで、DIコンテナが管理しているインスタンスが手に入ります。あとは this.cart.add() のように普通のメソッド呼び出しで使えます。
- 26HttpClient — APIとの通信サーバーのAPIからデータを取得するには HttpClient を使います。アプリ起動時の providers に provideHttpClient() を追加したうえで、コンポーネントやサービスで inject(HttpClient) して this.http.get<User[]>('/api/users') のように呼び出します。レスポンスは subscribe で受け取り、届いたデータを状態に反映します。
- 27Observable と RxJS — 時間とともに届く値前のレッスンの HttpClient は、結果を Observable という入れ物で返します。Observable は「これから(非同期に)届くかもしれない値の流れ」を表すもので、subscribe(購読)して初めて処理が動き、値が届くとコールバックが呼ばれます。1回きりの通信結果も、何度も発火するイベントも、同じ Observable として扱えるのが特徴です。
- 28ルーティング — URLで画面を切り替えるルーティングは、URLに応じて表示するコンポーネントを切り替える仕組みです。Routes型の配列に { path: 'about', component: AboutComponent } のようにURLとコンポーネントの対応を定義し、アプリ起動時に provideRouter(routes) で登録します。コンポーネントが表示される場所は <router-outlet /> で指定します。
- 29routerLink — 画面遷移のリンクを作るルーティングを設定したら、ページ間を移動するリンクは <a href> ではなく <a routerLink="/about"> と書きます。routerLink を使うと、ページ全体を再読み込みせずに、必要な部分だけを差し替えて画面が切り替わります。これがシングルページアプリの軽快な遷移を支える仕組みです。href を使うとブラウザがページ全体を読み込み直してしまい、その良さが失われます。
- 30ルートパラメータ — URLから値を受け取るURLの一部を可変にして値を受け取るには、ルート定義のpathに 'users/:id' のようにコロン付きの名前を書きます。表示されたコンポーネント側では inject(ActivatedRoute) でルート情報を受け取り、route.snapshot.paramMap.get('id') で実際の値を取り出します。/users/7 にアクセスすれば '7' が得られます。
- 31プログラムでの画面遷移 — Router.navigateリンクのクリック以外、たとえば「ログインに成功したらトップへ移動する」のように、処理の結果に応じてコード側から画面を切り替えたいことがあります。そのときは inject(Router) で受け取った Router の navigate() を呼びます。this.router.navigate(['/users', id]) と書くと、配列の要素がつながって /users/7 のようなURLへ遷移します。
フォームとテンプレート機能
- 32ライフサイクルフック — ngOnInit と ngOnDestroyコンポーネントには「生まれてから消えるまで」の段階があり、その節目で呼ばれるメソッドをライフサイクルフックと呼びます。ngOnInit() は入力値が揃って表示の準備が整った直後に1回だけ呼ばれ、ngOnDestroy() は画面から取り除かれる直前に呼ばれます。クラスにメソッドとして定義しておくだけで、Angularが適切なタイミングで自動的に呼び出します。
- 33パイプ — 値を表示用に整形するパイプは、テンプレート内で値を「見せ方」に変換する仕組みです。{{ price | currency }} のように縦棒に続けてパイプ名を書くと、元の値はそのままに、表示だけが整形されます。date・currency・uppercase・lowercase・json・percent など、よく使う変換はAngularに最初から用意されています。
- 34実行可カスタムパイプ — @Pipe で独自の整形を作る用意されたパイプで足りないときは、自分でパイプを作れます。@Pipe({ name: 'truncate' }) を付けたクラスに transform(value, ...args) メソッドを実装すると、テンプレートで {{ text | truncate:20 }} のように使えるようになります。transform の第1引数が変換前の値、2つ目以降がコロンで渡した引数で、戻り値が表示される値になります。
- 35リアクティブフォーム — FormControl と FormGroupリアクティブフォームは、フォームの値や状態をTypeScript側のオブジェクトとして扱う仕組みです。1つの入力欄を FormControl('') で表し、複数をまとめた form = new FormGroup({ name: ..., email: ... }) で1つのフォームを構成します。テンプレートでは <form [formGroup]="form"> でフォーム全体を結び付け、各入力欄に formControlName="name" を書いて対応させます。
- 36実行可フォームのバリデーション — Validators入力値が正しいかを検証するには、FormControl の第2引数に Validators を渡します。new FormControl('', [Validators.required, Validators.email]) と書くと、未入力ならエラー、メール形式でなければエラー、と判定されます。各コントロールやフォーム全体は valid / invalid というプロパティを持ち、検証結果に応じて true / false が切り替わります。
- 37属性ディレクティブ — ngClass と ngStyle属性ディレクティブは、要素の見た目や振る舞いを後付けで変える仕組みです。ngClass は条件に応じて複数のCSSクラスをまとめて付け外しでき、[ngClass]="{ active: isActive, error: hasError }" のようにオブジェクトで「クラス名: 付けるか」を渡します。ngStyle も同様に [ngStyle]="{ color: textColor }" でスタイルをまとめて指定できます。
テンプレートとディレクティブの応用
- 38双方向バインディング — [(ngModel)] で入力と同期するinput要素の入力内容とクラスのプロパティを常に同じ値に保つには、双方向バインディングを使います。FormsModule をコンポーネントの imports に追加し、<input [(ngModel)]="name"> と書くだけで、入力すればプロパティが更新され、クラス側で値を変えれば入力欄の表示も変わります。
- 39テンプレート参照変数 — # で要素に名前を付けるテンプレート内の要素に #inputEl のようにシャープ付きの名前を書くと、その要素を同じテンプレート内の別の場所から参照できます。これをテンプレート参照変数と呼び、input要素なら inputEl.value で現在の入力値を、inputEl.focus() でフォーカス操作を直接呼び出せます。
- 40@let — テンプレート内にローカル変数を作る@let は、テンプレートの中でローカル変数を宣言する構文です(v18.1以降)。@let taxed = price() * 1.1; のように書くと、その結果に名前が付き、以降のテンプレートで {{ taxed }} と何度でも使えます。値が変われば @let の結果も自動で追従します。
- 41カスタム属性ディレクティブ — 要素に振る舞いを足す属性ディレクティブは、既存の要素に振る舞いを後付けする部品で、@Directive デコレータで自作できます。selector を [appHighlight] のように角カッコで書くと、<p appHighlight> のように属性として要素に付けられます。inject(ElementRef) で付けられた要素そのものを取得し、スタイルの変更などの操作ができます。
- 42host — 自分自身の要素へバインドするコンポーネントやディレクティブのタグそのもの(ホスト要素)にクラスやイベントをバインドするには、@Component や @Directive の host プロパティを使います。'[class.active]': 'isActive()' と書けばホスト要素にクラスが条件付きで付き、'(click)': 'onClick()' と書けばホスト要素へのクリックがメソッドにつながります。
- 43viewChild() — テンプレートの要素をクラスから取得するテンプレート内の要素や子コンポーネントをクラス側のコードから扱うには viewChild() を使います。inputEl = viewChild.required<ElementRef>('inputEl') と書くと、テンプレートの #inputEl が付いた要素をシグナルとして取得でき、inputEl().nativeElement.focus() のように操作できます。
通信と非同期の応用
- 44asyncパイプ — 購読と解除をAngularに任せるasyncパイプは、Observableをテンプレートで直接受け取るための仕組みです。{{ user$ | async }} のように書くと、Angularが裏でsubscribeして値が届くたびに表示を更新し、コンポーネントが破棄されるときには購読の解除まで自動で行ってくれます。@if (users$ | async; as users) の形にすれば、届いた値に users という名前を付けてブロック内で使い回せます。
- 45よく使うRxJSオペレーター — map / filter / switchMapRxJSのオペレーターは、Observableを流れる値を加工する部品です。pipe() は「データが通る加工ライン」で、その中にオペレーターを並べると、値がラインを通るたびに順番に加工されます。map は値を別の形に変換し、filter は条件を満たす値だけを通します。まずはこの2つと、次に説明する switchMap の3つを覚えれば実務の大半をカバーできます。
- 46toSignal と toObservable — 2つの世界の橋渡しAngularには「時間とともに届く値」を扱うRxJSの世界と、「今の値」を扱うシグナルの世界があります。@angular/core/rxjs-interop の toSignal と toObservable は、この2つを行き来する橋です。toSignal(obs$) はObservableをシグナルに変換し、値が届くたびにシグナルが更新されます。逆に toObservable(sig) はシグナルの変化をObservableの流れとして取り出します。
- 47HTTPエラーへの備え — catchError と status通信はサーバーの障害やネットワークの切断でかならず失敗することがあります。RxJSの catchError オペレーターを pipe に挟むと、エラーが起きたときの代替処理を流れの中に組み込めます。catchError(() => of([])) のように of で代替値を返せば、流れはエラーで止まらず「空の結果が届いた」ことにして画面を守れます。
- 48インターセプター — すべての通信に共通処理を差し込むインターセプターは、アプリから出ていくすべてのHTTPリクエストと、返ってくるレスポンスに共通処理を差し込む仕組みです。現在は HttpInterceptorFn 型の関数として書くのが標準で、(req, next) を受け取り、リクエストを加工してから next(req) で次へ渡します。登録は provideHttpClient(withInterceptors([...])) に関数を並べるだけです。
ルーティングの応用
- 49子ルートとレイアウト — ルートを入れ子にするルート定義には children というプロパティがあり、ルートを入れ子にできます。{ path: 'admin', component: AdminLayout, children: [...] } と書くと、/admin/orders のようなURLでは、まず親の AdminLayout が表示され、その中の <router-outlet /> に子のコンポーネントがはめ込まれます。
- 50ルートの遅延読み込み — 使うまで読み込まないloadComponent を使うと、そのルートのコンポーネントを「実際にそのURLへ移動したとき」に初めて読み込めます。component: Admin の代わりに loadComponent: () => import('./admin/admin').then(m => m.Admin) と書くだけで、Admin のコードは初回表示のバンドルから切り離されます。
- 51ルートガード — 入ってよいかを判定するルートガードは、そのルートに入ってよいかを遷移の直前に判定する仕組みです。CanActivateFn 型の関数を作り、ルート定義に canActivate: [authGuard] と登録すると、遷移のたびにその関数が呼ばれます。true を返せば遷移が続行され、false や別のURLを返せば遷移が止まります。
- 52クエリパラメータ — 検索条件をURLに載せるURLの ? 以降に付く ?page=2&sort=price のような部分をクエリパラメータと呼びます。ルートパラメータ(:id)が「どのページか」を決めるのに対し、クエリは「そのページの表示条件」を運びます。読み取りは inject(ActivatedRoute) の queryParamMap から、リンク側は routerLink の [queryParams] で付与します。
DIの応用
- 53InjectionToken — クラスではない値を配るDIで配れるのはサービスクラスだけではありません。設定オブジェクトやAPIのベースURLのような「ただの値」も配れます。ただし値には型の実体(クラス)がないため、DIの目印として InjectionToken を作ります。new InjectionToken<string>('api.base.url') のように名前を付けて定義します。
- 54提供範囲 — root提供とコンポーネント提供サービスのインスタンスが「どの範囲で1個か」は、どこで提供するかで決まります。@Injectable({ providedIn: 'root' }) はアプリ全体で1個の共有インスタンスです。一方、コンポーネントの providers: [CounterService] に書くと、そのコンポーネントが作られるたびに専用のインスタンスが作られ、その部品ツリーの中だけで共有されます。
- 55DIと差し替えやすさ — テストで効く御利益DIの最大の御利益は「本物を偽物に差し替えられる」ことです。コンポーネントは inject(UserService) と目印を頼りに受け取るだけで、実体が何かを知りません。だからDIコンテナへの登録を変えるだけで、本番ではAPIと通信する本物、テストでは決まった値を返すフェイク、と中身を切り替えられます。
Angular v22の新機能
- 56シグナルフォーム — form() でフォームもシグナルにv22で正式版になったシグナルフォームは、フォームの値をシグナルのモデルとして持ち、form() でそこからフォームを組み立てる新しい方式です。signal({ email: '', password: '' }) のようにモデルを作り、form(model, schema) に渡すと、各項目に対応したフィールドのツリーができます。これまで学んだリアクティブフォーム(FormGroup / FormControl)の、シグナル時代版だと考えると分かりやすいです。
- 57resource() — 非同期データをシグナルで扱うv22で正式版になった resource() は、APIからの非同期取得をシグナルの世界に取り込む仕組みです。resource({ params, loader }) と書くと、params が返すシグナルの値が変わるたびに loader が自動で再実行され、結果がシグナルとして手に入ります。以前のように ngOnInit で subscribe して状態へ詰め替える定型コードが不要になります。
- 58ゾーンレスとOnPushの既定化v22では、新しく作るアプリから zone.js が標準で外れ、ゾーンレスがデフォルトになりました。これまでAngularは zone.js を使って「何か起きたかもしれない」タイミングで画面全体を見直していましたが、ゾーンレスでは更新のきっかけをシグナルなどの明示的な変更に絞ります。これがシグナル中心への移行の総仕上げにあたります。
- 59Angular Aria — アクセシビリティを部品で担保するv22で正式版になった Angular Aria は、タブ・メニュー・アコーディオンといった操作部品の「アクセシビリティの面倒な部分」をディレクティブとして提供する仕組みです。ARIA属性の付与、矢印キーでの移動、フォーカスの管理といった、自前で正しく実装するのが難しい部分をAngularが肩代わりしてくれます。
シグナルの応用
- 60linkedSignal — 連動して作り直せる書き込み可能な状態computed は依存するシグナルから自動で計算される便利な仕組みでしたが、読み取り専用で、外から値を上書きすることはできません。ところが実務では「基本は他のシグナルから決まるが、ユーザーの操作で上書きもしたい」という状態がよく出てきます。たとえば商品リストの「選択中の商品」です。リストが切り替わったら選択は先頭に戻したい、でもユーザーがリストの中から自由に選び直せてほしい。この両立を担うのが linkedSignal です。
- 61untracked — 依存に数えずにシグナルを読むcomputed や effect の中でシグナルを読むと、そのシグナルは自動で「依存」として登録され、値が変わるたびに再計算・再実行が走ります。これがシグナルの便利さの核心ですが、ときどき「値は使いたいけれど、その値の変更では再実行してほしくない」という場面があります。たとえば effect でアクセスログを送るとき、送信のきっかけはページの変化だけにしたいのに、ログ本文に含めたいユーザー名まで依存に入ると、ユーザー名の変更でも余計なログが飛んでしまいます。
- 62DestroyRef と takeUntilDestroyed — 後片付けを宣言的にライフサイクルフックのレッスンで、コンポーネントが破棄されるときは ngOnDestroy で購読解除などの後片付けをすると学びました。ただ、解除のためだけに Subscription をプロパティに持ち、ngOnDestroy を実装するのはかなり定型的です。現代のAngularには、この後片付けをもっと短く書ける道具が2つあります。DestroyRef と takeUntilDestroyed です。
- 63contentChild() — 差し込まれた中身をクラスから掴むviewChild() のレッスンでは、自分のテンプレートに書いた要素をクラスから取得しました。一方、ng-content のレッスンで学んだように、コンポーネントには親から「タグの間に差し込まれた中身」もやってきます。この差し込まれた中身は自分のテンプレートに書いたものではないので、viewChild() では取れません。そこで使うのが contentChild() と contentChildren() です。
- 64httpResource() — HTTP取得をさらに短く書くv22の新機能のレッスンで学んだ resource() は、loader に fetch などの取得処理を自分で書く汎用の仕組みでした。しかしAngularでのデータ取得といえば大半がHTTPです。そこで、HTTP取得に特化して resource() をさらに短く書けるようにしたのが httpResource() です。URLを返す関数を渡すだけで、その中で読んだシグナルが変わるたびに自動で再取得してくれます。
SSRとハイドレーション
- 65SSRとは — 最初のHTMLをサーバで作るこれまで作ってきたAngularアプリは、クライアントサイドレンダリング(CSR)という方式でした。起動の流れのレッスンで見た通り、サーバが返す index.html はほぼ空っぽで、ブラウザがJavaScriptを読み込んで実行して、初めて画面が現れます。この方式の弱点は2つあります。JSの読み込みと実行が終わるまでユーザーには白い画面が見えること、そして検索エンジンやSNSのクローラーがJS実行前の空のHTMLしか読めない場合があることです。
- 66@angular/ssr — ng add で始めるサーバサイドレンダリングAngularでSSRを始めるのは、ng add @angular/ssr の1コマンドです。実行すると、Node.js(Express)のサーバ本体である server.ts、サーバ側の起動口となる main.server.ts などが追加され、ビルドが「ブラウザ用」と「サーバ用」の二段構えに変わります。ng new のときに --ssr を付けて、最初からSSR込みで作ることもできます。
- 67ハイドレーション — サーバのHTMLをそのまま引き継ぐSSRで届いたHTMLは、見えはしますが、まだ動きません。ボタンのクリックにも反応しない「静止画」の状態です。その後ブラウザ側でAngularが起動し、届いているHTMLにイベントリスナーや状態を紐付けて、動くアプリに変えます。この引き継ぎの工程をハイドレーション(hydration)と呼びます。乾いたHTMLに水を注いで生き返らせる、というイメージの名前です。
- 68インクリメンタルハイドレーション — 必要な所から順に起こす通常のハイドレーションは、ページ全体をまとめて一度に起こします。つまり、画面のいちばん下にある重い部品のJSも、最初にまとめて読み込まれます。制御フローのレッスンで学んだ @defer は「重い部分を後から読み込む」道具でしたが、この考え方をハイドレーションに持ち込んだのがインクリメンタルハイドレーションです。ページの部品ごとに「見えたら起こす」「触られたら起こす」と、目覚めのタイミングを選べます。
レガシーコードの読み方
- 69NgModule — スタンドアロン以前のアプリの骨組みここからのセクションは、これまでと目的が少し違います。書けるようになるためではなく、読めるようになるためのレッスンです。スタンドアロンコンポーネントが標準になる前、Angularには長い「NgModule時代」があり、現場で動いている既存コードや検索で出てくる記事の多くは、いまもその時代の書き方です。まずはその中心にあるNgModuleから読み解きます。当時は、コンポーネントもパイプもサービスも、必ずどこかのNgModuleに登録しないと使えませんでした。
- 70*ngIf と *ngFor — @if / @for 以前の制御フロー制御フローのセクションで学んだ @if / @for / @switch は、比較的新しい構文です。それ以前の長い期間、条件分岐や繰り返しは *ngIf・*ngFor・*ngSwitch という、* 付きのディレクティブで書かれていました。既存プロジェクトのテンプレートは大半がこちらなので、対訳を頭に入れておきましょう。もう1つの違いとして、これらは CommonModule を imports しないと使えませんでした。テンプレートに *ngIf があるのに動かない、というときは import 忘れが定番の原因でした。
- 71ng-template と ng-container — 構造ディレクティブの正体前のレッスンで出てきた <ng-template> の正体を掘り下げます。ng-template は「描画されない下書き」です。テンプレートに書いても、その中身はそのままではDOMに出力されません。誰かが「この下書きを実体化して」と要求したときに初めて描画されます。*ngIf の else で #loading という名前の下書きを参照できたのはこの仕組みのおかげです。実は @if や @defer が内部でやっていることも本質は同じで、「ブロックの中身を下書きとして持ち、条件が満たされたら実体化する」という機構です。
- 72@Input / @Output / @ViewChild — デコレータ時代の部品APIinput() / output() / viewChild() というシグナルベースの関数APIには、それぞれ1世代前の姿があります。プロパティにデコレータを付ける書き方です。読み替えのコツを先に言うと、「デコレータ版はただのプロパティ、関数版はシグナル」。@Input() name = ''; と書かれた親からの値は普通のプロパティに入るので、() を付けずにそのまま参照します。値の変更に反応したいときは、シグナルの computed が使えないため、ngOnChanges というライフサイクルフックや setter で受け止めていました。
- 73constructor注入と BehaviorSubject — 旧世代のサービスの読み方最後はサービスまわりの旧作法を2つまとめて読み解きます。1つ目は注入の書き方です。inject() 関数が主流になる前は、constructor(private http: HttpClient) {} という「コンストラクタ注入」が標準でした。引数に private などの修飾子を付けると、TypeScriptがプロパティ宣言と代入を自動でやってくれる省略記法で、this.http として使えるようになります。読み方はシンプルで、コンストラクタの引数リストがそのクラスの依存一覧です。
Angularの内部機構
- 74AOTコンパイル — テンプレートは命令列に変換されるこのセクションでは、Angularで書いたコードがどう変換されてブラウザで動いているのか、という土台を掘り下げます。出発点はこの事実です: ブラウザはAngularのテンプレートを一切見ていません。ビルド時にAngularコンパイラ(ngtsc)が @Component のテンプレートを解析し、「DOMを組み立てるJavaScript命令の列」であるテンプレート関数に変換して、クラスの静的プロパティ(ɵcmp)として埋め込みます。これがAOT(Ahead-of-Time)コンパイルで、実行時のブラウザにはコンパイラを届けないので、その分軽く・速く・エラーはビルド時に見つかります。
- 75コンポーネントを追加すると何が変わるか — ビルド時の変化ng generate component で部品を1つ作り、親の imports に書き、テンプレートに <app-user-card> と書く — この日常の操作で、ビルド成果物に何が起きるかを追いかけます。まずコンパイラは、親テンプレートに現れたタグ名を、imports に並んだ各部品の selector と突き合わせます。重要なのは、このセレクタ照合がビルド時に完了していることです。実行時のAngularがHTMLの中から <app-user-card> を検索して探し回る、という処理は存在しません。
- 76TView と LView — 設計図と台帳実行時のAngular(Ivyランタイム)は、表示中のコンポーネントごとに2種類のデータを持っています。1つ目のTViewは「テンプレートの設計図」です。0番スロットは h1 要素、1番はテキストノード、というテンプレートの構造情報を持ち、同じコンポーネントを画面に100個表示してもTViewは1つだけで全インスタンスが共有します。作られるのは最初の1個目が表示されるときの一度きりで、これをファーストパスと呼びます。
- 77コンポーネントが画面に生まれる手順 — 実行時の変化@if が真になった瞬間、ルーティングで画面が切り替わった瞬間、起動時に <app-root> が動き出す瞬間 — コンポーネントが画面に現れるとき、内部では毎回同じ誕生手順が走ります。順に、(1) ホスト要素(<app-user-card>)をDOMに作る、(2) そのノード用のインジェクタを親のインジェクタにつなぐ、(3) コンストラクタを実行する(inject() が動くのはここ)、(4) 親からの input の値を流し込む、(5) ngOnInit などのライフサイクルフックを呼ぶ、(6) テンプレート関数を作成モードで実行し、テンプレート内に子コンポーネントがいればそこで同じ手順を再帰的に繰り返す、という流れです。
- 78変更検知 — 「いつ・どこを」再描画するかの世代交代台帳(LView)の更新モードを「いつ・どのコンポーネントで走らせるか」を決める仕組みが変更検知です。zone.js時代の答えは「いつ=何か非同期処理が終わるたび、どこを=全部」でした。zone.jsは起動時にクリック・タイマー・HTTP応答などの非同期APIを横取り(パッチ)し、「何かが終わった、画面が変わったかもしれない」というたびに、ルートから全コンポーネントの更新モードを実行します(tick)。何が変わったか分からない前提なので全査定するしかなく、確実ですが画面が大きいほど無駄が増えます。
- 79破棄と再利用 — コンポーネントが消えるときに起きること@if が偽になる、ルート遷移で画面が入れ替わる、@for の配列から要素が消える — コンポーネントが画面から消えるときは、誕生手順の逆再生が走ります。子から順に ngOnDestroy と DestroyRef に登録されたコールバックを呼び、Angularが付けたイベントリスナーを外し、LView を親のツリーから切り離し、最後にDOMノードを取り除きます。シグナルの応用で学んだ takeUntilDestroyed が「勝手に解除してくれる」のは、この逆再生の手順に購読解除を登録していたからです。
設計の考え方
- 80部品を切る基準 — いつコンポーネントに分けるかコンポーネントを分けるか迷ったら、3つの基準で考えます。①2箇所以上で使う(使いそうな見込みも含む)、②「商品カード」「評価スター」のように単体で名前が付けられる、③Input で受け取り Output で通知するだけで完結し、親の事情を知らなくても動く。この3つのうち2つ以上に当てはまるなら、切り出す価値が高い部品です。
- 81賢い親と表示に徹する子 — 役割で層を分けるコンポーネントは「ページコンポーネント」と「表示コンポーネント」の2種類に役割分担させると設計が安定します。ページコンポーネントはデータの取得と状態の保持を担当する賢い親です。表示コンポーネントは input() で受け取った値を表示し、操作を output() で通知するだけの、表示に徹する子です。
- 82状態の置き場所 — コンポーネント・サービス・URL状態には置き場所が3つあります。その画面の中だけで使う状態(モーダルの開閉など)はコンポーネントの signal に。複数の画面で共有する状態(ログインユーザー、カートの中身など)はサービスの signal、いわゆるストアに。そして検索条件やページ番号のように「URLを共有したら再現されてほしい状態」は、URLのクエリパラメータに置きます。
- 83データモデルの型設計 — interfaceは画面の設計図APIから受け取るデータには、必ず interface で型を付けます。HttpClient の get<Product[]>() のように型を渡せば、フィールド名の打ち間違いや存在しないプロパティへのアクセスを、実行前にエディタとコンパイラが指摘してくれます。型はデータの設計図であり、画面とAPIの間の契約書です。
- 84ディレクトリ構成 — feature単位で切り、sharedに寄せるフォルダは「ファイルの種類」ではなく「機能(feature)」の単位で切ります。features/products/ の中に商品機能のページ・部品・サービス・型をまとめて置けば、商品の修正で開くフォルダは1つで済みます。複数の機能から使う共通部品やパイプは shared/ に置き、依存の向きは「feature → shared」の一方向に保ちます。
開発の進め方
- 85作業を小さく分解する — 動く状態を保ちながら進む「お気に入り機能を作る」のような大きな仕事は、そのまま手を付けると必ず迷子になります。まず「①ボタンを置く ②クリックで状態を切り替える ③一覧に反映する ④保存する」のように、順番に積み上げられる小さなステップへ割ってから始めます。1ステップは「終わったら画面で動きを確かめられる」大きさが目安です。
- 86ブランチ→実装→確認→コミット — 開発の基本ループチーム開発の1機能は決まったループで進めます。まず main ブランチを最新にしてから git switch -c で feature ブランチを切ります。次にコードを書き、ng serve の画面で動作を確認し、うまく動いたら git add と git commit で記録します。この「実装→確認→コミット」を、1機能が完成するまで何周も回します。
- 87コミットメッセージ — 「なぜ」が伝わる書き方コミットメッセージは1行目に変更の要約を書きます。コツは「何をしたか」より「なぜしたか」を意識することです。何を変えたかは差分を見れば分かりますが、理由は書き残さない限り消えてしまいます。そして1コミットには1つの意味のある変更だけを入れます。無関係な修正は分けてコミットしましょう。
- 88こまめに動かして確認する — 小さく書いて小さく確かめるコードは数行書くごとに保存して画面で確かめます。ng serve が自動で再ビルドしてくれるので、確認のコストはほぼゼロです。画面に出ない値は console.log で出力したり、DevTools の Console で確かめたりします。手段は何でもよいので「書いた直後に確かめる」を徹底するのが基本です。
- 89コミットしてよいもの・いけないもの — .gitignoreの考え方リポジトリに入れるのは「自分たちが書いたもの」だけです。node_modules(ライブラリの実体)と dist(ビルドの生成物)はコマンドで再現できるのでコミットせず、.gitignore に書いて無視します。ng new が最初から設定済みなので、普段は git status に出てこないことを確認するだけで十分です。
テスト
- 90テストを書く理由 — 手動確認は改修のたびに破綻するテストとは「コードが期待どおり動くか」を別のコードで自動確認する仕組みです。開発初期は自分の手で全画面を触って確認できますが、画面が10個、機能が50個と増えると、改修のたびに全部を触り直すのは不可能になります。自動テストなら、何百項目もの確認が数秒〜数十秒で終わり、何度でも繰り返せます。
- 91純関数のテスト — 入力と出力だけの関数は最強に試しやすいテストが最も簡単なのは「入力を渡すと出力が返るだけの関数」、いわゆる純関数です。画面もサーバーも関係ないので、expect(fn(入力)).toBe(期待値) と書くだけで検証できます。関数を呼んで、返り値が期待どおりかを見る。テストの基本形はこれだけで、特別な道具は何も要りません。
- 92コンポーネントのテスト — TestBedで部品を組み立てて表示を検証コンポーネントのテストでは、TestBed という道具でテスト用の小さなアプリを組み立てます。standalone コンポーネントは imports に入れて登録し、TestBed.createComponent で生成します。返ってくる fixture が操作の起点で、fixture.detectChanges() で画面を描画し、fixture.nativeElement から実際のDOMを取り出して文言を検証します。
- 93HTTPを使うサービスのテスト — 本物のAPIは呼ばずに検証するサーバー通信するサービスをテストするとき、本物のAPIを呼ぶのは悪手です。サーバーの状態次第で結果が変わり、遅く、壊れやすいテストになります。そこで providers に provideHttpClientTesting() を登録すると、HttpClient の通信先がテスト用の偽物に差し替わり、実際の通信は一切飛ばなくなります。
- 94ng testとテストランナー — 保存のたびに回る開発リズムテストの実行は ng test コマンドが担当します。既定では watch モードで起動し、ファイルを保存するたびに関連テストが自動で再実行されます。「コードを直す→保存→数秒後に結果が出る」というリズムができると、テストは面倒な作業ではなく、手を動かしながら常に答え合わせしてくれる相棒になります。
デバッグ
- 95エラーメッセージの解剖 — NGコードとスタックトレースAngularの実行時エラーには NG0100 のような固有コードが付き、番号帯でおおよその分類が分かります。NG01xx はテンプレートや変更検知まわり、NG02xx は依存性の注入(DI)まわりが代表例です。長いメッセージを全部読む前に、まずコードを見て「どの領域の問題か」に当たりを付けるのが解剖の第一歩です。
- 96ブレークポイント — console.log を卒業するconsole.log での確認は手軽ですが、見たい変数が増えるたびにコードを書き換えて保存し直す必要があります。ブレークポイントを使えば、DevTools の Sources パネルで行番号をクリックするだけで実行をその場で止め、その瞬間のすべての変数を、コードを一切書き換えずに観察できます。
- 97Network タブ — 「画面に出ない」の半分は通信データが画面に出ないとき、原因の半分はコードではなく通信にあります。DevTools の Network タブを開いてから操作を再現すると、送られたリクエストが時系列で一覧に並びます。まず見るのはステータスコードで、404 はURLの間違い、500 はサーバー側の問題、failed はそもそも到達していないことを示します。
- 98Angular DevTools — 部品の中身を覗く拡張機能Angular DevTools はブラウザ拡張として入れる公式のデバッグ道具です。インストールすると DevTools に Angular タブが増え、Components ビューで画面を構成するコンポーネントツリーを一覧できます。ツリーから部品を選ぶと、その部品が持つ signal や input の現在値をその場で確認できます。
- 99原因の切り分け — 範囲を半分にしていく型バグ調査の基本は、どこが壊れたかの範囲をひたすら狭めることです。代表的な手法が二分探索で、怪しい範囲の半分をコメントアウトして症状が消えるか試します。消えればコメントアウトした側に、残ればもう半分に原因があります。これを繰り返せば、数十行の候補でも数回の試行で1行まで絞り込めます。
品質管理
- 100品質を道具で守る — lint・format・型・testの4点セットコードの品質を守る道具は大きく4種類あります。lintはコードの書き方に潜む問題を検査し、formatは見た目を機械的に整え、TypeScriptの型はデータの形の契約を守り、testは動作が正しいかを検証します。この4点セットを揃えると、人間が目視で探すしかなかった問題の多くを、機械が自動で見つけてくれるようになります。
- 101ESLintとangular-eslint — 書き方の問題を機械に見つけさせるESLintはTypeScript/JavaScriptの静的検査ツールで、Angularでは angular-eslint というパッケージを通じて使います。導入は ng add angular-eslint の一発で、設定ファイルの生成から angular.json への登録までが自動で行われます。以後は ng lint を実行するだけで、プロジェクト全体を検査できます。
- 102Prettier — 整形の議論を終わらせるフォーマッタPrettierはコードの見た目を機械的に統一するフォーマッタです。インデント幅・クォートの種類・改行位置などを、設定に従って自動で書き換えます。npx prettier --write . でプロジェクト全体を一括整形でき、エディタの保存時整形を有効にすれば、コードは書くそばから自動で整います。
- 103strictなTypeScript設定 — 壊れる前に知らせてくれるng new で作ったプロジェクトは、最初からTypeScriptの strict と、テンプレート用の strictTemplates が有効になっています。strictはnullやundefinedの扱いを厳密にチェックし、型の不明な変数を許しません。strictTemplatesは、その型チェックの対象をHTMLテンプレート内のバインディングにまで広げます。
- 104CIで品質を自動チェック — mainを守る門番を立てるCI(継続的インテグレーション)は、コードをpushするたびにlint・test・buildなどの検査をサーバー上で自動実行する仕組みです。GitHub Actionsなら、リポジトリに .github/workflows/ci.yml を1枚置くだけで動き始めます。検査の結果は、プルリクエストの画面に緑のチェックや赤のバツで表示されます。
パフォーマンスチューニング
- 105計測してから直す — 推測でコードをいじらないパフォーマンス改善の第一歩は、コードを触ることではなく計測です。ChromeのLighthouseを実行すればスコアと改善候補の一覧が手に入り、DevToolsのPerformanceタブで操作を録画すれば、どの処理に時間がかかっているか(長いタスク)が帯グラフで見えます。まず「どこが遅いか」を数字で特定します。
- 106初期表示を軽くする — バンドルサイズが最初の敵初期表示の速さを最も左右するのは、最初にダウンロードされるJavaScript(初期バンドル)のサイズです。武器はすでに学んだ2つ、ルートの遅延読み込み(loadComponent)と @defer です。機能自体は既習なので、このレッスンの主題は「どの画面・どの部品に効かせるか」という判断のほうです。
- 107バンドルの中身を調べる — 何が重いかを可視化するバンドルを軽くするには、まず「何が重いのか」を知る必要があります。ng build を実行すると出力ファイルの一覧とサイズが表示され、Initial total が初期バンドルの合計、Lazy chunk files が遅延読み込みで分割された部分です。この数字を読めるだけで、分割が効いているかを毎回確認できます。
- 108描画の無駄を減らす — 変わった部分だけ描き直す一覧画面がもたつくとき、よくある原因は @for の track が実質的に効いていないことです。track $index のままデータを並べ替えたり、更新のたびに全要素を新しいオブジェクトで作り直したりすると、Angularは行を再利用できず一覧全体を作り直します。この無駄はPerformanceタブの録画に長いタスクとして現れます。
- 109通信と画像の最適化 — 減らす・まとめる・待たせ方を工夫する画像はページの中で最も重い資産になりがちです。Angularでは img タグの src を ngSrc に替えて NgOptimizedImage を使うのが基本で、画面外の画像の遅延読み込みや、優先すべき画像への priority 指定が半自動で手に入ります。width と height の指定が必須なので、読み込み時に画面がガタつくずれも防げます。
プルリクエスト
- 110プルリクエストとは — 差分を取り込んでもらう依頼プルリクエスト(PR)は「このブランチの差分を main に取り込んでほしい」という依頼です。ブランチを push しただけでは main は1ミリも変わりません。GitHub 上で PR を作り、内容を見てもらい、承認されて初めてマージ(取り込み)されます。push は荷物を送っただけ、PR は受け取りの手続き、と考えると分かりやすいです。
- 111小さいPR — 差分が小さいほどレビューは速く深くなるPR の価値を大きく左右するのが差分の大きさです。数十行の PR は数分で読めて細部まで指摘できますが、数千行の PR は読むだけで疲れ、レビューは「ざっと見て承認」になりがちです。差分が小さいほどレビューは速く、深くなる。これが小さい PR が推奨される最大の理由です。
- 112PRの説明文 — 何を・なぜ・どう確認したかPR の説明文は「何を変えたか」「なぜ変えたか」「どう確認したか」の3点セットで書きます。差分を見れば「何を」はある程度分かりますが、「なぜ」はコードに残りません。背景となる課題やチケットへのリンクを書き、動作確認の手順と結果まで添えると、レビュアーは安心して読み始められます。
- 113レビューコメントへの対応 — 指摘はコードへの意見レビューで受ける指摘は、あなたの人格ではなくコードへの意見です。最初は指摘の多さに落ち込みがちですが、レビューはコードを良くする共同作業であり、指摘が多いのは真剣に読んでもらえた証拠です。対応の基本は、すべてのコメントに返信することです。修正したなら「修正しました」、しないなら理由を書いて、放置コメントを残さないようにします。
- 114マージとその後 — 取り込みと後片付けまでが仕事マージの条件は「CI がすべて緑」かつ「必要な承認が揃っている」ことです。条件が揃ったらマージボタンを押しますが、実務では squash マージがよく選ばれます。PR 内の細かいコミット(typo修正、レビュー対応など)を1つのコミットにまとめて main に積むため、main の履歴が「1 PR = 1 コミット」で読みやすくなります。