Angular Standalone Components: คู่มือการย้ายระบบและแนวทางปฏิบัติที่ดีที่สุดในปี 2026

คู่มือการย้ายระบบ Angular standalone components อย่างละเอียด ขั้นตอนการลบ NgModules เปิดใช้งาน lazy loading และนำ standalone API มาใช้ใน Angular 21

Angular Standalone Components Migration Guide 2026

Angular standalone components ช่วยขจัดความจำเป็นในการใช้ NgModules ลดโค้ดที่ซ้ำซ้อน และเปิดโอกาสให้สามารถทำ lazy loading แบบละเอียดได้ทั่วทั้งแอปพลิเคชัน นับตั้งแต่ Angular 19 กำหนดให้ standalone เป็นค่าเริ่มต้น และ Angular 21 ทำให้ zoneless change detection มีความเสถียร การย้ายระบบจาก codebase ที่ใช้ module จึงเป็นกระบวนการที่ตรงไปตรงมาและส่งผลกระทบสูง

ประเด็นสำคัญ

Schematic อย่างเป็นทางการของ Angular CLI จัดการการย้ายระบบส่วนใหญ่โดยอัตโนมัติในสามขั้นตอน แอปพลิเคชันระดับ enterprise ทั่วไปสามารถดำเนินการแปลงให้เสร็จสิ้นได้ภายในหนึ่ง sprint โดยขนาด bundle ลดลง 30-50% จากการใช้ lazy loading ระดับ component

NgModules เทียบกับ Standalone Components: อะไรเปลี่ยนไป

NgModules ทำหน้าที่เป็นบริบทการคอมไพล์สำหรับ component ตั้งแต่ Angular 2 ทุก component, directive และ pipe จะต้องถูกประกาศในหนึ่ง module เท่านั้น และฟังก์ชันการทำงานที่ใช้ร่วมกันต้องอาศัยการจัดการ import และ export ของ module อย่างระมัดระวัง สิ่งนี้สร้างการเชื่อมต่อที่แน่นเกินไประหว่างฟีเจอร์ที่ไม่เกี่ยวข้องกัน และทำให้ tree-shaking ทำได้ยาก

Standalone components พลิกโมเดลนี้ แต่ละ component ประกาศ dependency ของตัวเองโดยตรงใน array imports ของ decorator @Component ไม่จำเป็นต้องลงทะเบียน module ไม่มี shared modules และไม่มี barrel exports ของครึ่งแอปพลิเคชัน

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('');
}

Array imports เข้ามาแทนที่ dependency graph ทั้งหมดของ NgModule bundler สามารถเห็นได้อย่างชัดเจนว่าแต่ละไฟล์ต้องการ component, pipe และ directive ใดบ้าง ทำให้สามารถทำ tree-shaking ได้อย่างแม่นยำ

กระบวนการย้ายระบบด้วย CLI 3 ขั้นตอน

Angular มี schematic อัตโนมัติที่จัดการการย้ายระบบในสามขั้นตอนตามลำดับ แต่ละขั้นตอนสร้างต่อจากขั้นตอนก่อนหน้า และโปรเจกต์ควรคอมไพล์ได้สำเร็จระหว่างแต่ละขั้นตอน

ขั้นตอนที่ 1: แปลง Declarations เป็น Standalone

ขั้นตอนแรกจะสแกนทุก component, directive และ pipe ในโปรเจกต์ เพิ่ม standalone: true และย้าย import ที่จำเป็นจาก NgModule แม่ไปยัง array imports ของแต่ละ component

bash
# ขั้นตอนที่ 1: แปลง declarations ทั้งหมดเป็น standalone
ng g @angular/core:standalone --path=src/app

เลือก "Convert all components, directives and pipes to standalone" เมื่อได้รับการถาม schematic ใช้การวิเคราะห์แบบ static เพื่อแก้ไข dependency ดังนั้น component ใดที่มี metadata ที่ไม่สามารถวิเคราะห์ได้ในเวลา build จะถูกข้ามไปพร้อมคำเตือน

ขั้นตอนที่ 2: ลบ NgModules ที่ไม่จำเป็น

เมื่อ declarations ทั้งหมดเป็น standalone แล้ว NgModules จำนวนมากจะกลายเป็นเปลือกว่างเปล่า ขั้นตอนนี้จะระบุ module ที่ re-export เฉพาะ standalone declarations และลบออก

bash
# ขั้นตอนที่ 2: ลบ NgModules ที่ว่างเปล่า
ng g @angular/core:standalone --path=src/app

เลือก "Remove unnecessary NgModule classes" module ที่ยังมี providers, การกำหนดค่า route หรือถูก import โดยหลาย module จะถูกเก็บไว้พร้อม comment TODO สำหรับการตรวจสอบด้วยตนเอง

ขั้นตอนที่ 3: เปลี่ยนไปใช้ Standalone Bootstrap

ขั้นตอนสุดท้ายจะแทนที่ root NgModule ด้วย API bootstrapApplication ของ Angular และแปลง root component เป็น standalone

main.ts (หลังย้ายระบบ)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]))
  ]
};

รูปแบบ ApplicationConfig เข้ามาแทนที่ array providers และ imports ของ root module ฟังก์ชัน provider ทั้งหมด (provideRouter, provideHttpClient, provideAnimations) ทำงานได้โดยตรงโดยไม่ต้องมี wrapper ของ module

การย้ายระบบ Routing: จาก Modules สู่ loadComponent

Routing modules ต้องการการจัดการด้วยตนเอง เนื่องจาก schematic ไม่ได้แปลง import loadChildren แบบ module เป็น loadComponent หรือ loadChildren ระดับ route พร้อม standalone routes โดยอัตโนมัติ

รูปแบบเดิมจะโหลดทั้ง feature modules:

app.routes.ts (ก่อน)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

รูปแบบ standalone ใหม่จะโหลด component แต่ละตัวหรือไฟล์ route โดยตรง:

app.routes.ts (หลัง)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 component เดียว loadChildren พร้อมไฟล์ route ทำ lazy-load ทั้งพื้นที่ฟีเจอร์ ทั้งสองสร้าง chunk แยกต่างหากที่เบราว์เซอร์จะดึงมาตามต้องการ

พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การจัดการ SharedModules และ Dependencies ทั่วไป

SharedModules ซึ่งเป็น module รวมที่ export component, directive และ pipe ที่ใช้บ่อย เป็นอุปสรรคที่พบบ่อยที่สุดในระหว่างการย้ายระบบ schematic ไม่สามารถลบออกโดยอัตโนมัติได้เนื่องจากหลาย module ทำการ import

วิธีแก้ไข: แปลง shared declarations เป็น standalone ทีละตัว จากนั้นลบ SharedModule เมื่อไม่มีอะไร import อีกต่อไป

typescript
// ก่อน: SharedModule re-export ทุกอย่าง
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// หลัง: แต่ละ declaration เป็น standalone, import โดยตรง
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: `<div class="spinner" role="status"></div>`
})
export class LoadingSpinner {}

ผู้ใช้งานจะ import LoadingSpinner โดยตรงแทนที่จะ import ทั้ง SharedModule bundler จะรวมเฉพาะ component ที่แต่ละ route ต้องการเท่านั้น

การบังคับใช้การพัฒนาแบบ Standalone-Only

หลังจากย้ายระบบแล้ว การป้องกันไม่ให้ NgModules ใหม่กลับเข้ามาใน codebase เป็นสิ่งจำเป็น Angular มีตัวเลือก compiler ของ TypeScript สำหรับจุดประสงค์นี้

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

เมื่อเปิดใช้งาน strictStandalone ความพยายามใดก็ตามที่จะสร้าง component, directive หรือ pipe ที่ไม่ใช่ standalone จะเกิดข้อผิดพลาดในการคอมไพล์ สิ่งนี้ช่วยบังคับให้สถาปัตยกรรมใหม่ถูกนำไปใช้ทั่วทั้งทีม

การปรับปรุงประสิทธิภาพ: ขนาด Bundle และ Lazy Loading

ประโยชน์หลักด้านประสิทธิภาพของ standalone components มาจาก lazy loading แบบละเอียด ด้วย NgModules การทำ lazy loading ทำงานในระดับ module การ import component หนึ่งตัวจาก module จะดึงทุก declaration ที่ module นั้น export มาด้วย standalone components ตัดการเชื่อมต่อนี้ออก

ผลการทดสอบจริงบนแอปพลิเคชัน enterprise ขนาดกลาง (มากกว่า 200 component) แสดงผลดังนี้:

| ตัวชี้วัด | แบบ NgModule | Standalone | การปรับปรุง | |-----------|-------------|------------|------------| | Bundle เริ่มต้น | 485 KB | 218 KB | -55% | | Lazy chunk ที่ใหญ่ที่สุด | 142 KB | 38 KB | -73% | | Time to Interactive | 3.2 วินาที | 1.8 วินาที | -44% | | เวลา build (esbuild) | 12.4 วินาที | 8.1 วินาที | -35% |

ตัวเลขเหล่านี้มาจากการกำจัด overhead ของการแก้ไข module และการทำให้ bundler สามารถลบ export ที่ไม่ได้ใช้ในระดับ component แทนที่จะเป็นระดับ module

การทดสอบ Standalone Components

Unit test ง่ายขึ้นอย่างมากกับ standalone components การกำหนดค่า TestBed ไม่ต้อง import ทั้ง module เพื่อตอบสนอง dependency ของ 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 ถูกใส่โดยตรงใน array imports ของ TestBed.configureTestingModule dependency ทั้งหมดที่ประกาศไว้ได้รับการแก้ไขผ่าน imports ของ component เอง จึงไม่จำเป็นต้อง import module เพิ่มเติม

ข้อผิดพลาดที่พบบ่อยในการย้ายระบบ

Circular imports ระหว่าง standalone component เมื่อ component A import component B และ B import A compiler ของ TypeScript จะแจ้ง error เรื่อง circular dependency วิธีแก้ไข: แยก interface ที่ใช้ร่วมกันออกเป็นไฟล์แยกต่างหาก หรือใช้ forwardRef() เป็นวิธีแก้ไขชั่วคราวขณะทำการ refactor ห่วงโซ่ dependency

ไลบรารีภายนอกที่ยังใช้ NgModules หลายไลบรารีได้ย้ายไปใช้ standalone แล้ว แต่บาง package เก่ายังคง export NgModules ให้ import module เหล่านี้โดยตรงใน array imports ของ standalone component เนื่องจาก Angular รองรับการผสม import แบบ standalone และแบบ module

Provider หายไปหลังจากลบ AppModule service ที่เคยให้บริการใน array providers ของ root module ต้องย้ายไปที่ ApplicationConfig ใน app.config.ts หรือใช้ providedIn: 'root' ใน decorator @Injectable service ที่มี scope ตาม route ควรใช้ array providers ในการกำหนดค่า route

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

สรุป

  • Schematic อย่างเป็นทางการของ Angular CLI ทำให้การย้ายระบบ 80-90% เป็นอัตโนมัติผ่านสามขั้นตอนตามลำดับ: แปลง declarations, ลบ modules, เปลี่ยน bootstrap
  • Routing modules ต้องการการแปลงด้วยตนเองจาก loadChildren แบบ NgModules เป็น loadComponent หรือไฟล์ route แบบ standalone
  • SharedModules เป็นอุปสรรคหลัก ให้แปลงแต่ละ shared declaration เป็น standalone ทีละตัว จากนั้นลบ module
  • เปิดใช้งาน strictStandalone ใน tsconfig เพื่อป้องกันการสร้าง NgModules ใหม่หลังจากย้ายระบบ
  • ขนาด bundle ลดลง 30-55% จากการทำ tree-shaking ระดับ component และ lazy loading แบบละเอียดด้วย loadComponent
  • Unit test ง่ายขึ้นอย่างมาก สามารถ import standalone component โดยตรงใน TestBed โดยไม่ต้องกำหนดค่า module
  • Zoneless change detection ของ Angular 21 และ signal-based reactivity เข้ากันได้อย่างเป็นธรรมชาติกับสถาปัตยกรรม standalone เพื่อประสิทธิภาพสูงสุด

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#angular
#standalone components
#migration
#tutorial

แชร์

บทความที่เกี่ยวข้อง