Angular Standalone Components: Міграція та найкращі практики для Angular 22

Повний посібник з міграції Angular-додатків з NgModules на standalone-компоненти. Охоплює офіційний 3-кроковий процес CLI, безселекторні компоненти, OnPush за замовчуванням та lazy loading з loadComponent в Angular 22.

Angular Standalone Components міграція та найкращі практики

Standalone-компоненти Angular усувають потребу в NgModules, зменшуючи обсяг шаблонного коду та відкриваючи можливості для детального lazy loading по всьому додатку. Відтоді як Angular 19 зробив standalone режимом за замовчуванням, а Angular 22 представив стабільні безселекторні компоненти, міграція застарілих модульних кодових баз стала водночас простою та високоефективною.

Ключовий висновок

Офіційна схема Angular CLI автоматично обробляє більшу частину міграції за три проходи. Типовий корпоративний додаток може завершити конверсію за один спринт, а розміри бандлів зменшуються на 30-50% завдяки lazy loading на рівні компонентів.

NgModules проти standalone-компонентів: що змінилося

NgModules виконували роль контексту компіляції для компонентів починаючи з Angular 2. Кожен компонент, директива та pipe мали бути оголошені рівно в одному модулі, а спільна функціональність вимагала ретельно організованих імпортів та експортів модулів. Це створювало тісний зв'язок між непов'язаними функціями та ускладнювало tree-shaking.

Standalone-компоненти перевертають цю модель. Кожен компонент оголошує власні залежності безпосередньо в масиві imports декоратора @Component. Без реєстрації в модулі, без спільних модулів, без barrel-експортів половини додатку. Починаючи з Angular 22, цей патерн ще більше спрощений завдяки безселекторним компонентам, які дозволяють прямий імпорт без рядкових селекторів.

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

Масив imports замінює весь граф залежностей NgModule. Бандлер бачить точно, які компоненти, pipe та директиви потрібні кожному файлу, що уможливлює точний tree-shaking. CommonModule більше не потрібен в Angular 22, оскільки синтаксис control flow (@for, @if) вбудований в компілятор.

3-кроковий процес міграції CLI

Angular надає автоматизовану схему, яка обробляє міграцію в трьох послідовних проходах. Кожен крок базується на попередньому, і проєкт повинен компілюватися чисто між кожним проходом.

Крок 1: Конвертація оголошень у standalone

Перший прохід сканує кожен компонент, директиву та pipe в проєкті, додає standalone: true і переносить необхідні імпорти з батьківського NgModule у власний масив imports кожного компонента.

bash
# Крок 1: Конвертація всіх оголошень у standalone
ng generate @angular/core:standalone --path=src/app

Оберіть "Convert all components, directives and pipes to standalone" при запиті. Схема використовує статичний аналіз для вирішення залежностей, тому будь-який компонент з метаданими, які неможливо проаналізувати під час збірки, буде пропущений з попередженням.

Крок 2: Видалення непотрібних NgModules

Коли всі оголошення вже standalone, багато NgModules стають порожніми оболонками. Цей прохід ідентифікує модулі, які лише реекспортували standalone-оголошення, і видаляє їх.

bash
# Крок 2: Видалення порожніх NgModules
ng generate @angular/core:standalone --path=src/app

Оберіть "Remove unnecessary NgModule classes". Модулі, які все ще містять провайдери, конфігурації маршрутів або імпортуються кількома іншими модулями, будуть збережені з коментарем TODO для ручного перегляду.

Вимоги Angular 22

Angular 22 вимагає TypeScript 6 та Node.js 26. Старіші версії не підтримуються. Перед початком міграції запустіть ng version, щоб перевірити сумісність.

Крок 3: Перехід на standalone bootstrap

Останній прохід замінює кореневий NgModule на API bootstrapApplication Angular і конвертує кореневий компонент у 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, 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 замінює масиви providers та imports кореневого модуля. Всі функції-провайдери (provideRouter, provideHttpClient, provideAnimations) працюють безпосередньо без модульних обгорток.

Angular 22: OnPush за замовчуванням та безселекторні компоненти

Angular 22 представив дві зміни, що впливають на розробку standalone-компонентів: OnPush як стратегія виявлення змін за замовчуванням та стабільний статус безселекторних компонентів.

OnPush за замовчуванням

Нові компоненти, згенеровані в Angular 22, використовують ChangeDetectionStrategy.OnPush за замовчуванням. Це заохочує реактивність на основі сигналів замість перевірок, ініційованих Zone.js. Під час міграції схема додає явне changeDetection: ChangeDetectionStrategy.Default до існуючих компонентів для збереження зворотної сумісності.

typescript
// Angular 22: OnPush за замовчуванням для нових компонентів
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush тепер за замовчуванням
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

Для мігрованих додатків розгляньте поступове впровадження OnPush шляхом заміни властивостей класу на сигнали. Це узгоджується з архітектурою signal-first, яку просуває Angular 22.

Безселекторні компоненти (стабільні)

Безселекторні компоненти дозволяють імпортувати компоненти безпосередньо в шаблонах без визначення рядкових селекторів. Це усуває конфлікти імен селекторів у великих кодових базах і забезпечує кращу типову безпеку.

typescript
// Традиційний підхід з селектором
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// Безселекторний підхід (Angular 22+)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

У шаблонах безселекторні компоненти використовуються за їх ім'ям класу, а не за тегом спеціального селектора. Компілятор гарантує наявність імпорту, виявляючи помилки під час компіляції, а не під час виконання.

Міграція маршрутизації: від модулів до loadComponent

Модулі маршрутизації потребують ручної уваги, оскільки схема не конвертує автоматично імпорти loadChildren з модулів на loadComponent або loadChildren на рівні маршрутів зі standalone-маршрутами.

Старий патерн завантажував цілі функціональні модулі:

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

Standalone-еквівалент завантажує окремі компоненти або файли маршрутів безпосередньо:

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 ліниво завантажує окремий компонент. loadChildren з файлом маршрутів ліниво завантажує цілу функціональну область. Обидва варіанти створюють окремі chunk-и, які браузер отримує за запитом.

Готовий до співбесід з Angular?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Робота з SharedModules та спільними залежностями

SharedModules, універсальні модулі, що експортують широко використовувані компоненти, директиви та pipe, є найпоширенішою перешкодою під час міграції. Схема не може їх автоматично видалити, оскільки їх імпортують кілька модулів.

Рішення: конвертувати спільні оголошення в standalone по одному, а потім видалити SharedModule, коли нічого більше його не імпортує.

typescript
// До: SharedModule реекспортує все
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// Після: Кожне оголошення standalone, імпортуйте безпосередньо
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}

Споживачі тепер імпортують LoadingSpinner безпосередньо замість цілого SharedModule. Бандлер включає лише ті компоненти, які потрібні кожному маршруту.

Примусове використання виключно standalone

Після міграції запобігання появі нових NgModules у кодовій базі є критично важливим. Angular надає опцію компілятора TypeScript для цього.

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

З увімкненим strictStandalone будь-яка спроба створити не-standalone компонент, директиву або pipe призводить до помилки компіляції. Це забезпечує дотримання нової архітектури у всій команді. Міграція форм Angular слідує тому ж патерну, при цьому Signal Forms є стабільними в Angular 22.

Приріст продуктивності: розмір бандлів та lazy loading

Основна перевага standalone-компонентів у продуктивності полягає в детальному lazy loading. З NgModules lazy loading працював на рівні модуля: імпорт одного компонента з модуля тягнув за собою кожне оголошення, яке цей модуль експортував. Standalone-компоненти розривають цей зв'язок.

Реальний бенчмарк на середньому корпоративному додатку (200+ компонентів) показав:

МетрикаНа базі NgModuleStandaloneПокращення
Початковий бандл485 КБ218 КБ-55%
Найбільший лінивий chunk142 КБ38 КБ-73%
Час до інтерактивності3,2с1,8с-44%
Час збірки (esbuild)12,4с8,1с-35%

Ці показники є результатом усунення накладних витрат на розв'язання модулів та надання бандлеру можливості елімінувати невикористані експорти на рівні компонентів, а не модулів.

Тестування standalone-компонентів з Vitest

Юніт-тести значно спрощуються зі standalone-компонентами. Angular 22 зробив Vitest test runner за замовчуванням, замінивши Karma. Конфігурація TestBed більше не вимагає імпорту цілих модулів для задоволення залежностей компонента.

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

Компонент потрапляє безпосередньо в масив imports TestBed.configureTestingModule. Усі оголошені залежності вже розв'язані через власні imports компонента, тому додаткові імпорти модулів не потрібні.

Типові пастки міграції

Циклічні імпорти між standalone-компонентами. Коли компонент A імпортує компонент B, а B імпортує A, компілятор TypeScript видає помилку циклічної залежності. Рішення: виділити спільний інтерфейс в окремий файл або використати forwardRef() як тимчасове рішення під час рефакторингу ланцюга залежностей.

Сторонні бібліотеки, що все ще використовують NgModules. Багато бібліотек перейшли на standalone, але деякі застарілі пакети все ще експортують NgModules. Імпортуйте ці модулі безпосередньо в масив imports standalone-компонента, Angular підтримує змішування standalone та модульних імпортів.

Відсутні провайдери після видалення AppModule. Сервіси, що раніше надавалися в масиві providers кореневого модуля, повинні бути перенесені до ApplicationConfig в app.config.ts або використовувати providedIn: 'root' в декораторі @Injectable. Сервіси з областю видимості маршруту повинні використовувати масив providers у конфігураціях маршрутів.

Питання на співбесіді

Коли на співбесіді з Angular запитують про standalone-компоненти, зосередьтеся на трьох пунктах: (1) управління залежностями переміщується з рівня модуля на рівень компонента, (2) tree-shaking стає гранулярним, (3) налаштування тестів спрощується, оскільки компоненти оголошують власні імпорти.

Джерела

Міграція до standalone: ключові висновки для Angular 22

  • Офіційна схема Angular CLI автоматизує 80-90% міграції у трьох послідовних проходах: конвертація оголошень, видалення модулів, зміна bootstrap
  • Модулі маршрутизації потребують ручної конвертації з loadChildren з NgModules на loadComponent або standalone файли маршрутів
  • SharedModules є основною перешкодою, конвертуйте кожне спільне оголошення в standalone окремо, потім видаліть модуль
  • Увімкніть strictStandalone в tsconfig для запобігання появі нових NgModules після міграції
  • Розміри бандлів зменшуються на 30-55% завдяки tree-shaking на рівні компонентів та детальному lazy loading з loadComponent
  • Юніт-тести спрощуються з Vitest як test runner за замовчуванням, standalone-компонент імпортується безпосередньо в TestBed без конфігурації модулів
  • OnPush за замовчуванням в Angular 22 та безселекторні компоненти природно поєднуються зі standalone-архітектурою для чистішої та зручнішої в підтримці кодової бази

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Щоденний виклик

Чи знайдеш ти помилку в Angular?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 21 серпня 2026 р.

Теги

#angular
#standalone components
#міграція
#angular 21
#ngmodule
#lazy loading

Поділитися

Пов'язані статті