iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0

使用 Angular CLI MCP Server 啟動 Angular 應用程式

/angular-cli:  Please start the development server

Angular CLI MCP Server 在 http://localhost:54226/home 啟動應用程式。當 Gemini 進行程式碼修改時,Angular 會進行熱重載(Hot-reload),我可以立即在瀏覽器中看到最新的更新。

實作主畫面

產生主頁圖示並修改頁首元件

/implement Do step 1 to implement the inline template and css of HomeIconComponent. Then, do step 2 to add home screen navigation and delete the subtext.  Then, use /tdd to identity public seams of both components and implement unit tests.  Please strictly follow the contract in DESIGN.md

產生的元件意外地包含了 OnPush 變更偵測(Change Detection)宣告。在 Angular 22 中,獨立元件(Standalone components)預設即為 OnPush,因此我提示 Gemini 將其移除。我還將此規則新增至 Agent 的記憶中,以便 Gemini 不會再犯相同的錯誤。

Hi.  In Angular 22, Change Detection is OnPush, no need to add it into the Angular components.

重構後具備主畫面導覽功能的頁首元件如下所示:

@Component({
  selector: 'app-header',
  imports: [HomeIconComponent, RouterLink],
  template: `
    <header class="app-header">
      <a routerLink="/home">
        <app-home-icon />
      </a>
      <h1>Firebase AI Logic Obscure Fact Speech Generator</h1>
    </header>
  `,
  styleUrl: './header.component.css',
})
export class HeaderComponent {}

重構應用程式路由

我與 Gemini 討論了關於避免在 HTML 範本中寫死路徑(Hardcoded paths,例如 /dashboard 與 /home)的議題。當應用程式規模較小時,變更路由路徑只會影響少數檔案;但隨著應用程式擴展,路由變更很容易牽涉到數十個檔案,且遺漏的風險極高。

export const ROUTE_PATHS = {
  HOME: 'home',
  DASHBOARD: 'dashboard'
} as const;

export type RouteKey = keyof typeof ROUTE_PATHS;
export type AppRoute = `/${(typeof ROUTE_PATHS)[RouteKey]}`;

export const APP_LINKS: Record<RouteKey, AppRoute> = {
  HOME: '/home',
  DASHBOARD: '/dashboard',
};

我們宣告了 RouteKey 與 AppRoute 型別,其中 AppRoute 是 /home | /dashboard 的型別安全別名。

APP_LINKS 將 HOME 與 DASHBOARD 鍵(Keys)直接對應至它們各自的路由路徑。

在頁首元件類別中,APP_LINKS.HOME 被指派給唯讀的 homeLink 成員。範本中寫死(Hardcoded)的路徑已替換為 homeLink。

@Component({
  selector: 'app-header',
  imports: [HomeIconComponent, RouterLink],
  template: `
    <header class="app-header">
      <a [routerLink]="homeLink">
        <app-home-icon />
      </a>
      <h1>Firebase AI Logic Obscure Fact Speech Generator</h1>
    </header>
  `,
  styleUrl: './header.component.css',
})
export class HeaderComponent {
  readonly homeLink = APP_LINKS.HOME;
}

產生主畫面元件

/implement Do Step 3 to implement the HomeComponent.  Use Angular RouteLink directive on the launch button to navigate to the dashboard component. Assign APP_LINKS.DASHBOARD to a default readonly dashboard member of the class.  Use dashboard in the template instead of the hardcoded value.

Gemini 產生了 HomeComponent,但其 HTML 範本與 CSS 樣式需要進一步重構。

未預期的內聯 SVG

<a [routerLink]="dashboard" class="btn-primary btn-launch group">
  <span>Launch Studio</span>
  <svg
    class="w-5 h-5 transition-transform duration-200 group-hover:translate-x-1"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    viewBox="0 0 24 24"
    aria-hidden="true"
  >
    <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
  </svg>
</a>

HTML 範本在啟動按鈕(Launch button)上渲染了一個非預期的內聯向右箭頭 SVG。雖然它存在於 Stitch 的主畫面模型中,但並未明確記錄在 DESIGN.md 中。我們同意將該內聯 SVG 重構為獨立圖示元件,並在 Markdown 規格書中更新主畫面的設計規範。

/implement refactor the inline SVG in @src/app/features/home/home.component.html into an icon component and update the design contract of the home screen  @DESIGN.MD

在 HTML 範本中,內聯 SVG 圖示已替換為 ArrowRightIconComponent。

<section class="home-container">
  <div class="home-card surface-card">
    ... no change ...

    <a [routerLink]="dashboard" class="btn-primary btn-launch group">
      <span>Launch Studio</span>
      <app-arrow-right-icon class="btn-launch-icon" />
    </a>

    ... no change ...
  </div>
</section>

DESIGN.md 中的設計規範也已更新:

- Semantic link element with `[routerLink]="dashboard"` (typed `APP_LINKS.DASHBOARD`).
- Content: `"Launch Studio"` label accompanied by standalone `<app-arrow-right-icon>` (`ArrowRightIconComponent`, `aria-hidden="true"`) with micro-interaction hover translation (`group-hover:translate-x-1`).

主題語彙基元未套用至元件樣式

主畫面元件的初始 CSS 並未使用我們的主題語彙基元(Theme tokens),因此我提示 Gemini 檢查 home.component.css,找出是否重複定義了已經在 styles.css 中宣告的主題語彙基元、實用工具(Utilities)與類別。

@src/app/features/home/home.component.css has many styles, do the styles duplicate the theme tokens, utilities or classes in @src/styles.css?  Please explain in Markdown in succinct format.

Gemini 識別出重複之處,並將其彙總至 Markdown 表格中。

home.component.css 中的類別 home.component.css 中的規則 styles.css 中的對應項 分析
.home-card bg-slate-800/50 border border-slate-700 @utility surface-card 重複了背景與邊框顏色
.btn-launch font-semibold gap-2 @utility btn-primary 重複了字型粗細與 flex 間距
.home-title text-slate-100 --color-text-primary 與主要文字語彙基元重疊
.home-description text-slate-400 --color-text-muted 與次要淡化文字語彙基元重疊
.feature-label text-slate-400 --color-text-muted 與次要淡化文字語彙基元重疊
.feature-value text-slate-200 --color-text-secondary 與次要文字語彙基元重疊
.home-container flex flex-col items-center py-8 (無) 獨特頁面佈局
.home-badge text-indigo-400 bg-indigo-950/60 (無) 獨特分類標籤
.feature-item bg-slate-900/60 border border-slate-800 (無) 獨特子卡片

我們將寫死的顏色與類別替換為主題語彙基元與實用工具類別(Utility classes):

/implement clean up the duplicated CSS styles in home.component.css and extract the inline CSS of <app-arrow-right-icon> to the external CSS file. Verify the CSS styles of the icon component do not duplicate the theme tokens, utilities and classes in styles.css.

更新後的 HomeComponent 樣式表現在完全使用主題語彙基元,而非寫死的顏色值。

@reference "../../../styles.css";

.home-container {
  @apply w-full flex-1 flex flex-col items-center justify-center py-8 sm:py-16;
}

.home-card {
  @apply max-w-2xl w-full p-6 sm:p-12 text-center flex flex-col items-center gap-6 rounded-2xl shadow-2xl backdrop-blur-sm;
}

.home-badge {
  @apply text-xs uppercase font-bold tracking-widest text-indigo-400 bg-indigo-950/60 border border-indigo-800/60 px-3 py-1 rounded-full;
}

.home-title {
  @apply text-2xl sm:text-3xl lg:text-4xl font-bold tracking-tight text-(--color-text-primary);
}

.home-description {
  @apply text-sm sm:text-base text-(--color-text-muted) max-w-lg leading-relaxed;
}

.btn-launch {
  @apply px-8 py-3.5 text-base shadow-lg shadow-indigo-500/20 flex items-center justify-center;
}

.btn-launch-icon {
  @apply transition-transform duration-200 group-hover:translate-x-1;
}

.home-features-grid {
  @apply w-full grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4 pt-6 border-t border-(--color-surface-border)/60 mt-2;
}

.feature-item {
  @apply bg-slate-900/60 border border-slate-800 rounded-xl p-3 sm:p-4 flex flex-col items-center gap-1 text-center;
}

.feature-label {
  @apply text-xs font-medium text-(--color-text-muted) uppercase tracking-wider;
}

.feature-value {
  @apply text-sm font-semibold text-(--color-text-secondary);
}

現在我們擁有一個功能齊全的主畫面,當使用者點擊啟動按鈕時,可以流暢地導覽至儀表板。

然而,這引出了一個新的觀察結果。在為應用程式新增兩個新圖示元件(HomeIconComponent 與 ArrowRightIconComponent)後,圖示總數增加到了七個。我注意到這些圖示的 :host 宣告中存在重複的 CSS 類別,因此我提示 Gemini 檢查 src/app/shared/ui/icons 中的樣式,以找出通用的基礎類別。

/grill-with-docs look at the css files in @[src/app/shared/ui/icons], do you find base CSS classes?

Gemini 發現 HomeIconComponent、ArrowRightIconComponent、CheckIconComponent、MicIconComponent 以及 ArrowDropDownIconComponent 都共享相同的基礎樣式:inline-flex items-center justify-center shrink-0 w-5 h-5 select-none。

解決方案是在 styles.css 中新增一個實用工具 @utility app-icon,並在圖示元件的 :host 元素上重複使用它:

@utility app-icon {
  @apply inline-flex items-center justify-center shrink-0 w-5 h-5 select-none;
}

我們也發現 text-indigo-400 在多個元件中被寫死,因此我們在 styles.css 中引入了新的主題語彙基元與 CSS 自訂變數,並進行了全域替換:

@theme {
  --color-primary-accent: #818cf8;
}

:root {
  --color-primary-accent: #818cf8;
}

ArrowRightIconComponent、HomeIconComponent 與 ExpandMoreIconComponent 的 host 元素現在只需使用:

:host {
  @apply app-icon;
}

ArrowDropDownIconComponent 的 host 元素使用:

:host {
  @apply app-icon text-(--color-text-muted) transition-transform duration-200;
}

最後,CheckIconComponent 的 host 元素使用:

:host {
  @apply app-icon text-(--color-primary-accent);
}

在圖示元件共享標準化基礎樣式後,我們將注意力轉向延遲呼叫 ConfigService.initialize(),直到使用者實際造訪 /dashboard 為止。

動態匯入 Firebase 相依套件

HomeComponent 不需要 Firebase,因此沒有理由在應用程式啟動(Bootstrap)期間於 provideAppInitializer 中將其初始化。我們可以完全從 Application Config 中移除 provideAppInitializer。

在我們的路由設定中,當使用者導覽至 /dashboard 路由時,會叫用 ConfigService.initialize():

export const routes: Routes = [
  ... other paths ...
  {
    path: 'dashboard',
    title: 'Firebase TTS',
    providers: [provideEnvironmentInitializer(() => inject(ConfigService).initialize())],
    loadComponent: () => import('./features/dashboard/dashboard.component'),
  },
];

在 ConfigService 中,firebase/app 與 firebase/remote-config 在 initialize() 內部動態匯入,使它們完全排除在主打包檔案(Main bundle)之外:

import type { FirebaseApp } from 'firebase/app';

#appReady: Promise<void> | null = null;

async getAiBackend(): Promise<AI> {
    if (this.#appReady) {
      await this.#appReady;
    }

    /* ... return Firebase AI backend  */
}

private loadFirebase(): Promise<void> {
    return Promise.all([import('firebase/app'), import('firebase/remote-config')])
      .then(([{ initializeApp }, remoteConfigSdk]) => {
        this.#app = initializeApp(firebaseConfig.app);
        this.fetchRemoteConfig(remoteConfigSdk);
      })
      .catch((error) => {
        this.#appReady = null;
        this.#app = undefined;
        this.#ai = null;
      });
  }

private fetchRemoteConfig({
  getRemoteConfig, fetchAndActivate, getValue,
}: Pick<typeof import('firebase/remote-config'), 'getRemoteConfig' | 'fetchAndActivate' | 'getValue'>) {

  const rc = getRemoteConfig(this.#app);
  rc.defaultConfig = remoteConfigDefaults;
  if (this.#isOnline()) {
    fetchAndActivate(rc)
      .then((activated) => {
        this.#appConfig = {
          ... retrieve remote config values ...
        };
        this.#ai = null;
      });
  }
}

initialize(): void {
  if (!this.#appReady) {
    this.#appReady = this.loadFirebase();
  }
}

只有 FirebaseApp 型別是從 firebase/app 靜態匯入。初始化 Firebase 與擷取遠端設定(Remote Config)現在皆延遲至動態匯入 Promise 鏈中處理。

打包大小比較

區塊檔案(Chunk File) 之前(c1e2f7b) 之後(f98bf4e) 節省(原始大小) 節省(傳輸大小)
main.js(應用程式進入點) 187.72 kB 126.17 kB -61.55 kB (-32.8%) -16.59 kB (-35.2%)
Angular 框架區塊 170.88 kB 171.67 kB +0.79 kB +0.14 kB
全域樣式表 27.73 kB 31.93 kB +4.20 kB (Home CSS) +0.44 kB
路由器支援區塊 7.02 kB 7.03 kB +0.01 kB 0.00 kB
執行階段微區塊 464 B 464 B 0 B 0 B
總初始酬載(Total Initial Payload) 393.81 kB 337.26 kB -56.55 kB (-14.4%) -16.00 kB (-15.2%)

main.js 縮小了 61 kB(~32.8%),總初始酬載減少了 56.55 kB(~14.4%)。在網路傳輸方面,main.js 的傳輸大小下降了 16.59 kB(~35.2%)。

最重要的是,初次造訪的使用者下載的是一個輕量的登陸頁面,而不是在一開始就即時解析整個儀表板、其子元件以及繁重的 Firebase 相依套件。

讓我們使用 Chrome DevTools MCP Server,在桌面與行動裝置檢視埠中透過 Lighthouse 與效能稽核來驗證這些最佳化成果。

效能與 Lighthouse 稽核:http://localhost:5005

Lighthouse 分數

類別 桌面(Desktop) 行動裝置(Mobile)
無障礙(Accessibility) 100 100
最佳實踐(Best Practices) 100 100
搜尋引擎最佳化(SEO) 100 100
Agentic 瀏覽(Agentic Browsing) 100 100

在兩種檢視埠中,每一個類別皆取得了滿分 100 分。

核心 Web 指標與載入時間

指標 桌面(Desktop) 行動裝置(Mobile,4x CPU,Fast 4G) 目標 / 門檻值
首次內容繪製(FCP) 128 ms 460 ms < 1.8 s(良好)
最大內容繪製(LCP) 127 ms 457 ms < 2.5 s(良好)
總阻塞時間(TBT) 0 ms 0 ms < 200 ms(良好)
累計版面配置轉移(CLS) 0.00 0.00 < 0.1(良好)
DOMContentLoaded 63 ms 389 ms —
網頁載入時間(Page Load Time) 75 ms 429 ms —

FCP、LCP、TBT 與 CLS 在兩種檢視埠中皆維持了優異的數值。

第 30 天告一段落!明天我打算進行語音合成(TTS)最佳化:當 Signal Form 表單資料自上次生成後未曾變更時,Angular 就會略過向 Firebase AI Logic 發送生成請求。

參考資源


上一篇
Day 29 - 使用 Stitch MCP Server 與 AI Agent 技能設計新主畫面
下一篇
Day 31 - 降低雲端成本與延遲:AI 語音合成的響應式快取
系列文
2026年,如何利用 Antigravity CLI、Gemini、各項技能及 MCP Server 建構基於 Firebase 的 Angular 應用 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言