Skip to main content

Що таке модулі в Angular і як вони використовуються?

Модуль Angular (NgModule) - це контейнер, що групує компоненти, директиви, пайпи та сервіси через декоратор @NgModule.

Теорія

TL;DR

  • NgModule - як контейнер з вантажем: оголошує що йому належить, імпортує потрібне, і ділиться власним через exports
  • Кожен Angular-додаток стартує з кореневого модуля (AppModule); решта модулів організовують код по доменах
  • Eager-модулі завантажуються одразу при старті; ліниве завантаження (lazy loading) відкладає код до навігації на потрібний маршрут
  • Angular 14+ дав автономні компоненти (standalone components), яким NgModules взагалі не потрібні
  • Правило вибору: один кореневий модуль плюс feature-модуль на кожен домен; lazy-load для маршрутів глибше двох рівнів

Швидкий приклад

Мінімальне налаштування для будь-якого Angular-додатку:

ts
// app.module.ts - стартова точка додатку import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], // AppComponent належить цьому модулю imports: [BrowserModule], // BrowserModule підключає рендеринг DOM + CommonModule providers: [], // Сервіси для всього додатку bootstrap: [AppComponent] // Компонент, що рендериться при запуску }) export class AppModule {}

Прибери цей файл - і додаток не запуститься. Ці 10 рядків і є мінімум, якого Angular потребує.

П'ять полів декоратора

@NgModule приймає об'єкт з п'ятьма полями. Плутати їх - найпоширеніша помилка на інтерв'ю:

  • declarations - компоненти, директиви та пайпи, що належать цьому модулю. Приватні, якщо не зазначено в exports.
  • imports - інші модулі, чиї exports ти хочеш використовувати тут.
  • exports - твої declarations, відкриті для будь-якого модуля, що імпортує цей.
  • providers - сервіси, зареєстровані в інжекторі цього модуля.
  • bootstrap - кореневий компонент. Тільки AppModule це потребує.

Компонент, оголошений в одному модулі, невидимий в іншому, поки не буде експортований з першого і поки перший не буде імпортований другим. Саме це правило найчастіше забувають.

Типи модулів

ТипПризначення
AppModuleКореневий модуль, стартує додаток
Feature moduleІзолює один домен: user, admin, checkout
Shared moduleРеекспортує спільні утиліти: CommonModule, пайпи, UI-компоненти
Core moduleSingleton-сервіси, HTTP-інтерцептори, глобальні guards
Lazy moduleФункціональність, що завантажується за потребою через роутер

Як працює ліниве завантаження

Додай loadChildren до маршруту - і роутер Angular під час білда виокремлює цей модуль в окремий JS-чанк. Чанк завантажується тільки тоді, коли користувач переходить на відповідний маршрут.

ts
// app-routing.module.ts const routes: Routes = [ { path: 'admin', // Webpack виокремлює це в admin.js - завантажується тільки на /admin loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) } ];

Lazy-модуль отримує власний дочірній інжектор, батьком якого є кореневий. Сервіси в providers lazy-модуля ізольовані від решти додатку. Я бачив, як команди спотикались на цьому: очікують singleton, а при кожній навігації отримують новий екземпляр.

Standalone-компоненти (Angular 14+)

Standalone-компоненти оголошують свої залежності напряму, без модуля:

ts
// Модуль не потрібен - компонент сам керує своїми залежностями @Component({ standalone: true, imports: [CommonModule, ReactiveFormsModule], template: `<form [formGroup]="form">...</form>` }) export class UserProfileComponent {}

Для Angular 17+ проектів standalone - це стандарт за замовчуванням. NgModules залишаються в legacy-кодових базах і бібліотеках на зразок Angular Material.

Типові помилки

  1. Оголошення одного компонента в двох модулях. Angular кидає помилку при компіляції: "Type X is part of the declarations of 2 modules."

    ts
    // Неправильно: UserCard оголошений і в UserModule, і в SharedModule // Виправлення: оголосити в UserModule, реекспортувати з SharedModule exports: [UserCard]
  2. BrowserModule у feature-модулі. Він тільки для AppModule. У feature-модулях - CommonModule.

    ts
    // Неправильно в будь-якому модулі крім AppModule: imports: [BrowserModule] // Виправлення: imports: [CommonModule]
  3. Singleton-сервіс в providers lazy-модуля. При кожному завантаженні модуля Angular створює новий екземпляр - стан губиться.

    ts
    // Неправильно - новий UserService при кожній навігації: providers: [UserService] // Виправлення: @Injectable({ providedIn: 'root' }) export class UserService {}
  4. RouterModule.forRoot() у feature-модулі. forRoot() - тільки один раз в AppModule. У feature-модулях - forChild(). Повторне forRoot() непомітно ламає route guards.

  5. Відсутній CommonModule у feature-модулях. *ngIf і *ngFor перестають працювати, а повідомлення про помилку не одразу вказує на причину.

Де зустрічається в реальних проектах

  • Angular Material: окремий модуль на кожен компонент (MatButtonModule, MatDialogModule). Імпортуєш тільки те, що потрібно.
  • NGXS / NgRx: .forRoot() в AppModule, .forFeature() в lazy-модулях. Якщо переплутати - стейт зламається.
  • Nx workspace: автоматично генерує lazy feature-модулі для кожної бібліотеки. Кожна lib - окремий bounded context.
  • Патерн Core / Shared: CoreModule тримає інтерцептори та глобальні guards (імпортується один раз). SharedModule тримає CommonModule, спільні пайпи та UI-компоненти (імпортується в кожному feature-модулі).

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

Q: Яка різниця між declarations, imports та exports в @NgModule?
A: declarations реєструє компоненти, пайпи та директиви як власність цього модуля (локальна область видимості). imports підключає exports іншого модуля для використання тут. exports відкриває твої declarations для будь-якого модуля, що імпортує цей.

Q: Чому у feature-модулях CommonModule, а не BrowserModule?
A: BrowserModule реєструє загальноappні провайдери, які мають існувати тільки один раз. При повторному імпорті Angular кидає "BrowserModule has already been loaded." *ngIf і *ngFor у feature-модулях беруться з CommonModule напряму.

Q: Як ліниве завантаження (lazy loading) зменшує розмір бандла?
A: loadChildren з динамічним import() каже webpack виокремити модуль в окремий чанк. Цей чанк не входить до основного бандла і завантажується тільки при активації маршруту роутером.

Q: Коли від NgModules можна відмовитись?
A: У Angular 15+ проектах зі standalone-компонентами. Виклик bootstrapApplication() з ApplicationConfig замінює AppModule, а кожен компонент оголошує власні imports. Жодного AppModule не потрібно.

Q: (Senior) Як поводиться ієрархія інжекторів з lazy-модулями?
A: Lazy-модуль створює дочірній інжектор, батьком якого є кореневий. При запиті сервісу Angular йде знизу вгору: спочатку дочірній інжектор, потім кореневий. Сервіс в providers lazy-модуля - інший екземпляр, ніж той що в кореневому. Тому providedIn: 'root' - безпечний стандарт для всього, що має бути singleton.

Приклади

Кореневий модуль

Мінімальне налаштування для запуску Angular-додатку:

ts
// app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, // Тільки тут - налаштовує рендеринг DOM AppRoutingModule // Налаштування роутера ], bootstrap: [AppComponent] }) export class AppModule {}

BrowserModule - тільки тут і більше ніде. Він реекспортує CommonModule, тому *ngIf і *ngFor у кореневому модулі працюють автоматично.

Feature-модуль з роутингом

Домен профілю користувача з реактивними формами:

ts
// user.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { UserProfileComponent } from './user-profile.component'; import { UserRoutingModule } from './user-routing.module'; @NgModule({ declarations: [UserProfileComponent], imports: [ CommonModule, // *ngIf, *ngFor, AsyncPipe ReactiveFormsModule, // FormGroup, FormControl UserRoutingModule // Реєструє маршрут /profile ] // Без exports - модуль самодостатній }) export class UserModule {} // user-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: 'profile', component: UserProfileComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], // forChild, не forRoot exports: [RouterModule] }) export class UserRoutingModule {}

forChild тут не деталь. Якщо написати forRoot - route guards перестануть спрацьовувати, і знайти причину буде складно.

Патерн Shared module

Коли кілька feature-модулів потребують одних і тих самих компонентів і пайпів:

ts
// shared.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DateFormatPipe } from './pipes/date-format.pipe'; import { LoadingSpinnerComponent } from './loading-spinner.component'; @NgModule({ declarations: [DateFormatPipe, LoadingSpinnerComponent], imports: [CommonModule], exports: [ CommonModule, // Feature-модулі отримують *ngIf тощо автоматично DateFormatPipe, LoadingSpinnerComponent ] }) export class SharedModule {} // Будь-який feature-модуль отримує всі три одним рядком: // imports: [SharedModule]

Це і є реальний патерн CoreModule / SharedModule, який зустрічається в більшості production Angular-проектів. CoreModule тримає singleton-сервіси. SharedModule - багаторазові UI-шматки.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?