Що таке модулі в Angular і як вони використовуються?
Модуль Angular (NgModule) - це контейнер, що групує компоненти, директиви, пайпи та сервіси через декоратор @NgModule.
Теорія
TL;DR
- NgModule - як контейнер з вантажем: оголошує що йому належить, імпортує потрібне, і ділиться власним через
exports - Кожен Angular-додаток стартує з кореневого модуля (
AppModule); решта модулів організовують код по доменах - Eager-модулі завантажуються одразу при старті; ліниве завантаження (lazy loading) відкладає код до навігації на потрібний маршрут
- Angular 14+ дав автономні компоненти (standalone components), яким NgModules взагалі не потрібні
- Правило вибору: один кореневий модуль плюс feature-модуль на кожен домен; lazy-load для маршрутів глибше двох рівнів
Швидкий приклад
Мінімальне налаштування для будь-якого Angular-додатку:
// 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 module | Singleton-сервіси, HTTP-інтерцептори, глобальні guards |
| Lazy module | Функціональність, що завантажується за потребою через роутер |
Як працює ліниве завантаження
Додай loadChildren до маршруту - і роутер Angular під час білда виокремлює цей модуль в окремий JS-чанк. Чанк завантажується тільки тоді, коли користувач переходить на відповідний маршрут.
// 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-компоненти оголошують свої залежності напряму, без модуля:
// Модуль не потрібен - компонент сам керує своїми залежностями
@Component({
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `<form [formGroup]="form">...</form>`
})
export class UserProfileComponent {}Для Angular 17+ проектів standalone - це стандарт за замовчуванням. NgModules залишаються в legacy-кодових базах і бібліотеках на зразок Angular Material.
Типові помилки
-
Оголошення одного компонента в двох модулях. Angular кидає помилку при компіляції: "Type X is part of the declarations of 2 modules."
ts// Неправильно: UserCard оголошений і в UserModule, і в SharedModule // Виправлення: оголосити в UserModule, реекспортувати з SharedModule exports: [UserCard] -
BrowserModuleу feature-модулі. Він тільки дляAppModule. У feature-модулях -CommonModule.ts// Неправильно в будь-якому модулі крім AppModule: imports: [BrowserModule] // Виправлення: imports: [CommonModule] -
Singleton-сервіс в
providerslazy-модуля. При кожному завантаженні модуля Angular створює новий екземпляр - стан губиться.ts// Неправильно - новий UserService при кожній навігації: providers: [UserService] // Виправлення: @Injectable({ providedIn: 'root' }) export class UserService {} -
RouterModule.forRoot()у feature-модулі.forRoot()- тільки один раз вAppModule. У feature-модулях -forChild(). ПовторнеforRoot()непомітно ламає route guards. -
Відсутній
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-додатку:
// 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-модуль з роутингом
Домен профілю користувача з реактивними формами:
// 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-модулів потребують одних і тих самих компонентів і пайпів:
// 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-шматки.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.