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

16. Маршрутизация: основы

Маршрутизация (routing) — это система навигации между “страницами” в SPA (Single Page Application). Представь, что твоё приложение — это книга 📖, а роутер — это содержание: он знает, какую страницу открыть по адресу. При этом сама книга (браузер) не перезагружается — просто меняется содержимое!


В Angular маршруты описываются массивом конфигурации. Каждый маршрут — объект с path и component:

app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductsComponent } from './products/products.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { AboutComponent } from './about/about.component';
import { NotFoundComponent } from './not-found/not-found.component';
export const routes: Routes = [
{ path: '', component: HomeComponent }, // /
{ path: 'products', component: ProductsComponent }, // /products
{ path: 'products/:id', component: ProductDetailComponent }, // /products/42
{ path: 'about', component: AboutComponent }, // /about
{ path: '**', component: NotFoundComponent }, // любой другой путь = 404
];

Подключение в app.config.ts (Standalone, Angular 17+):

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes)
// С дополнительными опциями:
// provideRouter(routes, withDebugTracing(), withPreloading(PreloadAllModules))
]
};

Или классически через NgModule:

app.module.ts
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppModule {}

<router-outlet> — это “дырка” в шаблоне, куда Angular вставляет активный компонент маршрута. Как слот в конструкторе Lego 🧱:

app.component.ts
@Component({
selector: 'app-root',
template: `
<nav>
<a routerLink="/">Главная</a>
<a routerLink="/products">Продукты</a>
<a routerLink="/about">О нас</a>
</nav>
<!-- Сюда Angular рендерит компонент активного маршрута -->
<router-outlet />
<footer>© 2025 MyApp</footer>
`,
imports: [RouterOutlet, RouterLink]
})
export class AppComponent {}

RouterLink — директива для создания ссылок без перезагрузки страницы. Это не просто <a href>, это умная навигация Angular:

<!-- Простая строка -->
<a routerLink="/products">Продукты</a>
<!-- Массив сегментов (рекомендуется!) -->
<a [routerLink]="['/products']">Продукты</a>
<a [routerLink]="['/products', productId]">Товар</a>
<a [routerLink]="['/products', productId, 'reviews']">Отзывы</a>
<!-- С query params и fragment -->
<a
[routerLink]="['/products']"
[queryParams]="{ page: 2, sort: 'name' }"
fragment="top">
Страница 2
</a>
<!-- Результат: /products?page=2&sort=name#top -->
<!-- routerLinkActive — добавляет класс активной ссылке -->
<a routerLink="/products" routerLinkActive="active">Продукты</a>
<!-- Точное совпадение (не активируется на /products/1) -->
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
Главная
</a>

Иногда нужно перейти на страницу из TypeScript кода (после сохранения формы, после логина и т.д.):

import { Router } from '@angular/router';
@Component({...})
export class LoginComponent {
constructor(private router: Router) {}
onLogin() {
// Навигация с массивом сегментов
this.router.navigate(['/dashboard']);
// С параметрами
this.router.navigate(['/products', 42]);
// С query params
this.router.navigate(['/products'], {
queryParams: { category: 'phones', sort: 'price' }
});
// По строке URL (менее гибко)
this.router.navigateByUrl('/products?sort=name#top');
// Относительная навигация (от текущего маршрута)
this.router.navigate(['../sibling'], { relativeTo: this.route });
}
}

ActivatedRoute — это сервис, который содержит информацию о текущем маршруте: параметры, query params, данные и т.д.:

import { ActivatedRoute } from '@angular/router';
@Component({...})
export class ProductDetailComponent implements OnInit {
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// === Snapshot (статический — работает если компонент переиспользуется редко) ===
const id = this.route.snapshot.paramMap.get('id');
console.log('ID:', id); // '42'
// === Observable (реактивный — при навигации между /products/1 и /products/2) ===
this.route.paramMap.subscribe(params => {
const id = params.get('id');
this.loadProduct(id!);
});
// Современный подход с signal
const params = this.route.snapshot.params;
console.log(params['id']); // '42'
}
}

Query params идут после ? в URL: /products?page=2&sort=name. Они не обязательны и не влияют на маршрут:

// Чтение query params
@Component({...})
export class ProductsComponent implements OnInit {
currentPage = 1;
sortBy = 'name';
constructor(private route: ActivatedRoute, private router: Router) {}
ngOnInit() {
// Snapshot
const page = this.route.snapshot.queryParamMap.get('page') ?? '1';
this.currentPage = +page;
// Observable (обновляется при изменении URL)
this.route.queryParamMap.subscribe(params => {
this.currentPage = +(params.get('page') ?? '1');
this.sortBy = params.get('sort') ?? 'name';
this.loadProducts();
});
}
changePage(page: number) {
this.router.navigate([], {
queryParams: { page },
queryParamsHandling: 'merge' // сохраняем остальные params
});
}
}

Fragment — это часть URL после #. Используется для прокрутки к определённому месту на странице:

<!-- Навигация с fragment -->
<a [routerLink]="['/docs']" fragment="installation">Установка</a>
<!-- Результат: /docs#installation -->
// Чтение fragment
this.route.fragment.subscribe(fragment => {
if (fragment) {
const el = document.getElementById(fragment);
el?.scrollIntoView({ behavior: 'smooth' });
}
});

Порядок маршрутов важен! Angular проверяет их сверху вниз и берёт первое совпадение:

export const routes: Routes = [
// Редирект: / → /home
{ path: '', redirectTo: '/home', pathMatch: 'full' },
// ^^^^^^^^^ ОБЯЗАТЕЛЬНО для ''!
// pathMatch: 'full' — совпадает только точный путь ''
// pathMatch: 'prefix' (по умолчанию) — совпадает любой путь начинающийся с ''
{ path: 'home', component: HomeComponent },
{ path: 'products', component: ProductsComponent },
{ path: 'products/:id', component: ProductDetailComponent },
// Устаревшие URL → новые
{ path: 'old-products', redirectTo: '/products', pathMatch: 'full' },
// ⚠️ Wildcard ДОЛЖЕН быть последним!
{ path: '**', component: NotFoundComponent },
];

🏗️ Полный пример: SPA с несколькими страницами

Заголовок раздела «🏗️ Полный пример: SPA с несколькими страницами»
app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{
path: 'products',
component: ProductsComponent,
title: 'Продукты' // <title> в браузере
},
{
path: 'products/:id',
component: ProductDetailComponent,
title: 'Детали товара'
},
{ path: 'about', component: AboutComponent },
{ path: '**', component: NotFoundComponent },
];
// product-detail.component.ts
@Component({
template: `
<div *ngIf="product$ | async as product">
<h1>{{ product.name }}</h1>
<p>{{ product.description }}</p>
<button (click)="goBack()">← Назад</button>
</div>
`
})
export class ProductDetailComponent {
product$ = this.route.paramMap.pipe(
map(params => params.get('id')!),
switchMap(id => this.productsService.getById(+id))
);
constructor(
private route: ActivatedRoute,
private router: Router,
private productsService: ProductsService
) {}
goBack() {
this.router.navigate(['/products']);
// Или: this.location.back();
}
}

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