28. Angular Modules
📦 NgModule в Angular
Заголовок раздела «📦 NgModule в Angular»NgModule — это механизм группировки компонентов, директив, пайпов и сервисов в логические блоки. Исторически основной способ организации Angular приложений.
🔬 Анатомия NgModule
Заголовок раздела «🔬 Анатомия NgModule»import { NgModule } from '@angular/core';import { CommonModule } from '@angular/common';import { RouterModule } from '@angular/router';
// Компоненты, директивы, пайпыimport { DashboardComponent } from './dashboard.component';import { StatsCardComponent } from './stats-card/stats-card.component';import { MetricPipe } from './pipes/metric.pipe';import { HighlightDirective } from './directives/highlight.directive';
// Сервисыimport { DashboardService } from './dashboard.service';
@NgModule({ declarations: [ // Компоненты, директивы, пайпы которые ПРИНАДЛЕЖАТ этому модулю DashboardComponent, StatsCardComponent, MetricPipe, HighlightDirective, ],
imports: [ // Модули, от которых зависит этот модуль CommonModule, // NgIf, NgFor, AsyncPipe... RouterModule, // RouterLink, RouterOutlet... ],
exports: [ // Что ДОСТУПНО другим модулям, импортирующим этот DashboardComponent, StatsCardComponent, MetricPipe, // HighlightDirective НЕ экспортируем — только для внутреннего использования ],
providers: [ // Сервисы, доступные в рамках этого модуля DashboardService, ],
bootstrap: [ // Только в корневом AppModule — корневой компонент // AppComponent ]})export class DashboardModule {}🏗️ Feature модули
Заголовок раздела «🏗️ Feature модули»Feature модуль инкапсулирует отдельную функциональность:
@NgModule({ declarations: [ UsersListComponent, UserDetailComponent, UserFormComponent, UserAvatarComponent, ], imports: [ CommonModule, ReactiveFormsModule, RouterModule.forChild([ { path: '', component: UsersListComponent }, { path: ':id', component: UserDetailComponent }, { path: 'new', component: UserFormComponent }, ]), SharedModule, // общие компоненты ], providers: [ UsersService, // сервис scope — только в этом модуле ]})export class UsersModule {}🤝 Shared Module — общие компоненты
Заголовок раздела «🤝 Shared Module — общие компоненты»Shared модуль содержит переиспользуемые компоненты:
@NgModule({ declarations: [ // Компоненты общего использования ButtonComponent, InputComponent, ModalComponent, SpinnerComponent, AvatarComponent, BadgeComponent, ], imports: [ CommonModule, ReactiveFormsModule, RouterModule, ], exports: [ // Экспортируем ВСЁ что нужно другим модулям ButtonComponent, InputComponent, ModalComponent, SpinnerComponent, AvatarComponent, BadgeComponent, // Также реэкспортируем часто используемые модули CommonModule, ReactiveFormsModule, ]})export class SharedModule {}Правило: SharedModule не должен иметь providers. Иначе сервисы будут создаваться несколько раз при lazy loading.
🏛️ Core Module — синглтоны
Заголовок раздела «🏛️ Core Module — синглтоны»Core модуль содержит сервисы-синглтоны и компоненты один раз для всего приложения:
import { NgModule, Optional, SkipSelf } from '@angular/core';
@NgModule({ declarations: [ HeaderComponent, // Одиночный хедер приложения FooterComponent, // Одиночный футер SidebarComponent, // Одиночный сайдбар ], imports: [ CommonModule, RouterModule, SharedModule, ], exports: [ HeaderComponent, FooterComponent, SidebarComponent, ], providers: [ AuthService, // Синглтон сервис авторизации LoggerService, // Синглтон логгер ThemeService, // Синглтон сервис темы ]})export class CoreModule { // Защита от повторного импорта constructor(@Optional() @SkipSelf() parentModule?: CoreModule) { if (parentModule) { throw new Error('CoreModule уже загружен! Импортируй только в AppModule.'); } }}@NgModule({ imports: [ CoreModule, // Только здесь! SharedModule, // В каждом feature модуле ]})export class AppModule {}📱 AppModule — корневой модуль
Заголовок раздела «📱 AppModule — корневой модуль»import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { BrowserAnimationsModule } from '@angular/platform-browser/animations';import { HttpClientModule } from '@angular/common/http';import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';import { CoreModule } from './core/core.module';import { routes } from './app.routes';
@NgModule({ declarations: [ AppComponent, // Только корневой компонент ], imports: [ BrowserModule, // Браузерная платформа (только в AppModule!) BrowserAnimationsModule, // Angular animations HttpClientModule, // HTTP клиент RouterModule.forRoot(routes), // Корневые маршруты (forRoot только раз!) CoreModule, ], bootstrap: [AppComponent] // Что загружать при старте})export class AppModule {}🔄 forRoot vs forChild
Заголовок раздела «🔄 forRoot vs forChild»forRoot — конфигурация один раз (синглтоны):
// ✅ Только в AppModuleRouterModule.forRoot(routes, { enableTracing: false })StoreModule.forRoot(reducers)forChild — для feature модулей (без синглтонов):
// ✅ В каждом feature модулеRouterModule.forChild([ { path: 'users', component: UsersComponent }])StoreModule.forFeature('users', usersReducer)⚡ Модульная ленивая загрузка
Заголовок раздела «⚡ Модульная ленивая загрузка»export const routes: Routes = [ { path: 'users', loadChildren: () => import('./users/users.module').then(m => m.UsersModule) // UsersModule создаётся со своим инжектором // providers из UsersModule — только для этого модуля! }];📊 Структура модулей в реальном проекте
Заголовок раздела «📊 Структура модулей в реальном проекте»src/ app/ core/ ← CoreModule (синглтоны) services/ guards/ interceptors/ shared/ ← SharedModule (переиспользуемые компоненты) components/ directives/ pipes/ features/ users/ ← UsersModule (ленивая загрузка) products/ ← ProductsModule (ленивая загрузка) dashboard/ ← DashboardModule (ленивая загрузка) app.module.ts ← AppModule (корневой) app.routes.tsexport default function NgModulePlayground() { const [activeModule, setActiveModule] = React.useState(null); const [importedBy, setImportedBy] = React.useState([]);
const modules = { AppModule: { color: '#7c3aed', declarations: ['AppComponent'], imports: ['BrowserModule', 'RouterModule.forRoot', 'CoreModule'], exports: [], providers: [], bootstrap: ['AppComponent'], }, CoreModule: { color: '#dd0031', declarations: ['HeaderComponent', 'FooterComponent'], imports: ['CommonModule', 'RouterModule', 'SharedModule'], exports: ['HeaderComponent', 'FooterComponent'], providers: ['AuthService', 'LoggerService'], }, SharedModule: { color: '#1d4ed8', declarations: ['ButtonComponent', 'SpinnerComponent', 'AvatarComponent'], imports: ['CommonModule', 'ReactiveFormsModule'], exports: ['ButtonComponent', 'SpinnerComponent', 'AvatarComponent', 'CommonModule'], providers: [], }, UsersModule: { color: '#15803d', declarations: ['UsersListComponent', 'UserDetailComponent'], imports: ['CommonModule', 'SharedModule', 'RouterModule.forChild'], exports: [], providers: ['UsersService'], lazy: true, }, ProductsModule: { color: '#92400e', declarations: ['ProductsListComponent', 'ProductCardComponent'], imports: ['CommonModule', 'SharedModule', 'RouterModule.forChild'], exports: [], providers: ['ProductsService'], lazy: true, }, };
const hierarchy = [ { id: 'AppModule', level: 0 }, { id: 'CoreModule', level: 1 }, { id: 'SharedModule', level: 1 }, { id: 'UsersModule', level: 2 }, { id: 'ProductsModule', level: 2 }, ];
const select = (id) => { setActiveModule(id); const m = modules[id]; const parents = Object.entries(modules) .filter(([k, v]) => v.imports?.some(imp => imp === id || imp.startsWith(id))) .map(([k]) => k); setImportedBy(parents); };
const active = activeModule ? modules[activeModule] : null;
return ( <div style={{ background: '#0f172a', minHeight: 480, padding: 24, borderRadius: 12, fontFamily: 'system-ui', color: '#e2e8f0' }}> <div style={{ color: '#dd0031', fontWeight: 700, fontSize: 16, marginBottom: 20 }}> 📦 NgModule структура приложения </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}> <div> <div style={{ fontSize: 12, color: '#64748b', marginBottom: 10 }}>Кликни на модуль для деталей:</div> {hierarchy.map(({ id, level }) => { const m = modules[id]; return ( <div key={id} onClick={() => select(id)} style={{ marginLeft: level * 24, marginBottom: 8, background: activeModule === id ? m.color + '20' : '#1e293b', border: `1px solid ${activeModule === id ? m.color : '#334155'}`, borderRadius: 8, padding: '10px 14px', cursor: 'pointer', transition: 'all 0.2s' }} > <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ width: 10, height: 10, borderRadius: '50%', background: m.color }} /> <span style={{ fontSize: 13, fontWeight: 600, color: activeModule === id ? m.color : '#e2e8f0' }}> {id} </span> {m.lazy && <span style={{ fontSize: 10, color: '#f59e0b', border: '1px solid #f59e0b', padding: '1px 6px', borderRadius: 10 }}>lazy</span>} </div> <div style={{ fontSize: 11, color: '#475569', marginTop: 4, marginLeft: 18 }}> {m.declarations?.length} declarations · {m.providers?.length} providers </div> </div> ); })} </div>
<div> {active ? ( <div style={{ background: '#1e293b', borderRadius: 10, padding: 16, border: `1px solid ${modules[activeModule].color}` }}> <div style={{ color: modules[activeModule].color, fontWeight: 700, fontSize: 14, marginBottom: 14 }}> @NgModule({'{'}...{'}'}) <br /> <span style={{ fontSize: 12 }}>{activeModule}</span> </div>
{[ { key: 'declarations', color: '#7dd3fc', icon: '📋' }, { key: 'imports', color: '#a78bfa', icon: '📥' }, { key: 'exports', color: '#22c55e', icon: '📤' }, { key: 'providers', color: '#f59e0b', icon: '🔧' }, { key: 'bootstrap', color: '#dd0031', icon: '🚀' }, ].map(({ key, color, icon }) => { const items = active[key]; if (!items || items.length === 0) return null; return ( <div key={key} style={{ marginBottom: 12 }}> <div style={{ fontSize: 11, color, marginBottom: 4 }}>{icon} {key}:</div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}> {items.map(item => ( <span key={item} style={{ fontSize: 11, padding: '2px 8px', borderRadius: 6, background: color + '20', color, border: `1px solid ${color}60` }}> {item} </span> ))} </div> </div> ); })}
{importedBy.length > 0 && ( <div style={{ borderTop: '1px solid #334155', paddingTop: 10, marginTop: 4 }}> <div style={{ fontSize: 11, color: '#64748b', marginBottom: 4 }}>Импортируется в:</div> {importedBy.map(m => ( <span key={m} style={{ fontSize: 11, color: '#94a3b8' }}>{m} </span> ))} </div> )} </div> ) : ( <div style={{ background: '#1e293b', borderRadius: 10, padding: 20, border: '1px solid #334155', color: '#475569', fontSize: 13, textAlign: 'center' }}> Выбери модуль для просмотра его анатомии </div> )} </div> </div> </div> );}