Angular Standalone Components: Huong dan di chuyen va cac phuong phap tot nhat 2026

Huong dan chi tiet di chuyen Angular standalone components. Quy trinh tung buoc xoa NgModules, kich hoat lazy loading va ap dung standalone API trong Angular 21.

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 21 hoàn thiện zoneless change detection, 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.

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

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [CommonModule, 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.

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 g @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 g @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.

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 } 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: [
    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.

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.

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 NgModule | Standalone | Cải thiện | |--------|-------------------|------------|----------| | Bundle ban đầu | 485 KB | 218 KB | -55% | | Chunk lazy lớn nhất | 142 KB | 38 KB | -73% | | Time to Interactive | 3.2 giây | 1.8 giây | -44% | | Thời gian build (esbuild) | 12.4 giây | 8.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

Unit test trở nên đơn giản hơn đáng kể với standalone components. 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';

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.

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.

Kết Luận

  • 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 đáng kể — import standalone component trực tiếp trong TestBed mà không cần cấu hình module
  • Zoneless change detection của Angular 21 và signal-based reactivity kết hợp tự nhiên với kiến trúc standalone để đạt hiệu suất tối đa

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ẻ

#angular
#standalone components
#migration
#tutorial

Chia sẻ

Bài viết liên quan