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

65. Маршрутизация: продвинутая

🚀 Маршрутизация в Angular: Продвинутый уровень

Заголовок раздела «🚀 Маршрутизация в Angular: Продвинутый уровень»

Базовую маршрутизацию освоили — теперь ныряем глубже! Вложенные маршруты, именованные аутлеты, события роутера, lazy loading… Всё это превращает Angular Router из простого навигатора в мощную архитектурную систему. 💪


Вложенные маршруты — это как папки внутри папок 📁. Родительский компонент имеет свой <router-outlet>, и дочерние маршруты рендерятся именно туда:

app.routes.ts
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent, // Шапка + сайдбар
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent }, // /admin/dashboard
{ path: 'users', component: UsersComponent }, // /admin/users
{
path: 'settings',
component: SettingsComponent,
children: [
{ path: '', redirectTo: 'profile', pathMatch: 'full' },
{ path: 'profile', component: ProfileSettingsComponent }, // /admin/settings/profile
{ path: 'security', component: SecuritySettingsComponent }, // /admin/settings/security
]
}
]
}
];
admin-layout.component.ts
@Component({
template: `
<div class="admin-layout">
<nav class="sidebar">
<a routerLink="dashboard" routerLinkActive="active">Dashboard</a>
<a routerLink="users" routerLinkActive="active">Users</a>
<a routerLink="settings" routerLinkActive="active">Settings</a>
</nav>
<main>
<!-- Дочерние маршруты рендерятся здесь -->
<router-outlet />
</main>
</div>
`
})
export class AdminLayoutComponent {}

Иногда нужно рендерить несколько независимых областей одновременно. Например, основной контент + боковая панель помощи:

app.component.html
<router-outlet /> <!-- primary outlet -->
<router-outlet name="sidebar" /> <!-- named outlet -->
// Навигация в именованный аутлет
this.router.navigate([{
outlets: {
primary: ['products'],
sidebar: ['help', 'products-guide']
}
}]);
// URL: /products(sidebar:help/products-guide)
// Закрыть именованный аутлет
this.router.navigate([{ outlets: { sidebar: null } }]);
// В маршрутах — указываем outlet
export const routes: Routes = [
{ path: 'help/:topic', component: HelpComponent, outlet: 'sidebar' },
];

// Обязательный параметр: /users/:id
{ path: 'users/:id', component: UserComponent }
// Несколько параметров: /users/:userId/posts/:postId
{ path: 'users/:userId/posts/:postId', component: PostComponent }
// Чтение в компоненте
@Component({...})
export class PostComponent implements OnInit {
post$: Observable<Post>;
constructor(private route: ActivatedRoute, private service: PostService) {}
ngOnInit() {
// Реактивный подход: обновляется при изменении параметров
this.post$ = this.route.paramMap.pipe(
switchMap(params => {
const userId = params.get('userId')!;
const postId = params.get('postId')!;
return this.service.getPost(userId, postId);
})
);
}
}
// Matrix параметры: /users;sort=name;dir=asc
// Используются для параметров сегмента, не всего URL
<a [routerLink]="['/users', { sort: 'name', dir: 'asc' }]">Users</a>
// Чтение: this.route.snapshot.params['sort']

Роутер Angular генерирует события на каждом шаге навигации. Это идеально для глобального индикатора загрузки:

app.component.ts
@Component({
template: `
<!-- Полоска загрузки вверху страницы -->
<div class="loading-bar" *ngIf="loading$ | async"></div>
<router-outlet />
`
})
export class AppComponent {
loading$: Observable<boolean>;
constructor(private router: Router) {
this.loading$ = router.events.pipe(
filter(e =>
e instanceof NavigationStart ||
e instanceof NavigationEnd ||
e instanceof NavigationError ||
e instanceof NavigationCancel
),
map(e => e instanceof NavigationStart),
distinctUntilChanged()
);
}
}
// Все события роутера:
// NavigationStart — начало навигации
// RouteConfigLoadStart — начало загрузки lazy chunk
// RouteConfigLoadEnd — загрузка chunk завершена
// RoutesRecognized — маршрут распознан
// GuardsCheckStart — начало проверки guards
// GuardsCheckEnd — guards проверены
// ResolveStart — начало резолвера данных
// ResolveEnd — данные резолвера получены
// NavigationEnd — навигация завершена ✅
// NavigationError — ошибка навигации ❌
// NavigationCancel — навигация отменена (guard вернул false)

Lazy loading позволяет загружать части приложения только когда пользователь к ним обращается. Для Standalone компонентов:

app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
// Lazy loading отдельного компонента (Angular 15+)
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component').then(m => m.AdminComponent)
},
// Lazy loading целого набора маршрутов
{
path: 'shop',
loadChildren: () =>
import('./shop/shop.routes').then(m => m.SHOP_ROUTES)
},
// Lazy loading NgModule (классический подход)
{
path: 'reports',
loadChildren: () =>
import('./reports/reports.module').then(m => m.ReportsModule)
}
];
// shop/shop.routes.ts
export const SHOP_ROUTES: Routes = [
{ path: '', component: ShopHomeComponent },
{ path: 'products', component: ProductsComponent },
{ path: 'cart', component: CartComponent },
];

Lazy loading отлично, но иногда хочется предзагрузить модули заранее, пока пользователь не перешёл к ним:

app.config.ts
import { PreloadAllModules, NoPreloading } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withPreloading(PreloadAllModules) // Предзагружает все lazy модули сразу
// withPreloading(NoPreloading) // Не предзагружает (по умолчанию)
)
]
};
// Собственная стратегия предзагрузки
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Предзагружаем только маршруты с data.preload: true
return route.data?.['preload'] ? load() : EMPTY;
}
}
// В маршрутах
{ path: 'shop', data: { preload: true }, loadChildren: () => import('./shop/shop.routes') }

Resolver загружает данные ДО того, как компонент появится на экране. Пользователь не видит пустой шаблон — всё загружается “за кулисами”:

product.resolver.ts
export const productResolver: ResolveFn<Product> = (route) => {
const id = route.paramMap.get('id')!;
return inject(ProductsService).getById(+id).pipe(
catchError(() => {
inject(Router).navigate(['/not-found']);
return EMPTY;
})
);
};
// В маршруте
{
path: 'products/:id',
component: ProductDetailComponent,
resolve: { product: productResolver }
}
// В компоненте — данные уже доступны!
@Component({...})
export class ProductDetailComponent {
product = this.route.snapshot.data['product'] as Product;
// Или реактивно:
product$ = this.route.data.pipe(map(data => data['product'] as Product));
constructor(private route: ActivatedRoute) {}
}

Angular Router может автоматически восстанавливать позицию прокрутки при навигации:

app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withInMemoryScrolling({
scrollPositionRestoration: 'enabled', // восстанавливать позицию
anchorScrolling: 'enabled', // прокрутка к #fragment
})
)
]
};

Попробуйте концепцию в интерактивном редакторе: