iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

Angular 22 Signal 進化論系列 第 16 篇

Day 16:即將登場的 Route Resources,Angular Router 為什麼需要新的資料載入方式?

  • 分享至 

  • xImage
  •  

過去在 Angular Router 中,如果希望進入 Component 前先完成資料載入,可以透過 Resolver,讓資料取得流程在 Component 初始化之前執行。

而即將登場的 Route Resources,則把 Angular 的 Resource 機制整合進 Router。這篇會先從傳統 Resolver 開始,看看 Route Resources 在路由資料載入上帶來哪些不同的處理方式。

目前 Route Resources 仍處於 Developer Preview,API 與行為後續仍可能調整。使用前需要先透過 withRouterResources() 啟用;如果希望 Router 將 Resource 綁定到 Component Input,也可以一起加入 withComponentInputBinding():

import { ApplicationConfig } from '@angular/core';
import {
  provideRouter,
  withComponentInputBinding,
  withRouterResources,
} from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(),
      withRouterResources()
    ),
  ],
};

路由結構導致的瀑布流請求

當路由存在巢狀結構,而且 Parent Route 與 Child Route 都需要透過 Resolver 取得資料時,Resolver 會依照路由層級執行。

假設路由結構如下:

/users/:id
  └── posts

進入 /users/1/posts 時:

  • Parent Route 取得 User
  • Child Route 取得 Posts

使用 Resolver 可以這樣設定:

export const routes: Routes = [
  {
    path: 'users/:id',
    resolve: {
      user: userResolver,
    },
    children: [
      {
        path: 'posts',
        component: UserPostsComponent,
        resolve: {
          posts: postsResolver,
        },
      },
    ],
  },
];

Router 會先執行 Parent Route 的 userResolver,等資料取得完成後,才繼續執行 Child Route 的 postsResolver。

假設兩個 API 分別需要:

User API   300ms
Posts API  400ms

由於兩個 Resolver 依序執行,整體等待時間大約會是:

300ms + 400ms = 700ms

如果 Posts 的查詢必須依賴 User 的結果,依序執行本身沒有問題。這類情境也可以回頭檢視 API 設計,確認是否真的需要拆成兩段請求,或能否由後端一次提供需要的資料。

如果兩個請求彼此獨立,卻只是因為 Parent / Child 的路由結構而被安排成前後執行,就會形成不必要的瀑布流。

Angular Route 瀑布請求問題 - StackBlitz

使用 Route Resources

同樣的情境改用 Route Resources,可以把 Parent 與 Child 的資料載入分別宣告成各自的 Resource:

export const routes: Routes = [
  {
    path: 'users/:id',
    resources: (ctx) => ({
      user: resource({
        params: () => ctx.params()['id'],
        loader: ({ params: id, abortSignal }) =>
          fetchUser(id, abortSignal),
      }),
    }),
    children: [
      {
        path: 'posts',
        component: UserPostsComponent,
        resources: (ctx) => ({
          posts: resource({
            params: () => ctx.params()['id'],
            loader: ({ params: id, abortSignal }) =>
              fetchPosts(id, abortSignal),
          }),
        }),
      },
    ],
  },
];

ResourceContext 提供 params、queryParams、fragment 與 data 等 Route 資訊,而且這些資料都是 Signal。

因此可以直接把 Route Param 當成 Resource 的 params:

params: () => ctx.params()['id']

以前面的路由為例,Child Route 的 posts Resource 也能取得 Parent Route 定義的 id:

/users/:id
  └── posts
resources: (ctx) => ({
  posts: resource({
    params: () => ctx.params()['id'],
    loader: ({ params: id }) =>
      fetchPosts(id),
  }),
}),

目前 Router 預設的 paramsInheritanceStrategy 是 always,因此 Child Route 會繼承 Parent Route 的 Params,ctx.params()['id'] 可以直接取得 id。

在 Component 中透過 ActivatedRoute 讀取 Route Param 也可以做到:

const route = inject(ActivatedRoute);

route.paramMap.subscribe((params) => {
  console.log(params.get('id'));
});

差別在於 Route Resources 可以把 Route Param 與資料載入一起宣告在 Route 設定中,不需要讓 Component 自己訂閱參數後再發出請求。

當網址從:

/users/1/posts

切換成:

/users/2/posts

ctx.params()['id'] 會從 1 變成 2,兩個 Resource 的 params 也會跟著改變,重新執行對應的 loader:

fetchUser('2');
fetchPosts('2');

傳統 Resolver 在 Route Param 改變時同樣可以重新執行,因此差異不在於能不能重新取得資料,而是 Route Resources 可以直接把 Route Params 納入 Resource 的響應式資料載入流程。

另一個差異是 Parent 與 Child 的載入順序。對目前符合的 Route,Angular 會讓各層的 Resource 同時開始載入,不需要等待 Parent Resource 完成後才執行下一層。

因此相同的兩個請求,等待時間會接近其中較慢的一筆:

max(300ms, 400ms) = 400ms

Resolver 與 Route Resources 的載入順序比較

這裡改善的不是 API 本身的速度,而是 Router 不再因為 Parent / Child 的層級,把原本可以平行執行的請求拆成前後兩段,對彼此獨立的資料來說,就能減少因路由結構產生的額外等待時間。

Angular Route Resources 解決瀑布流請求問題 - StackBlitz

Blocking 與 nonBlocking

Route Resources 預設是 Blocking,Router 會等 Resource 載入完成後,才啟用對應的 Route 與 Component。

對 Blocking Resource 來說,Router 綁定到 Component Input 的是 Resource 完成後的 Value,因此進入 Component 時資料已經準備完成:

@Component({
  selector: 'app-user-posts',
  template: `
    @for (post of posts(); track post.id) {
      <p>{{ post.title }}</p>
    }
  `,
})
export class UserPostsComponent {
  posts = input.required<Post[]>();
}

這種方式和 Resolver 比較接近,Component 不需要另外處理初始 Loading 狀態。

如果資料不是進入頁面的必要條件,也可以透過 nonBlocking() 讓 Route 先啟用,再讓 Resource 在背景繼續載入:

import {
  nonBlocking,
  Routes,
} from '@angular/router';

export const routes: Routes = [
  {
    path: 'users/:id',
    children: [
      {
        path: 'posts',
        component: UserPostsComponent,
        resources: (ctx) => ({
          posts: nonBlocking(
            resource({
              params: () => ctx.params()['id'],
              loader: ({ params: id }) =>
                fetchPosts(id),
            })
          ),
        }),
      },
    ],
  },
];

使用 nonBlocking() 後,Router 綁定到 Component Input 的不再只是 Value,而是完整的 Resource,因此可以直接取得 isLoading()、error()、hasValue() 等狀態:

@Component({
  selector: 'app-user-posts',
  template: `
    @if (posts().isLoading()) {
      <p>Loading...</p>
    } @else if (posts().error()) {
      <p>載入失敗</p>
    } @else if (posts().hasValue()) {
      @for (post of posts().value(); track post.id) {
        <p>{{ post.title }}</p>
      }
    }
  `,
})
export class UserPostsComponent {
  posts = input.required<Resource<Post[]>>();
}

如果只是希望 Component 顯示後再取得資料,直接在 Component 中建立 Resource 也可以做到。

nonBlocking() 的差別在於 Resource 仍然宣告在 Route 設定中,可以繼續使用 ResourceContext 提供的 Route 資訊:

params: () => ctx.params()['id']

nonBlocking() 只改變 Router 是否要等待 Resource 完成才啟用 Route,Resource 的參數與資料取得邏輯仍然可以留在 Route 設定中,Component 只需要處理 Loading、Error 與資料顯示。

Blocking 與 nonBlocking 的差異

不重新 Navigation 也能重新載入

Resolver 和 Navigation 綁在一起。如果希望在同一個 URL 下重新執行 Resolver,通常需要再次觸發 Router Navigation。

Route Resources 則可以直接重新載入其中一個 Resource,不需要重新比對 Route 或重新執行 Guards。

對 Blocking Resource 來說,Component Input 收到的是 Resource 的 Value。如果需要操作 Resource 本身,可以從 ActivatedRoute.resources 取得:

import {
  Component,
  inject,
  input,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user-posts',
  template: `
    <button
      type="button"
      (click)="refresh()"
    >
      重新整理
    </button>

    @for (post of posts(); track post.id) {
      <p>{{ post.title }}</p>
    }
  `,
})
export class UserPostsComponent {
  posts = input.required<Post[]>();

  private readonly route = inject(ActivatedRoute);

  private readonly postsResource =
    this.route.resources?.['posts'];

  refresh() {
    this.postsResource?.reload();
  }
}

呼叫:

this.postsResource?.reload();

只會重新執行這一份 Resource 的載入,不需要改變目前 URL,也不會重新執行 Guards 或重新比對 Route。

如果畫面只是需要重新取得最新 Posts,就不必為了重新執行資料請求,再跑一次完整的 Navigation。

不重新 Navigation 也能重新載入 Resource

不限定使用 resource()

Route Resources 不限定只能使用 resource()。

resources 可以回傳 Angular 的 Resource,因此也能依照資料來源選擇不同的 Resource API。

例如使用 httpResource():

resources: (ctx) => ({
  user: httpResource<User>(
    () => `/api/users/${ctx.params()['id']}`
  ),
}),

或是使用 rxResource() 串接 Observable:

resources: (ctx) => ({
  user: rxResource({
    params: () => ctx.params()['id'],
    stream: ({ params: id }) =>
      userService.getUser(id),
  }),
}),

Route Resources 並不是另一套資料請求方式,而是讓 Angular Resource 可以直接放進 Router 的資料載入流程。

原本 Resource 的響應式參數、重新載入能力,以及 nonBlocking 時可以使用的載入狀態,都能繼續沿用,再搭配 Router 提供的 Route 資訊與 Navigation 生命週期。

Route Resources 整合不同的 Resource API

本日結語

Resolver 的做法是先完成資料載入,再進入對應的 Route。當 Parent 與 Child 都有 Resolver 時,也會依照路由層級執行,因此彼此獨立的請求仍可能形成瀑布流。

Route Resources 把 Resource 納入 Router 後,各層 Route 的 Resource 可以平行載入,Route Params 也能直接成為 Resource 的響應式依賴。資料不需要阻塞頁面時,可以改成 nonBlocking;需要重新取得資料時,也能只重新載入特定 Resource,不必再觸發一次 Navigation。

資料是否適合放進 Route Resources,可以看它和 Route 的關係。明確依賴 Route Params、會隨網址改變,或本身就是這條 Route 所需要的資料,可以由 Router 管理;如果只是單一 Component 內部使用的資料,直接留在 Component 中會更單純。

資料來源


上一篇
Day 15:搞懂 Injection Context,從 inject()、runInInjectionContext() 到 injectAsync()
系列文
Angular 22 Signal 進化論 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言