在 Angular 14 新增了 Standalone Component 的功能,以往我們新增的 Component、Directive 以及 Pipe 可以不需要透過 NgModule 來管理元件,同時也能簡化使用 Angular 開發應用的體驗。現有應用可以選擇性逐步採用新的獨立樣式,而無需進行任何重大更改。
Stand Alone

如何使用 Standalone Component

建立 Standalone Component

語法相當簡單,只需要在 ng generate 指令後面加上 --standalone 即可:

ng generate component <component-name> --standalone

Component 轉換成 Standalone Component

如果要在既有的 Component 轉換成 Standalone Component,只需要在 @Component 裡面加上 standalone: true

@Component({
  standalone: true,
  selector: 'photo-gallery',
  imports: [ImageGridComponent],
  template: ` ... <image-grid [images]="imageList"></image-grid> `,
})
export class PhotoGalleryComponent {
  // component logic
}

加上 standalone: true 之後,我們就可以在 Component 使用 imports 來引入其他的 Dependency,像是 Directive、Pipe、Component 等等。此外 imports 也可以引入其他的 NgModule。

既有的 NgModule 加入 Standalone Component

Standalone Component 透過 NgModule.imports 就可以加到既有的 NgModule 中:

@NgModule({
  declarations: [AlbumComponent],
  exports: [AlbumComponent],
  imports: [PhotoGalleryComponent],
})
export class AlbumModule {}

直接從 Standalone Component 執行應用

我們可以不用透過任何 NgModule 來啟動應用,Angular 提供了 bootstrapApplication API,可以直接從 Standalone Component 啟動:

// in the main.ts file
import { bootstrapApplication } from '@angular/platform-browser';
import { PhotoAppComponent } from './app/photo.app.component';

bootstrapApplication(PhotoAppComponent);

新的 Routing API 簡化 lazy-loading

Angular router API 也更新並簡化,以便利用 Standalone Component,在許多常見 lazy-loading 的情境不再需要 NgModule。例如要 lazy-loading 一個 Component,只需要在 Route.loadComponent 裡面使用 import() 來引入 Component 即可:

export const ROUTES: Route[] = [
  {
    path: 'admin',
    loadComponent: () =>
      import('./admin/panel.component').then((mod) => mod.AdminPanelComponent),
  },
  // ...
];

一次 lazy-loading 多個路由

LoadChildren 現在支援加載一組新的子路由,不需要寫一個 lazy-load 的 NgModule,利用 RouterModule.forChild 宣告路由:

// In the main application:
export const ROUTES: Route[] = [
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/routes').then((mod) => mod.ADMIN_ROUTES),
  },
  // ...
];

// In admin/routes.ts:
export const ADMIN_ROUTES: Route[] = [
  { path: 'home', component: AdminHomeComponent },
  { path: 'users', component: AdminUsersComponent },
  // ...
];

Lazyloading 與 default exports

上面的範例中 ADMIN_ROUTES 也可以改成 export default,如此一來在 loadChildrenloadComponent 都只需要 import('./admin/routes')

// In the main application:
export const ROUTES: Route[] = [
  { path: 'admin', loadChildren: () => import('./admin/routes') },
  // ...
];

// In admin/routes.ts:
export default [
  { path: 'home', component: AdminHomeComponent },
  { path: 'users', component: AdminUsersComponent },
  // ...
] as Route[];

為部分路由提供服務的方法

對於 NgModules 的 lazy-load API(即 loadChildren),在讀取路由的延遲載入子路由時,會創建一個新的模組注入器(Injector)。這個功能經常被用來為特定的路由提供 service。舉個例子,如果把所有 /admin 下的路由都用 loadChildren 來設定範圍,那麼只有這些路由才能獲得針對 Admin 的特定 service。要做到這一點,即使不需要延遲載入相關路由,也需要使用 loadChildren API。

如今,Router 允許在路由上明確指定額外的 Providers,這樣可以在不需要延遲載入或 NgModule 的情況下實現相同的範圍設定。舉例來說,/admin 路由結構內範圍限定的 service 將如下:

export const ROUTES: Route[] = [
  {
    path: 'admin',
    providers: [AdminService, { provide: ADMIN_API_KEY, useValue: '12345' }],
    children: [
      { path: 'users', component: AdminUsersComponent },
      { path: 'teams', component: AdminTeamsComponent },
    ],
  },
  // ... other application routes that don't
  //     have access to ADMIN_API_KEY or AdminService.
];

我們可以將 provider 與額外路由配置的 loadChildren 相結合,以實現延遲載入帶有額外路由和路由級 provider 的 NgModule 的相同效果。這個例子配置了與上述相同的 provider/子路由,但是在延遲載入的邊界之後:

// Main application:
export const ROUTES: Route[] = {
  // Lazy-load the admin routes.
  {path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
  // ... rest of the routes
}

// In admin/routes.ts:
export const ADMIN_ROUTES: Route[] = [{
  path: '',
  pathMatch: 'prefix',
  providers: [
    AdminService,
    {provide: ADMIN_API_KEY, useValue: 12345},
  ],
  children: [
    {path: 'users', component: AdminUsersCmp},
    {path: 'teams', component: AdminTeamsCmp},
  ],
}];

要留意一下空路徑的路由下的 providers 是在所有的子路由共享的。 另外,importProvidersFrom 這個方法可以 import 基於 NgModule 的 DI 注入到 Route 的 providers 中:

export const ROUTES: Route[] = [
  {
    path: 'foo',
    providers: [importProvidersFrom(NgModuleOne, NgModuleTwo)],
    component: YourStandaloneComponent,
  },
];

小結

當專案規模越來越大的時候 NgModule 的管理可能會面臨挑戰,時常要思考是否要建立新的 NgModule,或是這個元件是否會被重覆使用,一不小心就進入了重構地獄。Standalone Coponent 算是把 NgModule 一部分的功能下放到元件的層級,這樣可以避免建立元件時可能的摩擦,並且簡化了學習歷程,同時也可以讓延遲載入變得更容易。未來需要什麼東西就直接 import 到元件中,不需要再去管 NgModule 的事情。

參考資料

Angular Standalone Component
Getting started with Angular Standalone Component