由於 Angular 是 單頁應用程式 (SPA) ,是透過 JavaScript 動態更新頁面內容,而不是重新載入整個頁面。
可以把 這個標籤想成一個框,這個框放進去誰,就顯示誰。
程式上,當URL改變的時候,Router 會:
<router-outlet>
中實際運作流程:
使用者點擊連結 → URL 改變 → Router 偵測到變化 →
查找路由表 → 載入對應 Component → 替換框內內容
當應用服務的對象單純,或是這是一個展示概念用的專案,通常單層的設計可以應付,依照需求跟設計,目前我使用過的有兩種。
另外為保持使用者體驗良好,都會加上Loading的元件,讓他們知道系統正在處理中,也可以減少工程師聽到使用者在問:為什麼都沒在動,同時,避免使用者重複操作相同行為,因為loading元件會全畫面覆蓋,防止重複操作。
固定佈局設計
需要保持一致性跟導航的時候
🏠 index.html
└── 🛋️ app.component.html (有固定佈局)
├── ⏳ LoadingComponent (開燈中)
├── 🧭 導航列 (固定家具)
├── 🖼️ <router-outlet> (相框)
│ └── 📷 Component (只是內容,沒有完整佈局)
└── 📱 頁尾 (固定家具)
靈活設計
需要靈活設計,比如說登入、註冊、活動頁,整個版型會落差很大
🏠 index.html (劇院建築)
└── 🛋️ app.component.html (舞台)
├── ⏳ LoadingComponent (布幕/準備中的告示)
└── 🖼️ <router-outlet> (表演區域)
└── 📷 Component (不同的戲劇/表演)
在比較大型的後台,通常是使用巢狀的設計,在 LayoutModule 裡面還會有多的 Lazy Loading,因為你需要服務多個單位,可能有產品A團隊、產品B團隊、產品A、B的管理者團隊,這時候就會切子路由 + Lazy Loading 來處理
🏠 index.html
└── 🎬 app.component.html (極簡)
├── ⏳ LoadingComponent
└── 🎭 <router-outlet> (主路由)
├── 🔐 LoginComponent (完整頁面)
└── 📱 LayoutModule
├── 🧭 導航列
├── 🖼️ <router-outlet> (子路由)
│ ├── 📊 DashboardModule (lazy loading)
│ └── 👥 UsersModule (lazy loading)
└── 📱 頁尾
🔧 設計彈性提醒
不過因為Router-outlet的特性,都可以再根據需求去重新組裝單層跟我提供的嵌套的使用方式,可以把我提供的單層視為一個單元去組裝就可以了。
由於Loading跟Guard不是本章的重點,所以這裡不會放上程式碼,後續的章節才會放上
main.ts
)首先要先註冊路由
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),// 這裡註冊了 app.routes.ts 的路由配置
// 其他 providers...
]
}).catch(err => console.error(err));
app.component.ts
)這裡單純只有使用 loading 跟 router-outlet,在import引入相關元件後就可以了
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoadingComponent } from './core/loading/components/loading.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, LoadingComponent],
template:`<app-loading></app-loading><router-outlet></router-outlet>`
})
export class AppComponent {}
app.routes.ts
)之後在Routes這個檔案設定好,路由器會自動監聽瀏覽器的 URL 變化並更新視圖,由於 登入頁面沒有固定的頁首頁尾,所以跟 Layout 切開處理。
import { Routes } from '@angular/router';
import { LoginComponent } from './layouts/login/login.component';
import { APP_ROUTES } from './core/const/route.const';
import { loginGuard } from './core/guard/login.guard';
export const routes: Routes = [
{ path: APP_ROUTES.LOGIN, component: LoginComponent },
{
path: '', // 定義了子路由
loadChildren: () => import('./layouts/layout.route').then(m => m.LAYOUT_ROUTES),
canActivate: [loginGuard]
}
];
layout.routes.ts
)Angular 透過 app.routes.ts
中的 loadChildren
建立了這個連接關係。
而路由器透過 loadChildren
載入了 layout.routes.ts
,然後發現其中有 MainLayoutComponent 和各個子路由,依此類推
import { Routes } from '@angular/router';
import { APP_ROUTES } from '../core/const/route.const';
import { MainLayoutComponent } from './main-layout/main-layout.component';
export const LAYOUT_ROUTES: Routes = [
{
path: '', component: MainLayoutComponent,
children: [ // 定義了子路由
{
path: APP_ROUTES.DOCUMENT_APPLICATION,
loadChildren: () => import('../features/document-application/document-application.routes').then(m => m.DOCUMENT_APPLICATION_ROUTES)
},
{
path: APP_ROUTES.APPROVAL_CENTER,
loadChildren: () => import('../features/approval-center/approval-center.routes').then(m => m.APPROVAL_CENTER_ROUTES)
},
{
path: APP_ROUTES.SYSTEM_MANGE,
loadChildren: () => import('../features/system-mange/system-mange.routes').then(m => m.SYSTEM_MANGE_ROUTES)
}
]
}
];
我在一開始學的時候,主要還是抄前人跟網路上的寫法,常常處於知其然而不知其所以然的狀況,並不知道他們彼此之間怎麼溝通的。
畫成簡易的流程圖的話大概如下。
main.ts
│
│ provideRouter(routes)
▼
app.routes.ts
│
│ bootstrapApplication(AppComponent)
▼
app.component.ts
│
│ <router-outlet> + loadChildren 觸發
▼
layout.routes.ts (layout.route.ts)
│
│ component: MainLayoutComponent
▼
main-layout.component.ts
當 URL 匹配到 path: ''
時,Angular 會:
loadChildren
函數layout.route.ts
模組LAYOUT_ROUTES
配置LAYOUT_ROUTES
當作子路由系統來處理layout.route.ts
檔案LAYOUT_ROUTES
合併到應用程式的路由樹中component: MainLayoutComponent
,知道要渲染這個組件原本的路由樹:
app.routes.ts
├── /login → LoginComponent
└── / → (要載入子路由)
載入 LAYOUT_ROUTES
後變成:
合併後的路由樹
├── /login → LoginComponent
└── / → MainLayoutComponent
├── /document-application → ...
├── /approval-center → ...
└── /system-mange → ...
┌─────────────────────────────────────────────────────────────┐
│ main.ts │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ bootstrapApplication(AppComponent, { │ │
│ │ providers: [ │ │
│ │ provideRouter(routes) ←── import from app.routes│ │
│ │ ] │ │
│ │ }) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Angular 路由器 │
│ 註冊 app.routes.ts 中的路由配置 │
│ │
│ routes = [ │
│ { path: 'login', component: LoginComponent }, │
│ { path: '', loadChildren: () => import(...) } │
│ ] │
└─────────────────────────────────────────────────────────────┘
用戶訪問 URL: '/' 或 '/document-application'
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 路由匹配過程 │
│ │
│ ┌─────────────────┐ ┌───────────────────────────────┐ │
│ │ URL: '/' │ ──→ │ 匹配到 path: '' │ │
│ │ 或其他子路徑 │ │ (在 app.routes.ts 中) │ │
│ └─────────────────┘ └───────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 執行 loadChildren │
│ │
│ loadChildren: () => import('./layouts/layout.route') │
│ .then(m => m.LAYOUT_ROUTES) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 動態載入 layout.route.ts │ │
│ │ │ │
│ │ export const LAYOUT_ROUTES: Routes = [ │ │
│ │ { │ │
│ │ path: '', │ │
│ │ component: MainLayoutComponent, ←── 關鍵 │ │
│ │ children: [...] │ │
│ │ } │ │
│ │ ] │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 組件渲染階段 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AppComponent │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ <router-outlet> │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ MainLayoutComponent │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ 側邊選單 │ │ │ │
│ │ │ │ 頂部導航 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ <router-outlet> │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │ │
│ │ │ │ (準備渲染子路由組件) │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ URL 解析 │
│ │
│ URL: '/document-application/create' │
│ │ │ │
│ ▼ ▼ │
│ 第一段:'' 剩餘:'document-application/create'│
│ (匹配到 MainLayout) (傳給子路由系統處理) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 子路由匹配 (在 MainLayout 的 children 中) │
│ │
│ children: [ │
│ { │
│ path: 'document-application', │
│ loadChildren: () => import('...').then(...) │
│ } │
│ ] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 載入 document-application.routes.ts │ │
│ │ │ │
│ │ 繼續匹配 '/create' 路徑 │ │
│ │ 找到對應組件 │ │
│ │ │ │
│ │ 渲染在 MainLayoutComponent 的 <router-outlet> 中 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 瀏覽器畫面 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AppComponent │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ MainLayoutComponent │ │ │
│ │ │ ┌─────────┐ ┌─────────────────────────────┐ │ │ │
│ │ │ │ 側邊選單 │ │ 內容區域 │ │ │ │
│ │ │ │ (固定) │ │ │ │ │ │
│ │ │ │ │ │ DocumentCreateComponent │ │ │ │
│ │ │ │ - 文件 │ │ (或其他子路由組件) │ │ │ │
│ │ │ │ - 審核 │ │ │ │ │ │
│ │ │ │ - 系統 │ │ ← 這裡會根據 URL 切換 │ │ │ │
│ │ │ │ │ │ 但選單和導航保持不變 │ │ │ │
│ │ │ └─────────┘ └─────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
loadChildren
建立與 layout.routes.ts 的連接<router-outlet>
<router-outlet>
實現了巢狀路由的渲染