65. Маршрутизация: продвинутая
🚀 Маршрутизация в Angular: Продвинутый уровень
Заголовок раздела «🚀 Маршрутизация в Angular: Продвинутый уровень»Базовую маршрутизацию освоили — теперь ныряем глубже! Вложенные маршруты, именованные аутлеты, события роутера, lazy loading… Всё это превращает Angular Router из простого навигатора в мощную архитектурную систему. 💪
🪆 Вложенные маршруты (children)
Заголовок раздела «🪆 Вложенные маршруты (children)»Вложенные маршруты — это как папки внутри папок 📁. Родительский компонент имеет свой <router-outlet>, и дочерние маршруты рендерятся именно туда:
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 ] } ] }];@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 {}🔀 Named Outlets (Именованные аутлеты)
Заголовок раздела «🔀 Named Outlets (Именованные аутлеты)»Иногда нужно рендерить несколько независимых областей одновременно. Например, основной контент + боковая панель помощи:
<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 } }]);// В маршрутах — указываем outletexport 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']📡 Router Events: индикатор загрузки
Заголовок раздела «📡 Router Events: индикатор загрузки»Роутер Angular генерирует события на каждом шаге навигации. Это идеально для глобального индикатора загрузки:
@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 маршрутов
Заголовок раздела «🦥 Lazy Loading маршрутов»Lazy loading позволяет загружать части приложения только когда пользователь к ним обращается. Для Standalone компонентов:
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.tsexport const SHOP_ROUTES: Routes = [ { path: '', component: ShopHomeComponent }, { path: 'products', component: ProductsComponent }, { path: 'cart', component: CartComponent },];⚡ Стратегии предзагрузки
Заголовок раздела «⚡ Стратегии предзагрузки»Lazy loading отлично, но иногда хочется предзагрузить модули заранее, пока пользователь не перешёл к ним:
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') }🗄️ Route Resolver: данные до навигации
Заголовок раздела «🗄️ Route Resolver: данные до навигации»Resolver загружает данные ДО того, как компонент появится на экране. Пользователь не видит пустой шаблон — всё загружается “за кулисами”:
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) {}}📜 scrollPositionRestoration
Заголовок раздела «📜 scrollPositionRestoration»Angular Router может автоматически восстанавливать позицию прокрутки при навигации:
export const appConfig: ApplicationConfig = { providers: [ provideRouter( routes, withInMemoryScrolling({ scrollPositionRestoration: 'enabled', // восстанавливать позицию anchorScrolling: 'enabled', // прокрутка к #fragment }) ) ]};Практика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: