Angular基礎 #6 Router 基礎

読了 7分

前回は Service と依存性注入を扱いました。これまでは画面 1 つ の中でコンポーネントとデータをやり取りする話を見てきましたが、実際のアプリは普通複数の画面を持ちます。メニューをクリックすると画面が変わり、URL が変わり、戻るボタンも動作する必要があります。こうした画面遷移を扱うツールが Angular の Router です。

React が React Router を外部ライブラリとして別途インストールして使うのとは違い、Angular はフルパッケージのフレームワークらしく Router を 公式パッケージ (@angular/router) として一式に収めて提供します。新しいプロジェクトを ng new するときに「Would you like to add Angular routing?」と聞いてくるあいつです。

Router セットアップ #

モダン Angular (v17+) は standalone パターンを基本とします。Router も NgModule なしで provideRouter 関数 1 つでセットアップします。

src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';
import { AboutComponent } from './pages/about.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
];
src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
  ],
};
src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig);

ng new で作ったプロジェクトならこの構造はすでに整っています。私たちが手を入れる箇所は通常 app.routes.ts の 1 箇所だけです。

注記
古い資料では RouterModule.forRoot(routes)imports に入れる NgModule 方式がよく見られます。動作はしますがもはや推奨されるパターンではありません。この講座はモダン Angular の standalone + provideRouter 方式だけを扱います。

ルートを定義する #

Routes はただのオブジェクト配列です。各オブジェクトが「どんな経路のとき、どのコンポーネントを表示するか」を記述します。

src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';
import { AboutComponent } from './pages/about.component';
import { NotFoundComponent } from './pages/not-found.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'old-about', redirectTo: 'about', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent },
];

よく使うプロパティ:

  • path — URL パターン。スラッシュ (/) は付けません。空文字列 ('') はルート経路
  • component — その経路のときにレンダリングするコンポーネント
  • redirectTo — 別の経路に自動移動。pathMatch: 'full' を一緒に置く場合が多い
  • ** — ワイルドカード。上から順にマッチを試みて、どこにもマッチしないと拾います。通常 404 ページ用に最後に置きます

<router-outlet> #

ルートがマッチするとそのコンポーネントはどこに描画されるのでしょうか? まさに <router-outlet> の位置です。通常は最上位の AppComponent のテンプレートに置きます。

src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <header>
      <a routerLink="/">ホーム</a>
      <a routerLink="/about">紹介</a>
    </header>

    <main>
      <router-outlet />
    </main>
  `,
})
export class AppComponent {}

<router-outlet> がある位置に現在の URL とマッチするコンポーネントが差し込まれます。ヘッダーとフッターはそのまま残り、真ん中のコンテンツだけ差し替わるスタイルです。

RouterOutletRouterLink は standalone コンポーネントの imports 配列に直接追加する必要があるという点を忘れないでください。

routerLink #

ページ遷移リンクは <a> タグに href の代わりに routerLink ディレクティブを使います。

テンプレート断片
<a routerLink="/">ホーム</a>
<a routerLink="/about">紹介</a>
<a [routerLink]="['/users', userId]">マイプロフィール</a>
  • 文字列で絶対経路を直接渡すのがもっとも一般的です
  • [routerLink]="[...]" 配列形式は動的セグメントを差し込んで作るときに便利です (/users/123)
  • 普通の <a href="/about"> を使うとブラウザがページを新しくロードして SPA の利点を失います。必ず routerLink を使ってください

アクティブリンク表示 #

ナビゲーションバーで現在のページのリンクを強調するには routerLinkActive ディレクティブを一緒に置きます。

テンプレート断片
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
  ホーム
</a>
<a routerLink="/about" routerLinkActive="active">紹介</a>

現在の URL が該当する routerLink とマッチすると自動で active クラスが付きます。CSS で .active スタイルだけ定義しておけば終わりです。

/ のようなルート経路は [routerLinkActiveOptions]="{ exact: true }" を一緒に置く方が良いです。そうしないとすべての下位経路でもアクティブとして拾われてしまいます (/ は事実上すべての URL のプレフィックスなので)。

動的パラメータ #

商品詳細やユーザープロフィールのように URL の一部が動的に変わる経路はコロン (:) で表します。

src/app/app.routes.ts
export const routes: Routes = [
  { path: 'users/:id', component: UserDetailComponent },
];

/users/123/users/taro のような URL がすべてこのルートにマッチします。コンポーネントの中では ActivatedRoute を注入してパラメータを読みます。

src/app/pages/user-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `<h1>ユーザー ID: {{ userId }}</h1>`,
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);

  userId = this.route.snapshot.paramMap.get('id');
}

ActivatedRoute からパラメータを取り出す方法は 2 つあります。

  • snapshot.paramMap.get('id') — コンポーネントが最初に作られた時点の値を一度読みます。ページが新しくロードされるたびにコンポーネントも新しく作られるので、ほとんどの状況でこれで十分です
  • paramMap Observable で購読 — 同じコンポーネントが生きたままパラメータだけ変わる場合 (例: /users/1/users/2) には snapshot では新しい値を受け取れません。このときは Observable を購読する必要があります
Observable 購読パターン
this.route.paramMap.subscribe(params => {
  this.userId = params.get('id');
  // 新しいユーザーデータの読み込みなど
});

入門段階では snapshot だけ知っていれば十分です。同じコンポーネントの中でパラメータを頻繁に差し替える画面を作るようになったときに Observable パターンを思い出してください。

子ルートと入れ子の outlet #

複数のページが同じレイアウトを共有するとき、あるいは大きなページの中に sub-section があるときには 子ルート (children) がきれいです。

src/app/app.routes.ts
export const routes: Routes = [
  {
    path: 'users/:id',
    component: UserDetailComponent,
    children: [
      { path: '', redirectTo: 'profile', pathMatch: 'full' },
      { path: 'profile', component: UserProfileComponent },
      { path: 'posts', component: UserPostsComponent },
    ],
  },
];

親コンポーネント (UserDetailComponent) のテンプレートの中にもう 1 つの <router-outlet> を置きます。

src/app/pages/user-detail.component.ts
@Component({
  selector: 'app-user-detail',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <h1>ユーザー #{{ userId }}</h1>
    <nav>
      <a routerLink="profile" routerLinkActive="active">プロフィール</a>
      <a routerLink="posts" routerLinkActive="active">記事一覧</a>
    </nav>
    <router-outlet />
  `,
})

/users/123/profile に入ると親のヘッダーとタブはそのまま残し、真ん中の outlet に UserProfileComponent が入ります。routerLink="profile" のようにスラッシュなしで書くと現在のルート基準で 相対パス として動作するので便利です。

Lazy loading #

デフォルトではすべてのルートのコンポーネントが初期バンドルに含まれます。アプリが大きくなると初回読み込みが遅くなります。あまり使わないページは lazy loading で切り分けると良いです。

src/app/app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadComponent: () => import('./pages/admin.component').then(m => m.AdminComponent),
  },
];

component の代わりに loadComponent に動的 import() を渡すと、ユーザーがその経路に移動するときになって初めて該当のモジュールがネットワークで取得されます。ビルド時点で Angular CLI が自動的に別のチャンクに分割してくれます — 私たちはルート定義だけ少し変えれば構いません。

ヒント
Lazy loading は最初から強迫的に適用する必要はありません。ひとまず component: で普通に書いて、ビルド結果の初期バンドルサイズが気になり始めたら管理者ページ・設定ページのような「たまにしか使わない大きな画面」を中心に移していくのが実用的です。

まとめ #

今回の記事では Angular Router の核心を見てきました。整理すると:

  • provideRouter(routes) で standalone セットアップ
  • Routes 配列に path + component (+ redirectTo** ワイルドカード)
  • <router-outlet> の位置にマッチしたコンポーネントが描画される
  • routerLink で SPA 遷移、routerLinkActive でアクティブ表示
  • :id 動的パラメータは inject(ActivatedRoute) + snapshot.paramMap で読む
  • children で入れ子ルート、親テンプレートにもう 1 つの outlet
  • loadComponent で lazy loading

これくらいあれば小さな複数ページのアプリはほぼ全部作れます。次回の「Angular基礎 #7 HttpClient 基礎」ではバックエンド API と通信する方法を扱います。これからが本当のアプリらしいアプリを作る段階です。

X