Angular v14+ Standalone Components:告別 NgModule,擁抱更簡潔的元件架構

李尚儒 2025/06/11 19:15:45
25

在 Angular 的傳統架構中,NgModule 扮演著組織應用程式、封裝元件 (Components)、指令 (Directives) 和管道 (Pipes) 的核心角色。然而,隨著應用程式規模的增長,管理眾多的 NgModule 有時會變得複雜,並可能引入不必要的樣板程式碼。

為了解決這些問題並簡化 Angular 的開發模型,Angular v14 引入了 Standalone Components (獨立元件),並在 v15 正式穩定。這個特性允許開發者建立不依賴於任何 NgModule 的元件、指令和管道,使得架構更輕量、依賴關係更清晰,並且是目前 Angular 官方推薦的元件開發方式。

直觀比較:Standalone Components 如何簡化元件定義

讓我們以一個簡單的「使用者歡迎卡片」元件為例,看看 Standalone Components 與傳統 NgModule-based Components 在定義上有何不同。

過去寫法 (NgModule-based Component):

通常需要至少兩個檔案:元件本身的 .ts 檔案和一個專門用來聲明與導出此元件的 .module.ts 檔案 (或者在一個共享模組中聲明)。

<!-- user-card.component.ts (舊版) -->
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-card-old',
  template: `<div>歡迎,{{ userName }}!</div>`
})
export class UserCardOldComponent {
  @Input() userName: string = '';
}

<!-- user-card.module.ts (舊版) -->
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; // 可能需要,如果模板用了 *ngIf 等
import { UserCardOldComponent } from './user-card.component';

@NgModule({
  declarations: [UserCardOldComponent],
  imports: [CommonModule],
  exports: [UserCardOldComponent] // 如果其他模組要使用
})
export class UserCardOldModule {}

<!-- 要使用此元件的父元件的模組 -->
<!-- parent.module.ts -->
<!--
import { UserCardOldModule } from './user-card/user-card.module';
// ...
@NgModule({
  // ...
  imports: [
    // ...
    UserCardOldModule
  ]
})
export class ParentModule {}
-->

(上述過去的寫法中,即使是一個簡單的元件,也需要 NgModule 來進行聲明和管理其依賴,並在使用時導入該模組。)

現在寫法 (Standalone Component):

元件可以直接管理自己的依賴,無需額外的 NgModule

<!-- user-card.component.ts (新版 - Standalone) -->
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; // 假設模板中用了 *ngIf 或其他 CommonModule 的功能

@Component({
  selector: 'app-user-card-new',
  standalone: true, // 關鍵!標記為 Standalone Component
  imports: [CommonModule], // 直接在此處導入依賴 (其他 Standalone Components, Directives, Pipes 或 NgModules)
  template: `
    <div class="user-card">
      @if (userName) {
        <h4>歡迎回來,{{ userName }}!</h4>
        <p>這是您的個人化卡片。</p>
      } @else {
        <p>請登入以查看您的卡片。</p>
      }
    </div>
  `,
  styles: [`.user-card { border: 1px solid #ccc; padding: 15px; border-radius: 5px; }`]
})
export class UserCardNewComponent {
  @Input() userName: string = '';
}

<!-- 要使用此元件的父元件 (可以是 Standalone Component 或 NgModule-based Component) -->
<!-- parent.component.ts -->
<!--
import { UserCardNewComponent } from './user-card/user-card.component';
// ...
@Component({
  standalone: true, // 假設父元件也是 Standalone
  imports: [UserCardNewComponent], // 直接導入 Standalone Component
  // ...
})
export class ParentComponent {}
-->

比較結果顯而易見:Standalone Components 大幅減少了定義和使用元件所需的樣板程式碼。元件的依賴關係在其自身的 imports 陣列中一目了然,不再需要透過 NgModule 進行中介管理,使得架構更扁平、更直觀。

深入瞭解 Standalone Components

什麼是 Standalone Components?

Standalone Components (以及 Directives 和 Pipes) 是指那些不屬於任何特定 NgModule,並且能夠直接管理自身模板依賴的 Angular 建構塊。它們透過設定元數據中的 standalone: true 屬性來標識。

核心優勢

  • 簡化架構與減少樣板:無需為每個元件、指令或管道都建立或尋找對應的 NgModule
  • 更清晰的依賴管理:元件的依賴 (其他 Components, Directives, Pipes, NgModules) 直接在其 imports 陣列中聲明,一目了然。
  • 提升學習曲線平緩度:對於 Angular 新手而言,可以先專注於 Component 的概念,而暫時無需深入理解複雜的 NgModule 體系。
  • 易於共享與重用:Standalone Components 更容易在不同專案或函式庫之間共享。
  • 潛在的建置優化:更細粒度的依賴關係有助於更精確的 Tree-shaking (搖樹優化)。

如何建立與使用 Standalone Component

  1. 標記為 Standalone: 在 Component (或 Directive, Pipe) 的 @Component (或 @Directive, @Pipe) 裝飾器中,設定 standalone: true
    import { Component } from '@angular/core';
    
    @Component({
      standalone: true,
      selector: 'app-my-standalone-widget',
      template: `<p>我是一個獨立的小工具!</p>`
    })
    export class MyStandaloneWidgetComponent {}
  2. 管理模板依賴: 使用 imports 陣列來導入該 Component 模板中需要用到的其他 Standalone Components, Directives, Pipes,或者傳統的 NgModules。
    import { Component } from '@angular/core';
    import { CommonModule } from '@angular/common'; // 用於 *ngIf, *ngFor 等
    import { AnotherStandaloneComponent } from '../another-standalone/another-standalone.component';
    import { MyStandalonePipe } from '../my-standalone.pipe';
    // import { SomeNgModule } from '../some-ng.module'; // 也可以導入 NgModule
    
    @Component({
      standalone: true,
      selector: 'app-complex-standalone',
      imports: [
        CommonModule, // 注意:v17+ 的新控制流 @if, @for 已無需 CommonModule
        AnotherStandaloneComponent,
        MyStandalonePipe,
        // SomeNgModule
      ],
      template: `
        <h3>複雜獨立元件</h3>
        @if (isVisible) {
          <app-another-standalone></app-another-standalone>
          <p>{{ someValue | myStandalonePipe }}</p>
        }
      `
    })
    export class ComplexStandaloneComponent {
      isVisible = true;
      someValue = '測試文字';
    }
  3. 使用 CLI 產生: Angular CLI 支援直接產生 Standalone Components:
    ng generate component my-feature --standalone
    ng g c my-other-feature --standalone=true
    自 Angular v17 起,ng new 建立的新專案預設就會使用 Standalone Components。

Standalone Components 中的依賴注入

依賴注入的機制在 Standalone Components 中基本保持不變。您仍然可以在建構函式中注入服務。主要的區別在於提供服務 (Providers) 的方式:

  • 對於整個應用程式範圍的服務,推薦使用 providedIn: 'root'
  • Standalone Components 可以有自己的 Providers,在其 @Component 裝飾器的 providers 陣列中定義,這些 Providers 的作用域將限定在此 Component 及其子代。
  • 路由級別的 Providers 也可以在路由設定中提供。

路由 (Routing) 與 Standalone Components

Standalone Components 可以直接用於路由,包括延遲載入:

// app.routes.ts
import { Routes } from '@angular/router';
// 假設 UserProfileComponent 和 ProductDetailComponent 都是 Standalone Components

export const routes: Routes = [
  {
    path: 'profile/:id',
    loadComponent: () => import('./user-profile/user-profile.component').then(m => m.UserProfileComponent)
  },
  {
    path: 'product/:productId',
    component: ProductDetailComponent // 直接路由到一個 Standalone Component
  },
  // ... 其他路由
];

// main.ts 或 app.config.ts (引導應用程式)
// bootstrapApplication(AppComponent, {
//   providers: [
//     provideRouter(routes)
//   ]
// });

使用 loadComponent 可以延遲載入一個 Standalone Component 及其相關的 JavaScript chunk。

與 NgModule 的關係與互通性

Standalone Components 並非要完全取代 NgModule,它們可以與現有的 NgModule 和諧共存,並逐步遷移。

  • Standalone Component 導入 NgModule:一個 Standalone Component 可以在其 imports 陣列中導入一個 NgModule,以使用該 NgModule 所導出的 Components, Directives, Pipes。
  • NgModule 導入 Standalone Component:一個 NgModule 也可以在其 imports 陣列中導入一個 Standalone Component,然後在其 `declarations` 中聲明的 Components 的模板裡使用這個 Standalone Component。

測試 Standalone Components

測試 Standalone Components 通常比測試 NgModule-based Components 更簡單,因為您只需要在測試環境中導入該 Standalone Component 及其直接的模板依賴,而無需處理複雜的 TestBed.configureTestingModule 中的 NgModule 設定。

// my-standalone.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyStandaloneComponent } from './my-standalone.component';
// 假設 MyStandaloneComponent 依賴了 CommonModule 和 AnotherStandaloneComponent
import { CommonModule } from '@angular/common';
import { AnotherStandaloneComponent } from '../another-standalone/another-standalone.component';

describe('MyStandaloneComponent', () => {
  let component: MyStandaloneComponent;
  let fixture: ComponentFixture<MyStandaloneComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        MyStandaloneComponent, // 直接導入待測試的 Standalone Component
        // CommonModule, // 如果模板中用到 *ngIf 等,且 MyStandaloneComponent 內部沒有 import
        // AnotherStandaloneComponent // 如果模板中用到
      ]
    })
    .compileComponents();

    fixture = TestBed.createComponent(MyStandaloneComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

實際上,如果 MyStandaloneComponent 已經在其 imports 陣列中正確聲明了 CommonModuleAnotherStandaloneComponent,那麼在 TestBed.configureTestingModuleimports 中可能只需要列出 MyStandaloneComponent 本身。

遷移至 Standalone Components

Angular CLI 提供了 schematics 來幫助將現有的 NgModule-based 應用遷移到使用 Standalone APIs:

ng generate @angular/core:standalone

這個命令會引導您完成遷移過程,包括將 declarations 轉換為 standalone,更新引導方式,以及將 NgModule 轉換為只提供路由設定等。建議在遷移大型專案時分階段進行並充分測試。

結論:Standalone Components —— 更現代、更簡潔的 Angular

Standalone Components 是 Angular 發展的一個重要方向,它大幅簡化了元件的定義和依賴管理,降低了學習門檻,並為建構更輕量、更模組化的應用程式提供了堅實的基礎。對於新專案,強烈建議直接採用 Standalone Components;對於現有專案,也可以考慮逐步遷移,以享受其帶來的諸多益處。

掌握 Standalone Components,將使您的 Angular 開發之旅更加高效和愉快!

李尚儒