Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке модулі в Angular і як вони використовуються?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Модуль Angular (NgModule)** - це контейнер, що групує компоненти, директиви, пайпи та сервіси через декоратор `@NgModule`. ```ts @NgModule({ declarations: [MyComponent], // Компоненти цього модуля imports: [CommonModule], // Інші модулі для використання тут exports: [MyComponent] // Що доступно іншим модулям }) export class MyModule {} ``` **Головне:** `declarations` реєструє компоненти локально; `exports` відкриває їх для інших модулів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Модуль 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 module | Singleton-сервіси, 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-шматки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.