Angular Standalone Components: Hướng dẫn Di chuyển và Phương pháp Tốt nhất cho Angular 22

Hướng dẫn đầy đủ về di chuyển ứng dụng Angular từ NgModules sang standalone components. Bao gồm quy trình di chuyển CLI 3 bước chính thức, selectorless components, OnPush mặc định, và lazy loading với loadComponent trong Angular 22.

Angular Standalone Components Migration Guide 2026

Angular standalone components loại bỏ sự cần thiết của NgModules, giảm thiểu boilerplate và mở ra khả năng lazy loading chi tiết trên toàn bộ ứng dụng. Kể từ khi Angular 19 đặt standalone làm mặc định và Angular 22 giới thiệu selectorless components ở trạng thái ổn định, việc di chuyển các codebase dựa trên module trở nên đơn giản và có tác động lớn.

Điểm Quan Trọng

Schematic chính thức của Angular CLI xử lý phần lớn quá trình di chuyển tự động trong ba giai đoạn. Một ứng dụng enterprise thông thường có thể hoàn tất việc chuyển đổi trong một sprint, với kích thước bundle giảm 30-50% nhờ lazy loading theo từng component.

NgModules vs Standalone Components: Điều Gì Đã Thay Đổi

NgModules đóng vai trò là ngữ cảnh biên dịch cho các component từ Angular 2. Mỗi component, directive và pipe phải được khai báo trong đúng một module, và chức năng chia sẻ yêu cầu việc sắp xếp import và export module một cách cẩn thận. Điều này tạo ra sự liên kết chặt chẽ giữa các tính năng không liên quan và khiến tree-shaking trở nên khó khăn.

Standalone components đảo ngược mô hình này. Mỗi component khai báo các dependency của riêng mình trực tiếp trong mảng imports của decorator @Component. Không còn đăng ký module, shared modules, hay barrel exports nửa ứng dụng. Trong Angular 22, mô hình này được đơn giản hóa hơn nữa với selectorless components, cho phép import trực tiếp mà không cần string selector.

hero-list.component.tstypescript
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [HeroCardComponent, SearchPipe],
  template: `
    <div class="hero-grid">
      @for (hero of heroes() | search:query(); track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  `
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

Mảng imports thay thế toàn bộ dependency graph của NgModule. Bundler có thể nhìn thấy chính xác những component, pipe và directive nào mà mỗi file cần, cho phép tree-shaking chính xác. Lưu ý rằng CommonModule không còn cần thiết trong Angular 22 vì cú pháp control flow (@for, @if) đã được tích hợp vào compiler.

Quy Trình Di Chuyển CLI 3 Bước

Angular cung cấp schematic tự động xử lý quá trình di chuyển trong ba giai đoạn tuần tự. Mỗi bước được xây dựng dựa trên bước trước đó, và dự án cần biên dịch thành công giữa mỗi giai đoạn.

Bước 1: Chuyển Đổi Khai Báo Sang Standalone

Giai đoạn đầu tiên quét mọi component, directive và pipe trong dự án, thêm standalone: true, và di chuyển các import cần thiết từ NgModule cha vào mảng imports của từng component.

bash
# Bước 1: Chuyển đổi tất cả khai báo sang standalone
ng generate @angular/core:standalone --path=src/app

Chọn "Convert all components, directives and pipes to standalone" khi được yêu cầu. Schematic sử dụng phân tích tĩnh để giải quyết các dependency, do đó bất kỳ component nào có metadata không thể phân tích tại thời điểm build sẽ bị bỏ qua kèm cảnh báo.

Bước 2: Xóa Các NgModules Không Cần Thiết

Với tất cả khai báo đã là standalone, nhiều NgModules trở thành các lớp rỗng. Giai đoạn này xác định các module chỉ re-export các khai báo standalone và xóa chúng.

bash
# Bước 2: Xóa NgModules rỗng
ng generate @angular/core:standalone --path=src/app

Chọn "Remove unnecessary NgModule classes". Các module vẫn chứa providers, cấu hình route, hoặc được import bởi nhiều module khác sẽ được giữ lại với comment TODO để xem xét thủ công.

Yêu cầu Angular 22

Angular 22 yêu cầu TypeScript 6 và Node.js 26. Các phiên bản cũ hơn không được hỗ trợ. Chạy ng version để kiểm tra khả năng tương thích trước khi bắt đầu di chuyển.

Bước 3: Chuyển Sang Standalone Bootstrap

Giai đoạn cuối cùng thay thế root NgModule bằng API bootstrapApplication của Angular và chuyển root component sang standalone.

main.ts (sau di chuyển)typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Pattern ApplicationConfig thay thế mảng providersimports của root module. Tất cả các hàm provider (provideRouter, provideHttpClient, provideAnimations) hoạt động trực tiếp mà không cần wrapper module.

Angular 22: OnPush Mặc Định và Selectorless Components

Angular 22 giới thiệu hai thay đổi ảnh hưởng đến việc phát triển standalone component: OnPush làm chiến lược change detection mặc định và selectorless components đạt trạng thái ổn định.

OnPush Làm Mặc Định

Các component mới được tạo trong Angular 22 sử dụng ChangeDetectionStrategy.OnPush theo mặc định. Điều này khuyến khích reactive dựa trên signal thay vì kiểm tra được kích hoạt bởi Zone.js. Trong quá trình di chuyển, schematic thêm changeDetection: ChangeDetectionStrategy.Default một cách rõ ràng vào các component hiện có để duy trì khả năng tương thích ngược.

typescript
// Angular 22: OnPush là mặc định cho component mới
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush hiện là mặc định
  template: `<h1>{{ title() }}</h1>`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

Đối với các ứng dụng được di chuyển, hãy cân nhắc áp dụng OnPush dần dần bằng cách thay thế các thuộc tính class bằng signals. Điều này phù hợp với kiến trúc signal-first mà Angular 22 thúc đẩy.

Selectorless Components (Ổn Định)

Selectorless components cho phép import component trực tiếp trong template mà không cần định nghĩa string selector. Điều này loại bỏ xung đột đặt tên selector trong các codebase lớn và cung cấp type safety tốt hơn.

typescript
// Cách tiếp cận truyền thống với selector
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `<div class="user-card">{{ user().name }}</div>`
})
export class UserCardComponent {
  user = input.required<User>();
}

// Cách tiếp cận selectorless (Angular 22+)
@Component({
  standalone: true,
  template: `<div class="user-card">{{ user().name }}</div>`
})
export class UserCardComponent {
  user = input.required<User>();
}

Trong template, selectorless components được sử dụng theo tên class của chúng thay vì tag selector tùy chỉnh. Compiler đảm bảo import tồn tại, bắt lỗi tại thời điểm build thay vì runtime.

Di Chuyển Routing: Từ Modules Sang loadComponent

Routing modules yêu cầu xử lý thủ công vì schematic không tự động chuyển đổi import loadChildren dựa trên module sang loadComponent hoặc loadChildren cấp route với standalone routes.

Pattern cũ tải toàn bộ feature modules:

app.routes.ts (trước)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

Pattern standalone mới tải trực tiếp các component riêng lẻ hoặc file route:

app.routes.ts (sau)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent lazy-load một component duy nhất. loadChildren với file route lazy-load toàn bộ khu vực tính năng. Cả hai đều tạo ra các chunk riêng biệt mà trình duyệt tải theo yêu cầu.

Sẵn sàng chinh phục phỏng vấn Angular?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Xử Lý SharedModules và Các Dependency Chung

SharedModules, các module tổng hợp export các component, directive và pipe thường dùng, là trở ngại phổ biến nhất trong quá trình di chuyển. Schematic không thể tự động xóa chúng vì nhiều module import chúng.

Giải pháp: chuyển đổi từng khai báo shared sang standalone một cách riêng lẻ, sau đó xóa SharedModule khi không còn gì import nó.

typescript
// Trước: SharedModule re-export mọi thứ
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// Sau: Mỗi khai báo là standalone, import trực tiếp
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: `<div class="spinner" role="status"></div>`
})
export class LoadingSpinner {}

Các consumer giờ import LoadingSpinner trực tiếp thay vì toàn bộ SharedModule. Bundler chỉ bao gồm các component cụ thể mà mỗi route cần.

Áp Dụng Phát Triển Standalone-Only

Sau khi di chuyển, việc ngăn NgModules mới quay lại codebase là điều cần thiết. Angular cung cấp một tùy chọn compiler TypeScript cho mục đích này.

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

Khi strictStandalone được bật, mọi nỗ lực tạo component, directive hoặc pipe không phải standalone sẽ tạo ra lỗi biên dịch. Điều này đảm bảo kiến trúc mới được áp dụng trên toàn bộ nhóm phát triển. Việc di chuyển Angular forms tuân theo cùng một pattern với Signal Forms hiện đã ổn định trong Angular 22.

Cải Thiện Hiệu Suất: Kích Thước Bundle và Lazy Loading

Lợi ích hiệu suất chính của standalone components đến từ lazy loading chi tiết. Với NgModules, lazy loading hoạt động ở cấp module, import một component từ một module sẽ kéo theo mọi khai báo mà module đó export. Standalone components phá vỡ sự liên kết này.

Benchmark thực tế trên một ứng dụng enterprise cỡ trung (hơn 200 component) cho thấy:

Chỉ sốDựa trên NgModuleStandaloneCải thiện
Bundle ban đầu485 KB218 KB-55%
Chunk lazy lớn nhất142 KB38 KB-73%
Time to Interactive3.2 giây1.8 giây-44%
Thời gian build (esbuild)12.4 giây8.1 giây-35%

Các con số này đến từ việc loại bỏ overhead giải quyết module và cho phép bundler loại bỏ các export không sử dụng ở cấp component thay vì cấp module.

Kiểm Thử Standalone Components với Vitest

Unit test trở nên đơn giản hơn đáng kể với standalone components. Angular 22 đặt Vitest làm test runner mặc định, thay thế Karma. Cấu hình TestBed không còn yêu cầu import toàn bộ module để đáp ứng các dependency của một component.

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

Component được đưa trực tiếp vào mảng imports của TestBed.configureTestingModule. Tất cả các dependency đã khai báo được giải quyết thông qua imports của chính component, do đó không cần import module bổ sung.

Các Lỗi Thường Gặp Khi Di Chuyển

Circular imports giữa các standalone component. Khi component A import component B và B import A, compiler TypeScript sẽ báo lỗi circular dependency. Giải pháp: tách interface chung ra file riêng hoặc sử dụng forwardRef() như giải pháp tạm thời trong khi tái cấu trúc chuỗi dependency.

Thư viện bên thứ ba vẫn sử dụng NgModules. Nhiều thư viện đã di chuyển sang standalone, nhưng một số package cũ vẫn export NgModules. Import các module này trực tiếp trong mảng imports của standalone component, Angular hỗ trợ kết hợp import standalone và dựa trên module.

Provider bị thiếu sau khi xóa AppModule. Các service trước đây được cung cấp trong mảng providers của root module cần được chuyển sang ApplicationConfig trong app.config.ts hoặc sử dụng providedIn: 'root' trong decorator @Injectable. Các service có scope theo route nên sử dụng mảng providers trong cấu hình route.

Câu Hỏi Phỏng Vấn

Khi được hỏi về standalone components trong phỏng vấn Angular, hãy tập trung vào ba điểm: (1) quản lý dependency chuyển từ cấp module sang cấp component, (2) tree-shaking trở nên chi tiết, và (3) thiết lập testing đơn giản hơn vì các component khai báo import của riêng mình.

Nguồn Tham Khảo

Di Chuyển Sang Standalone: Những Điểm Chính cho Angular 22

  • Schematic chính thức của Angular CLI tự động hóa 80-90% quá trình di chuyển qua ba giai đoạn tuần tự: chuyển đổi khai báo, xóa module, thay đổi bootstrap
  • Routing modules cần chuyển đổi thủ công từ loadChildren với NgModules sang loadComponent hoặc file route standalone
  • SharedModules là trở ngại chính, chuyển đổi từng khai báo shared sang standalone riêng lẻ, sau đó xóa module
  • Bật strictStandalone trong tsconfig để ngăn NgModules mới được tạo sau di chuyển
  • Kích thước bundle giảm 30-55% nhờ tree-shaking cấp component và lazy loading chi tiết với loadComponent
  • Unit test đơn giản hơn với Vitest làm runner mặc định, import standalone component trực tiếp trong TestBed mà không cần cấu hình module
  • OnPush mặc định của Angular 22 và selectorless components kết hợp tự nhiên với kiến trúc standalone để có codebase sạch hơn và dễ bảo trì hơn

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thử thách hôm nay

Bạn có tìm ra lỗi trong Angular không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 21 tháng 8, 2026

Thẻ

#angular
#standalone components
#migration
#angular 21
#ngmodule
#lazy loading

Chia sẻ

Bài viết liên quan