iT邦幫忙

2025 iThome 鐵人賽

DAY 17
1
Modern Web

Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站系列 第 17

Day 17 Router 進階 – 守衛 (Guards) 與 Resolver(資料先載好再進頁)

  • 分享至 

  • xImage
  •  

今日目標

  • 了解 GuardscanActivate / canActivateChild / canMatch(或 canLoad) 的差別與使用時機
  • 建立 AuthGuard,保護受限頁面(例如 /projects/:slug/edit
  • 建立 ProjectResolver,在進入 /projects/:slug 前先把單筆專案載入好
  • 失敗情境:找不到專案 時導回列表或顯示 404

基礎概念(白話版)

  • Guard(守衛):決定「能不能進去」某個路由。像門口的保全。
    • canActivate:要進入這個路由前檢查。
    • canActivateChild:要進入這個路由的子路由前檢查。
    • canMatch(v15+ 取代多數 canLoad 場景):是否允許配對到某 lazy route。常用於 Lazy Module 的權限判斷。
  • Resolver:在進入路由前把資料載好並注入頁面(route.data)。使用者一進頁就看到完整內容,不用再「跳著載」。

實作 A:AuthGuard(保護 /projects/:slug/edit

1) 假登入服務(超小型)

src/app/services/auth.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AuthService {
  // 假登入狀態;實務上會接 token / user profile
  private _isLoggedIn = new BehaviorSubject<boolean>(false);
  isLoggedIn$ = this._isLoggedIn.asObservable();

  get isLoggedIn() { return this._isLoggedIn.value; }
  login()  { this._isLoggedIn.next(true); }
  logout() { this._isLoggedIn.next(false); }
}

2) 建 Guard

src/app/guards/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}

  canActivate(): boolean | UrlTree {
    // 已登入 → 放行;未登入 → 導回列表(也可導到 /login)
    return this.auth.isLoggedIn ? true : this.router.parseUrl('/projects');
  }
}

如果你的 Projects 是 Lazy Module,想在整個 lazy 路由層級就擋掉,改用 canMatch(或舊版 canLoad)也可以。

3) 在路由上套用

(以下範例在 projects-routing.module.ts,延續 Day 16 的 slug 版路由結構)

import { Routes } from '@angular/router';
import { ProjectsComponent } from './projects.component';
import { ProjectDetailComponent } from './project-detail.component';
import { ProjectEditComponent } from './project-edit.component'; // 你可以先做個空元件
import { AuthGuard } from '../guards/auth.guard';
import { ProjectResolver } from '../resolvers/project.resolver';

export const routes: Routes = [
  { path: '', component: ProjectsComponent },
  {
    path: ':slug',
    component: ProjectDetailComponent,
    resolve: { project: ProjectResolver } // 等等 B 段會實作
  },
  {
    path: ':slug/edit',
    component: ProjectEditComponent,
    canActivate: [AuthGuard],            // 受保護頁面
    resolve: { project: ProjectResolver }
  }
];


實作 B:ProjectResolver(先載好單筆專案)

1) 建 Resolver

src/app/resolvers/project.resolver.ts

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
import { Observable, of } from 'rxjs';
import { switchMap, map, catchError } from 'rxjs/operators';
import { ProjectsDataService } from '../services/projects-data.service';
import { Project } from '../models/project.model';

@Injectable({ providedIn: 'root' })
export class ProjectResolver implements Resolve<Project | UrlTree> {
  constructor(private projects: ProjectsDataService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot): Observable<Project | UrlTree> {
    const slug = route.paramMap.get('slug') ?? '';
    if (!slug) return of(this.router.parseUrl('/projects'));

    return this.projects.getBySlug$(slug).pipe(
      map(p => p ? p : this.router.parseUrl('/projects')), // 找不到 → 導回列表(或導 404)
      catchError(() => of(this.router.parseUrl('/projects')))
    );
  }
}

你也可以改成導到 /not-found,或回傳 null 然後在 component 判斷顯示 404 區塊。這裡示範「直接在 Resolver 做導向」。

2) 在詳情頁直接讀 route.data

src/app/components/project-detail/project-detail.component.ts

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Project } from '../../models/project.model';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-project-detail',
  templateUrl: './project-detail.component.html'
})
export class ProjectDetailComponent {
  project$ = this.route.data.pipe(map(data => data['project'] as Project));

  constructor(private route: ActivatedRoute) {}
}

project-detail.component.html

<div class="container section" *ngIf="project$ | async as project">
  <h2>{{ project.title }}</h2>
  <p class="muted">{{ project.tech }}</p>

  <div class="gallery" *ngIf="project.images?.length">
    <img *ngFor="let src of project.images" [src]="src" alt="專案截圖" width="400" />
  </div>

  <p>{{ project.desc }}</p>

  <div class="actions">
    <a class="btn" [href]="project.demo" target="_blank">Live Demo</a>
    <a class="btn btn-outline" [href]="project.repo" target="_blank">GitHub</a>
  </div>

  <br />
  <a routerLink="/projects" class="btn btn-outline">返回列表</a>
</div>

✅ 重點:component 不再自己打服務、處理 loading/error;資料在路由階段就準備好了。


(可選)Not Found 頁面

如果你想用 404:

ng g c components/not-found

app-routing.module.ts

{ path: 'not-found', component: NotFoundComponent },
{ path: '**', redirectTo: 'not-found' }

把 Resolver 的導向改成 this.router.parseUrl('/not-found') 即可。


(可選)守整個 Lazy Module:canMatch

假設 projects 是 Lazy 模組,要整包保護(例如只有登入者才能看):

app-routing.module.ts

{
  path: 'projects',
  canMatch: [AuthGuard], // 或 canLoad(舊)
  loadChildren: () => import('./projects/projects.module').then(m => m.ProjectsModule)
}

Guard 內回傳 boolean | UrlTree;未登入時導回 //login


成果

  • /projects/:slug 在進頁前先由 Resolver 取得單筆資料,畫面更順暢。
  • /projects/:slug/editAuthGuard 保護,未登入者被導走。
  • 你掌握了 路由層處理資料/權限 的正確位置,Component 更輕盈、邏輯更清楚。

小心踩雷

  1. Resolver 沒回傳可觀察值或 UrlTree
  • ❌ 直接 return this.projects.getBySlug$(...) 外還做 subscribe()
  • ✅ Resolver 回傳 Observable<Project | UrlTree>不要自己 subscribe
  1. Guard 忘記回傳布林/UrlTree
  • ❌ 只做副作用 router.navigate(...) 然後回 void
  • ✅ 回傳 true/falseUrlTree(推薦 UrlTree,更語義化)
  1. component 舊邏輯沒整理
  • 既然用 Resolver,component 就不需要再打一次 API;否則浪費資源又可能閃爍
  1. Lazy Module 的路徑誤用
  • canActivate 只在已載入後生效;要在「載入前就擋掉」,請用 canMatch(或舊 canLoad

下一步(Day 18 預告)

我們來做 狀態管理(使用 Service + BehaviorSubject 打造輕量 store)

  • 把「技能分類」、「關鍵字搜尋」、甚至「主題切換」這類 UI 狀態集中在一個 Store Service
  • 多個元件共用狀態、跨頁維持(可搭配 localStorage
  • 用 RxJS 把資料流整理得更乾淨(selectors、updaters 的實戰寫法)

上一篇
Day 16 Angular HttpClient 與 RxJS – 串接 API 資料
下一篇
Day 18 Angular 狀態管理 – 用 Service + BehaviorSubject 打造小型 Store
系列文
Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言