目前我們已經建立了負責身分驗證的 AuthService,以及讓使用者輸入電子郵件與密碼的 SignInModalComponent,接著我們需要一個輕量級的首頁畫面(HomeComponent)來將兩者串接起來。當使用者尚未通過身分驗證時,點擊首頁畫面上的 Sign In 按鈕會開啟 CDK 登入互動視窗;使用者輸入電子郵件與密碼後,即可完成身分驗證並導覽至 /dashboard 路由。
此外,HeaderComponent 不再僅顯示靜態文字,而是在左側邊緣顯示首頁圖示連結,並在右側邊緣依條件顯示登出(Sign-Out)按鈕。

圖說:未驗證狀態下的首頁畫面,在頁首左側顯示首頁圖示、主視覺卡片中顯示「Sign In」按鈕,以及下方的功能介紹卡片。
HomeComponent 與 HeaderComponent 共用 AuthService.isAuthenticated 計算型 Signal(Computed Signal),藉此在以下三種狀態之間協調使用者介面(UI):
/home:HeaderComponent 僅顯示靠左對齊的首頁圖示連結(/home),而 HomeComponent 則渲染 Sign In 按鈕。openSignInModal()):點擊 Sign In 會透過 Promise.all() 動態匯入 SignInModalComponent 與 Dialog(@angular/cdk/dialog),避免將 CDK 對話框與 Signal Forms 包含在初始的 main.js 打包檔案中。/dashboard 與 /home:一旦登入成功,HeaderComponent 會顯示靠右對齊的登出(Sign-Out)按鈕。當已驗證的使用者返回 /home 時,HomeComponent 會檢查 isAuthenticated(),並顯示 Launch Studio 連結(/dashboard)以取代 Sign In 按鈕。flowchart LR
subgraph Header["HeaderComponent (app-header)"]
HOME_ICON["HomeIconComponent ([routerLink]='/home')"]
AUTH_HDR{"isAuthenticated()?"}
SIGNOUT_BTN["SignOutIconComponent (onSignOut -> /home)"]
AUTH_HDR -- "true" --> SIGNOUT_BTN
end
subgraph Home["HomeComponent (/home)"]
RESTORE["constructor: ensureAuth().catch(...)"]
AUTH_HOME{"isAuthenticated()?"}
SIGNIN_BTN["Button: Sign In (openSignInModal)"]
LAUNCH_LINK["Link: Launch Studio ([routerLink]='/dashboard')"]
RESTORE --> AUTH_HOME
AUTH_HOME -- "false" --> SIGNIN_BTN
AUTH_HOME -- "true" --> LAUNCH_LINK
end
subgraph LazyModal["Deferred Modal Chunk"]
IMPORT["Promise.all([import(SignInModalComponent), import('@angular/cdk/dialog')])"]
INJECT["runInInjectionContext(#injector, () => inject(Dialog).open(...))"]
IMPORT --> INJECT
end
SIGNIN_BTN --> IMPORT
INJECT -- "Auth Success" --> AUTH_HOME
INJECT -- "Auth Success" --> AUTH_HDR
HeaderComponent 以加入首頁導覽與條件式登出按鈕在 HeaderComponent 中,我們加入了首頁導覽連結與條件式登出按鈕:
imports 陣列中匯入 HomeIconComponent、RouterLink 與 SignOutIconComponent。<app-home-icon /> 置於設定了 [routerLink]="homeLink"(APP_LINKS.HOME,即 /home)的 <a> 標籤內,點擊時即可導覽回首頁畫面。isAuthenticated() 為 true 時顯示 <app-sign-out-icon /> 按鈕;點擊時會呼叫 onSignOut() 以登出使用者、更新 isAuthenticated(),並返回 /home。// src/app/shared/ui/layout/header/header.component.ts
@Component({
selector: 'app-header',
imports: [HomeIconComponent, RouterLink, SignOutIconComponent],
})
export class HeaderComponent {
readonly homeLink = APP_LINKS.HOME;
readonly #authService = inject(AuthService);
readonly #router = inject(Router);
readonly isAuthenticated = this.#authService.isAuthenticated;
async onSignOut(): Promise<void> {
await this.#authService.signOut();
await this.#router.navigate([APP_LINKS.HOME]);
}
}
<!-- src/app/shared/ui/layout/header/header.component.html -->
<header>
<a [routerLink]="homeLink" aria-label="Go to Home Screen">
<app-home-icon />
</a>
<h1>Firebase AI Logic Obscure Fact Speech Generator</h1>
@if (isAuthenticated()) {
<button type="button" (click)="onSignOut()" aria-label="Sign out">
<app-sign-out-icon />
</button>
}
</header>
HomeComponent 與延遲載入 CDK DialogensureAuth().catch(...))當 HomeComponent 被建構時,我們需要確保 Firebase Auth Emulator 與 Firebase Auth 已正確初始化。然而,constructor() 不能設為 async,因此我們必須使用 Promise 鏈(Promise chain)而非 async/await 來執行 this.#authService.ensureAuth()。
.catch() 區塊會記錄並靜默攔截任何錯誤,而不會重新拋出錯誤。由於這項背景任務不需要使用者介入處理,在此處顯示錯誤訊息只會造成使用者的困惑與不必要的恐慌。
// src/app/features/home/home.component.ts (Part 1: Fields & Constructor)
@Component({
selector: 'app-home',
imports: [RouterLink, ArrowRightIconComponent],
})
export class HomeComponent {
readonly dashboard = APP_LINKS.DASHBOARD;
readonly #authService = inject(AuthService);
readonly isAuthenticated = this.#authService.isAuthenticated;
readonly #injector = inject(Injector);
#dialogRef: DialogRef<unknown, SignInModalComponent> | null = null;
#isOpeningModal = false;
constructor() {
this.#authService.ensureAuth().catch((error) => console.error('Failed to restore auth session:', error));
}
@angular/cdk/dialog 並使用 runInInjectionContext 解決 NG0203 注入環境錯誤在 openSignInModal() 中開啟登入互動視窗時,我們在維持初始打包檔案精簡的同時,也需要保留 Angular 的注入環境(Injection Context):
main.js 輕量化:由於 HomeComponent 會被預先載入(Eagerly Loaded)至 main.js 打包檔案中,任何靜態匯入都會增加其原始大小與傳輸時間。@angular/cdk/dialog 與 NG0203 錯誤:我們透過在 openSignInModal() 中動態匯入 SignInModalComponent 與 @angular/cdk/dialog 來最佳化打包檔案。然而,在 await Promise.all(...) 之後呼叫 inject(Dialog) 會拋出 NG0203 錯誤,因為該呼叫是在有效的注入環境之外執行的。HomeComponent 的 Injector(this.#injector),將對話框開啟邏輯放在 runInInjectionContext 的回呼函式內執行,藉此解決此問題。runInInjectionContext 而非高階函式:使用 runInInjectionContext 讓我們能直接在箭頭函式內指派 this.#dialogRef = dialogRef,這比對高階函式傳入 .bind(this) 更加簡潔。this.#dialogRef(dialogRef.closed.subscribe)在 openSignInModal() 中,我們會防止開啟重複的登入互動視窗,並在互動視窗關閉時清理對話框參考:
#isOpeningModal 與 #dialogRef 可防止同時開啟多個登入互動視窗;當其中任一值為真值(Truthy)時,代表互動視窗正在開啟或已經開啟,因此該方法會提早結束。Promise.all() 動態匯入 SignInModalComponent 與 @angular/cdk/dialog,注入 Dialog 以開啟互動視窗,並將其參考儲存於 this.#dialogRef 中。#dialogRef:訂閱 dialogRef.closed 會在互動視窗關閉時將 this.#dialogRef 重設為 null,確保之後能再次開啟對話框。// src/app/features/home/home.component.ts (Part 2: openSignInModal)
async openSignInModal(): Promise<void> {
if (this.#isOpeningModal || this.#dialogRef) {
return;
}
this.#isOpeningModal = true;
try {
const [{ SignInModalComponent }, { Dialog }] = await Promise.all([
import('@/shared/ui/sign-in-modal/sign-in-modal.component'),
import('@angular/cdk/dialog'),
]);
runInInjectionContext(this.#injector, () => {
const dialog = inject(Dialog);
const dialogRef = dialog.open(SignInModalComponent, {
backdropClass: ['bg-surface-card'],
});
this.#dialogRef = dialogRef;
dialogRef.closed.subscribe(() => (this.#dialogRef = null));
});
} finally {
this.#isOpeningModal = false;
}
}
}
home.component.html 中的條件式行動呼籲(CTA)渲染在 home.component.html 中,isAuthenticated() 計算型 Signal 會決定要顯示哪一個按鈕——包括當使用者在網址列輸入 /home 或點擊 HeaderComponent 中的首頁圖示返回 /home 時:
isAuthenticated() 為 true 時:首頁畫面會渲染 Launch Studio 連結([routerLink]="dashboard"),直接導覽至 /dashboard。isAuthenticated() 為 false 時:首頁畫面會渲染 Sign In 按鈕((click)="openSignInModal()"),點擊後會開啟 CDK 登入互動視窗。<!-- src/app/features/home/home.component.html (CTA excerpt) -->
@if (isAuthenticated()) {
<a [routerLink]="dashboard">
<span>Launch Studio</span>
<app-arrow-right-icon />
</a>
} @else {
<button type="button" (click)="openSignInModal()">
<span>Sign In</span>
</button>
}

圖說:已驗證狀態下的首頁畫面,在頁首左側顯示首頁圖示、右側顯示登出圖示,並在主視覺卡片中顯示「Launch Studio」按鈕。
在第 3 部分中,我們透過串接 HomeComponent、SignInModalComponent 與 HeaderComponent 完成了整個身分驗證流程:
HomeComponent 中的條件式操作:我們建置了輕量級的 HomeComponent,根據 isAuthenticated() Signal 依條件渲染 Sign In 與 Launch Studio 按鈕。當 isAuthenticated() 為 false 時顯示 Sign In 按鈕;為 true 時則顯示 Launch Studio。SignInModalComponent,讓使用者輸入電子郵件與密碼。當憑證有效時,互動視窗會將使用者導覽至 /dashboard 路由;否則會顯示身分驗證錯誤訊息。runInInjectionContext 解決 NG0203:在 openSignInModal() 的 await Promise.all(...) 之後注入 Dialog 會觸發 Angular 的 NG0203 錯誤,因為非同步接續區塊是在注入環境之外執行的。我們透過使用 HomeComponent 的 Injector,在 runInInjectionContext 的回呼函式內執行對話框邏輯來解決此問題。HeaderComponent 加入了首頁導覽與登出功能。它會固定顯示首頁圖示,讓使用者在點擊時返回 /home 畫面;而條件式顯示的登出圖示則讓已驗證的使用者能從 Firebase Auth 登出,並將 isAuthenticated() Signal 重設為 false。