Angularルーティング
AngularのルーティングはNext.jsのファイルベースルーティングとは異なり、設定ファイルベースのアプローチを取ります。
1. ルーティングの基本設定
Angularでは、URLとコンポーネントの対応を ルート定義(Routes 配列) として TypeScript で明示的に記述します。ディレクトリ構造がそのままURLになるわけではありません。
app.routes.ts でのルート定義
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UserListComponent } from './users/user-list.component';
import { NotFoundComponent } from './not-found.component';
export const routes: Routes = [
{ path: '', component: HomeComponent, title: 'ホーム' },
{ path: 'users', component: UserListComponent, title: 'ユーザー一覧' },
{ path: 'members', redirectTo: 'users', pathMatch: 'full' },
{ path: '**', component: NotFoundComponent }, // 上記のどれにも一致しない場合
];
ポイントは次のとおりです。
- ルートは 上から順に評価され、最初に一致したものが採用 されます。ワイルドカード
**は必ず最後に置きます。 pathMatch: 'full'は、パス全体が一致した場合のみリダイレクトする指定です。空パスのリダイレクトでは必須です。titleを指定すると、遷移時にdocument.titleが自動で更新されます。
provideRouter() の設定
スタンドアロン構成(Angular 15以降)では、app.config.ts で provideRouter() を登録します。RouterModule.forRoot() を使う NgModule 方式は現在では推奨されません。
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
} from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // ルートパラメータをinput()にバインド(後述)
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
),
],
};
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
withXxx() 形式の関数でルーターの機能を必要なものだけ有効化できます(tree-shaking に有利です)。
RouterOutlet コンポーネント
<router-outlet> は、一致したルートのコンポーネントが描画される場所を示すプレースホルダーです。ルートコンポーネントにはスタンドアロンコンポーネントとして RouterOutlet をインポートします。
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink],
template: `
<header>
<a routerLink="/">ホーム</a>
<a routerLink="/users">ユーザー</a>
</header>
<main>
<router-outlet />
</main>
`,
})
export class AppComponent {}
Next.js との対比
| 観点 | Angular | Next.js (App Router) |
|---|---|---|
| ルート定義 | Routes 配列(コードで宣言) | app/ ディレクトリ構造(規約) |
| ページ本体 | component に指定したクラス | page.tsx |
| 描画位置 | <router-outlet /> | children / layout.tsx |
| 404 | path: '**' | not-found.tsx |
| リダイレクト | redirectTo | redirect() / next.config.js |
| ページタイトル | title プロパティ | metadata / generateMetadata |
Angular は設定が明示的で、URL とファイル配置を独立して設計できます。一方、Next.js はファイルを置くだけでルートが増える手軽さがあります。
2. ナビゲーション
routerLink ディレクティブ
<a href> の代わりに routerLink を使うと、ページ全体をリロードせずにSPA遷移します。
<!-- 静的パス -->
<a routerLink="/users">ユーザー一覧</a>
<!-- パスパラメータを配列で組み立てる -->
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>
<!-- クエリパラメータ付き -->
<a routerLink="/users" [queryParams]="{ page: 2, sort: 'name' }">2ページ目</a>
routerLinkActive
現在のURLに一致するリンクへ CSS クラスを付与します。ナビゲーションのハイライトに使います。
<nav>
<a routerLink="/" routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">ホーム</a>
<a routerLink="/users" routerLinkActive="active">ユーザー</a>
</nav>
/ のようなルートリンクは、exact: true を付けないと全URLで active になります。使用するには RouterLinkActive をコンポーネントの imports に追加します。
Router サービスによるプログラマティックナビゲーション
フォーム送信後など、コードから遷移する場合は Router を inject() で取得します。
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-user-form',
template: `<button (click)="save()">保存</button>`,
})
export class UserFormComponent {
private readonly router = inject(Router);
async save(): Promise<void> {
// ...保存処理...
await this.router.navigate(['/users', 42], {
queryParams: { saved: true },
});
// 文字列URLで遷移する場合
// await this.router.navigateByUrl('/users/42?saved=true');
}
}
navigate() は Promise<boolean> を返し、遷移が成功すると true になります(ガードに拒否された場合は false)。
Next.js との比較
| 目的 | Angular | Next.js |
|---|---|---|
| リンク | routerLink | <Link href> |
| アクティブ判定 | routerLinkActive | usePathname() で自前判定 |
| コードからの遷移 | Router.navigate() | useRouter().push() |
| 置換遷移 | navigate([...], { replaceUrl: true }) | router.replace() |
Next.js では <Link> が自動プリフェッチを行いますが、Angular の routerLink は既定ではプリフェッチしません(プリロード戦略は「遅延ロード」の節を参照)。
3. ルートパラメータとクエリパラメータ
パスパラメータ (:id)
export const routes: Routes = [
{ path: 'users/:id', component: UserDetailComponent },
];
/users/42 にアクセスすると、id は文字列 '42' として取得できます。
ActivatedRoute サービス
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({
selector: 'app-user-detail',
template: `<h2>User ID: {{ id() }}</h2>`,
})
export class UserDetailComponent {
private readonly route = inject(ActivatedRoute);
// paramMap は Observable。同じコンポーネントでパラメータだけ変わる場合にも追従する
readonly id = toSignal(
this.route.paramMap.pipe(map((p) => p.get('id'))),
{ initialValue: null },
);
}
snapshot.paramMap.get('id') で一度だけ読む方法もありますが、/users/1 から /users/2 へ遷移するとき、Angular は 同じコンポーネントインスタンスを再利用する ため、snapshot では値が更新されません。パラメータの変化に追従したい場合は Observable(paramMap)を購読するか、次の input() バインディングを使います。
withComponentInputBinding による input() バインディング(推奨)
provideRouter に withComponentInputBinding() を渡すと、パスパラメータ・クエリパラメータ・ルートの data が コンポーネントの input() に直接バインド されます。ActivatedRoute を使わずに済み、最も簡潔です。
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-detail',
template: `<h2>User {{ userId() }} / page {{ page() }}</h2>`,
})
export class UserDetailComponent {
id = input.required<string>(); // /users/:id
page = input<string>('1'); // ?page=2
userId = computed(() => Number(this.id()));
}
クエリパラメータの読み取り
// ActivatedRoute を使う場合
const route = inject(ActivatedRoute);
const page$ = route.queryParamMap.pipe(map((q) => q.get('page') ?? '1'));
// 現在のクエリを保ったまま一部だけ更新する
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: 3 },
queryParamsHandling: 'merge',
});
Next.js との対比
| 目的 | Angular | Next.js |
|---|---|---|
| パスパラメータ | input() / ActivatedRoute.paramMap | useParams() / params props |
| クエリ取得 | input() / queryParamMap | useSearchParams() / searchParams props |
| 変化への追従 | Observable / Signal | 再レンダリング |
| 型 | 基本は string(変換は自前) | 基本は string |
4. ネストされたルートとレイアウト
子ルートの定義
children を使うと、親コンポーネントのテンプレート内の <router-outlet> に子ルートが描画されます。共通レイアウトを親に置く用途に最適です。
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent, // サイドバーなど共通UI
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users', component: AdminUsersComponent },
],
},
];
@Component({
selector: 'app-admin-layout',
imports: [RouterOutlet, RouterLink],
template: `
<aside>
<a routerLink="dashboard">ダッシュボード</a>
<a routerLink="users">ユーザー管理</a>
</aside>
<section>
<router-outlet /> <!-- 子ルートがここに描画される -->
</section>
`,
})
export class AdminLayoutComponent {}
component を持たない コンポーネントレスルート も定義できます。URL のプレフィックスや、ガード・リゾルバをまとめて適用したいときに便利です。
{
path: 'account',
canActivate: [authGuard], // 配下すべてに適用
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'settings', component: SettingsComponent },
],
}
名前付き RouterOutlet
1つの画面に複数の outlet を置く場合、outlet 名を指定します(補助ルート)。
{ path: 'chat', component: ChatComponent, outlet: 'sidebar' }
<router-outlet /> <!-- プライマリ -->
<router-outlet name="sidebar" /> <!-- 名前付き -->
this.router.navigate([{ outlets: { sidebar: ['chat'] } }]);
URL は /home(sidebar:chat) のようになります。使用頻度は高くなく、モーダルやサイドパネルなど限定的な用途向けです。
Next.js の layout.tsx との比較
| 観点 | Angular | Next.js |
|---|---|---|
| 共通レイアウト | 親ルートのコンポーネント + <router-outlet /> | layout.tsx + children |
| 入れ子の表現 | children 配列 | フォルダのネスト |
| ルートグループ | コンポーネントレスルート | (group) フォルダ |
| 並列ルート | 名前付き outlet | Parallel Routes (@slot) |
5. ルートガード
ガードは、ルートへの遷移を許可・拒否・リダイレクトするための仕組みです。
| ガード | 用途 |
|---|---|
canActivate | ルートに入ってよいか |
canActivateChild | 子ルートに入ってよいか |
canDeactivate | ルートから離れてよいか(未保存警告など) |
canMatch | そのルート定義自体を一致対象にするか(ロール別の出し分けなど) |
関数型ガード(新しい書き方)
Angular 15.2 以降、ガードは 単なる関数 として書くのが推奨です。クラスベースのガード(CanActivate インターフェース)は非推奨となっており、inject() で必要なサービスを取得します。
戻り値は boolean、UrlTree(リダイレクト)、またはそれらの Promise / Observable です。
認証チェック
// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
// ログイン後に元のURLへ戻せるよう returnUrl を渡す
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
権限チェック(ガードファクトリ)
引数を取りたい場合は、ガードを返す関数を作ります。
// role.guard.ts
export const roleGuard = (requiredRole: string): CanActivateFn => {
return () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.hasRole(requiredRole)
? true
: router.createUrlTree(['/forbidden']);
};
};
export const routes: Routes = [
{
path: 'admin',
canActivate: [authGuard, roleGuard('admin')],
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];
canDeactivate(未保存変更の警告)
import { CanDeactivateFn } from '@angular/router';
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) =>
component.hasUnsavedChanges()
? confirm('保存されていない変更があります。破棄して移動しますか?')
: true;
{ path: 'edit/:id', component: EditComponent, canDeactivate: [unsavedChangesGuard] }
Next.js の middleware.ts との概念比較
| 観点 | Angular ガード | Next.js middleware |
|---|---|---|
| 実行場所 | ブラウザ(クライアント)。SSR時はサーバーでも実行 | サーバー / Edge(リクエスト前) |
| 適用単位 | ルート定義ごとに指定 | matcher でパスを指定 |
| 結果 | true / false / UrlTree | NextResponse.redirect() など |
| 用途 | UXのための遷移制御 | リクエスト単位の認証・書き換え |
クライアント側のガードは UX 上の制御であり、セキュリティ境界ではありません。ブラウザ上のコードは改変できるため、認可の最終判断は必ずバックエンド API で行います。
6. 遅延ロード(Lazy Loading)
初期バンドルを小さくするため、必要になった時点でコードを読み込みます。
loadComponent(単一コンポーネント)
export const routes: Routes = [
{
path: 'reports',
loadComponent: () =>
import('./reports/reports.component').then((m) => m.ReportsComponent),
},
];
コンポーネントが export default されている場合は、さらに簡潔に書けます。
{ path: 'reports', loadComponent: () => import('./reports/reports.component') }
loadChildren(ルート群)
機能単位で子ルートの配列ごと遅延ロードします。
// admin/admin.routes.ts
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminHomeComponent },
{ path: 'users', component: AdminUsersComponent },
];
// app.routes.ts
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
}
動的 import() を境界として、ビルド時にチャンクが自動分割されます。
プリロード戦略
既定では遷移時に初めてチャンクを取得するため、初回遷移に待ちが発生します。withPreloading() で、アイドル時に先読みできます。
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
provideRouter(routes, withPreloading(PreloadAllModules));
大規模アプリでは、重要なルートだけを先読みするカスタム戦略(PreloadingStrategy の実装)も検討します。
Next.js との比較
| 観点 | Angular | Next.js |
|---|---|---|
| 分割単位 | 開発者が loadComponent / loadChildren で明示 | ページ(ルート)ごとに自動 |
| 先読み | 戦略を設定(既定はなし) | <Link> が viewport 内で自動プリフェッチ |
| コンポーネント単位 | @defer ブロック | next/dynamic / React.lazy |
Angular 17以降は、ルート単位に加えて、テンプレート内の @defer ブロックでコンポーネント単位の遅延ロードも可能です。
7. リゾルバ(Resolvers)
リゾルバは、ルートのアクティブ化前にデータを取得 しておく仕組みです。コンポーネントの表示時にはデータが揃っているため、ローディング表示のちらつきを避けられます。
データのプリフェッチ
// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService, User } from './user.service';
export const userResolver: ResolveFn<User> = (route) => {
const id = Number(route.paramMap.get('id'));
return inject(UserService).getUserById(id); // Observable / Promise / 値
};
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: userResolver },
}
withComponentInputBinding() が有効であれば、解決済みデータは resolve のキー名と同じ input() で受け取れます。
@Component({
selector: 'app-user-detail',
template: `<h2>{{ user().name }}</h2>`,
})
export class UserDetailComponent {
user = input.required<User>();
}
注意点として、リゾルバが完了するまで 遷移が保留 されます。重い処理だとユーザーには「何も起きない」ように見えるため、短時間で終わるデータに限定するか、コンポーネント内で取得してスケルトン表示にする方が適切な場合もあります。リゾルバが失敗(エラー)した場合は遷移がキャンセルされるため、catchError で扱いを決めます。
Next.js との対比
| 観点 | Angular リゾルバ | Next.js |
|---|---|---|
| 実行タイミング | 遷移前(クライアント主体) | サーバーでレンダリング時 |
| データの渡し方 | resolve → input() / ActivatedRoute.data | Server Component が直接 await |
| 旧来の方式 | - | getServerSideProps(Pages Router) |
| ローディング | 遷移が保留される | loading.tsx / Suspense でストリーミング |
Next.js の Server Components ではコンポーネント自体が非同期にデータを取得するため、リゾルバに相当する専用概念がありません。Angular でも、リゾルバを使わずコンポーネント内でサービスを呼ぶ構成は一般的です。使い分けの目安は「データが無いと画面が成立しない・エラー時は遷移させたくない」場合にリゾルバ、それ以外はコンポーネント内取得です。