Перейти к содержимому

28. Angular Modules

NgModule — это механизм группировки компонентов, директив, пайпов и сервисов в логические блоки. Исторически основной способ организации Angular приложений.


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 модуль инкапсулирует отдельную функциональность:

users/users.module.ts
@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 модуль содержит переиспользуемые компоненты:

shared/shared.module.ts
@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 модуль содержит сервисы-синглтоны и компоненты один раз для всего приложения:

core/core.module.ts
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.');
}
}
}
app.module.ts
@NgModule({
imports: [
CoreModule, // Только здесь!
SharedModule, // В каждом feature модуле
]
})
export class AppModule {}

app.module.ts
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 — конфигурация один раз (синглтоны):

// ✅ Только в AppModule
RouterModule.forRoot(routes, { enableTracing: false })
StoreModule.forRoot(reducers)

forChild — для feature модулей (без синглтонов):

// ✅ В каждом feature модуле
RouterModule.forChild([
{ path: 'users', component: UsersComponent }
])
StoreModule.forFeature('users', usersReducer)

app.routes.ts
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.ts

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