Angular Standalone Components: คู่มือการย้ายระบบและแนวทางปฏิบัติที่ดีที่สุดสำหรับ Angular 22
คู่มือฉบับสมบูรณ์สำหรับการย้ายแอปพลิเคชัน Angular จาก NgModules ไปยัง standalone components ครอบคลุมการย้ายระบบด้วย CLI 3 ขั้นตอนอย่างเป็นทางการ, selectorless components, OnPush เป็นค่าเริ่มต้น และ lazy loading ด้วย loadComponent ใน Angular 22

Angular standalone components ช่วยขจัดความจำเป็นในการใช้ NgModules ลดโค้ดที่ซ้ำซ้อน และเปิดโอกาสให้สามารถทำ lazy loading แบบละเอียดได้ทั่วทั้งแอปพลิเคชัน นับตั้งแต่ Angular 19 กำหนดให้ standalone เป็นค่าเริ่มต้น และ Angular 22 นำ selectorless components มาใช้อย่างเสถียร การย้ายระบบจาก 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 ของครึ่งแอปพลิเคชัน ใน Angular 22 รูปแบบนี้ถูกทำให้ง่ายขึ้นด้วย selectorless components ที่อนุญาตให้ import โดยตรงโดยไม่ต้องใช้ string selector
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('');
}Array imports เข้ามาแทนที่ dependency graph ทั้งหมดของ NgModule bundler สามารถเห็นได้อย่างชัดเจนว่าแต่ละไฟล์ต้องการ component, pipe และ directive ใดบ้าง ทำให้สามารถทำ tree-shaking ได้อย่างแม่นยำ โปรดทราบว่า CommonModule ไม่จำเป็นอีกต่อไปใน Angular 22 เนื่องจาก control flow syntax (@for, @if) ถูกสร้างไว้ในตัว compiler แล้ว
กระบวนการย้ายระบบด้วย CLI 3 ขั้นตอน
Angular มี schematic อัตโนมัติที่จัดการการย้ายระบบในสามขั้นตอนตามลำดับ แต่ละขั้นตอนสร้างต่อจากขั้นตอนก่อนหน้า และโปรเจกต์ควรคอมไพล์ได้สำเร็จระหว่างแต่ละขั้นตอน
ขั้นตอนที่ 1: แปลง Declarations เป็น Standalone
ขั้นตอนแรกจะสแกนทุก component, directive และ pipe ในโปรเจกต์ เพิ่ม standalone: true และย้าย import ที่จำเป็นจาก NgModule แม่ไปยัง array imports ของแต่ละ component
# ขั้นตอนที่ 1: แปลง declarations ทั้งหมดเป็น standalone
ng generate @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 และลบออก
# ขั้นตอนที่ 2: ลบ NgModules ที่ว่างเปล่า
ng generate @angular/core:standalone --path=src/appเลือก "Remove unnecessary NgModule classes" module ที่ยังมี providers, การกำหนดค่า route หรือถูก import โดยหลาย module จะถูกเก็บไว้พร้อม comment TODO สำหรับการตรวจสอบด้วยตนเอง
Angular 22 ต้องการ TypeScript 6 และ Node.js 26 เวอร์ชันเก่ากว่าไม่รองรับ รัน ng version เพื่อตรวจสอบความเข้ากันได้ก่อนเริ่มการย้ายระบบ
ขั้นตอนที่ 3: เปลี่ยนไปใช้ Standalone Bootstrap
ขั้นตอนสุดท้ายจะแทนที่ root NgModule ด้วย API bootstrapApplication ของ Angular และแปลง root component เป็น standalone
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));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]))
]
};รูปแบบ ApplicationConfig เข้ามาแทนที่ array providers และ imports ของ root module ฟังก์ชัน provider ทั้งหมด (provideRouter, provideHttpClient, provideAnimations) ทำงานได้โดยตรงโดยไม่ต้องมี wrapper ของ module
Angular 22: OnPush เป็นค่าเริ่มต้นและ Selectorless Components
Angular 22 แนะนำการเปลี่ยนแปลงสองอย่างที่ส่งผลกระทบต่อการพัฒนา standalone component: OnPush เป็นกลยุทธ์ change detection เริ่มต้น และ selectorless components ถึงสถานะเสถียร
OnPush เป็นค่าเริ่มต้น
Component ใหม่ที่สร้างใน Angular 22 ใช้ ChangeDetectionStrategy.OnPush โดยเป็นค่าเริ่มต้น สิ่งนี้ส่งเสริม reactivity แบบ signal แทนการตรวจสอบที่ถูกกระตุ้นโดย Zone.js ระหว่างการย้ายระบบ schematic จะเพิ่ม changeDetection: ChangeDetectionStrategy.Default อย่างชัดเจนให้กับ component ที่มีอยู่เพื่อรักษาความเข้ากันได้แบบย้อนหลัง
// Angular 22: OnPush เป็นค่าเริ่มต้นสำหรับ component ใหม่
@Component({
selector: 'app-dashboard',
standalone: true,
// changeDetection: ChangeDetectionStrategy.OnPush เป็นค่าเริ่มต้นแล้ว
template: `<h1>{{ title() }}</h1>`
})
export class DashboardComponent {
title = signal('Dashboard');
}สำหรับแอปพลิเคชันที่ย้ายระบบแล้ว ควรพิจารณานำ OnPush มาใช้อย่างค่อยเป็นค่อยไปโดยแทนที่คุณสมบัติ class ด้วย signals ซึ่งสอดคล้องกับ สถาปัตยกรรม signal-first ที่ Angular 22 ส่งเสริม
Selectorless Components (เสถียร)
Selectorless components อนุญาตให้ import component โดยตรงใน template โดยไม่ต้องกำหนด string selector สิ่งนี้ขจัดความขัดแย้งในการตั้งชื่อ selector ใน codebase ขนาดใหญ่และให้ type safety ที่ดีกว่า
// แนวทางแบบดั้งเดิมที่มี selector
@Component({
selector: 'app-user-card',
standalone: true,
template: `<div class="user-card">{{ user().name }}</div>`
})
export class UserCardComponent {
user = input.required<User>();
}
// แนวทาง selectorless (Angular 22+)
@Component({
standalone: true,
template: `<div class="user-card">{{ user().name }}</div>`
})
export class UserCardComponent {
user = input.required<User>();
}ใน template, selectorless components ถูกใช้โดยชื่อ class แทนที่จะเป็น tag selector ที่กำหนดเอง compiler ตรวจสอบให้แน่ใจว่ามี import อยู่ จับ error ในเวลา build แทนที่จะเป็น runtime
การย้ายระบบ Routing: จาก Modules สู่ loadComponent
Routing modules ต้องการการจัดการด้วยตนเอง เนื่องจาก schematic ไม่ได้แปลง import loadChildren แบบ module เป็น loadComponent หรือ loadChildren ระดับ route พร้อม standalone routes โดยอัตโนมัติ
รูปแบบเดิมจะโหลดทั้ง feature modules:
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];รูปแบบ standalone ใหม่จะโหลด component แต่ละตัวหรือไฟล์ route โดยตรง:
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)
}
];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 อีกต่อไป
// ก่อน: 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 สำหรับจุดประสงค์นี้
{
"angularCompilerOptions": {
"strictStandalone": true
}
}เมื่อเปิดใช้งาน strictStandalone ความพยายามใดก็ตามที่จะสร้าง component, directive หรือ pipe ที่ไม่ใช่ standalone จะเกิดข้อผิดพลาดในการคอมไพล์ สิ่งนี้ช่วยบังคับให้สถาปัตยกรรมใหม่ถูกนำไปใช้ทั่วทั้งทีม การย้ายระบบ Angular forms ปฏิบัติตามรูปแบบเดียวกันโดย Signal Forms เสถียรแล้วใน Angular 22
การปรับปรุงประสิทธิภาพ: ขนาด 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 ด้วย Vitest
Unit test ง่ายขึ้นอย่างมากกับ standalone components Angular 22 กำหนดให้ Vitest เป็น test runner เริ่มต้น แทนที่ Karma การกำหนดค่า TestBed ไม่ต้อง import ทั้ง module เพื่อตอบสนอง dependency ของ component อีกต่อไป
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 ถูกใส่โดยตรงใน 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
เมื่อถูกถามเกี่ยวกับ standalone components ในการสัมภาษณ์งาน Angular ให้เน้นสามจุด: (1) การจัดการ dependency ย้ายจากระดับ module ไปยังระดับ component, (2) tree-shaking กลายเป็นแบบละเอียด, และ (3) การตั้งค่า testing ง่ายขึ้นเพราะ component ประกาศ import ของตัวเอง
แหล่งอ้างอิง
- Angular v22 Release Notes - OnPush เป็นค่าเริ่มต้น, selectorless เสถียร, ต้องการ TypeScript 6
- Angular Standalone Migration Guide - เอกสาร schematic 3 ขั้นตอนอย่างเป็นทางการ
- What's New in Angular 22 - Changelog โดยละเอียดรวมถึงการเปลี่ยนแปลงกลยุทธ์ change detection
- Selectorless Components RFC - เหตุผลในการออกแบบและสถานะการดำเนินการ
การย้ายไปยัง Standalone: ประเด็นสำคัญสำหรับ Angular 22
- 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 ง่ายขึ้นด้วย Vitest เป็น runner เริ่มต้น สามารถ import standalone component โดยตรงใน
TestBedโดยไม่ต้องกำหนดค่า module - OnPush เป็นค่าเริ่มต้นของ Angular 22 และ selectorless components เข้ากันได้อย่างเป็นธรรมชาติกับสถาปัตยกรรม standalone สำหรับ codebase ที่สะอาดและบำรุงรักษาง่ายกว่า
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
คุณหาบั๊กใน Angular เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 21 สิงหาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Angular @defer ในปี 2026: Declarative Lazy Loading สำหรับแอปพลิเคชันที่เร็วขึ้น
เจาะลึก Angular @defer blocks สำหรับ declarative lazy loading พร้อม triggers ทุกประเภท prefetching, incremental hydration, พฤติกรรมบน SSR และรูปแบบการใช้งานจริงเพื่อเพิ่มประสิทธิภาพ

Dependency Injection ขั้นสูงใน Angular 2026: Providers, Tokens และคำถามสัมภาษณ์
เรียนรู้ระบบ dependency injection ของ Angular อย่างลึกซึ้ง รวมถึงกลยุทธ์ provider, InjectionToken, injector แบบลำดับชั้น และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Angular ที่มีประสบการณ์

ไวยากรณ์ Control Flow ใน Angular 2026: @if, @for, @switch และคำถามสัมภาษณ์
เรียนรู้ไวยากรณ์ control flow ของ Angular (@if, @for, @switch) ที่มาแทนที่ structural directive ครบถ้วนพร้อมตัวอย่างโค้ด เคล็ดลับการ migrate และคำถามสัมภาษณ์เทคนิค