ДокументацияAngularМаршрутизация в Angular
Средний 12 мин чтения

Маршрутизация в Angular

Angular Router — навигация между страницами SPA: настройка маршрутов, параметры, guards для защиты страниц, lazy loading модулей и resolver для предзагрузки данных.

AngularRouternavigationguardslazy loadingresolver

Что такое Angular Router

Angular Router превращает одностраничное приложение (SPA) в многостраничное с точки зрения пользователя. URL в адресной строке меняется, но страница не перезагружается — Router просто подставляет нужный компонент в <router-outlet>.

Базовая настройка

app.routes.ts

Начиная с Angular 17 standalone-приложения используют файл маршрутов вместо RouterModule:

// src/app/app.routes.ts
import { Routes } from '@angular/router'

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'tasks', component: TaskListComponent },
  { path: 'tasks/:id', component: TaskDetailComponent },
  { path: 'profile', component: ProfileComponent },
  { path: '**', component: NotFoundComponent },
]

Регистрация в приложении

// src/app/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),
  ]
}

Router outlet

В корневом компоненте указываем, куда рендерить текущий маршрут:

<!-- app.component.html -->
<nav>
  <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Главная</a>
  <a routerLink="/tasks" routerLinkActive="active">Задачи</a>
  <a routerLink="/profile" routerLinkActive="active">Профиль</a>
</nav>

<router-outlet />

routerLinkActive добавляет CSS-класс active, когда ссылка совпадает с текущим URL. Опция exact: true нужна для корневого маршрута, иначе / совпадёт со всеми путями.

Параметры маршрута

Параметры пути (path params)

// Маршрут с параметром
{ path: 'tasks/:id', component: TaskDetailComponent }
// task-detail.component.ts
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute } from '@angular/router'

@Component({
  standalone: true,
  template: `<h2>Задача #{{ taskId }}</h2>`
})
export class TaskDetailComponent implements OnInit {
  taskId = ''

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Один раз при загрузке
    this.taskId = this.route.snapshot.paramMap.get('id') ?? ''

    // Реагировать на изменение параметра без перезагрузки компонента
    this.route.paramMap.subscribe(params => {
      this.taskId = params.get('id') ?? ''
      this.loadTask(this.taskId)
    })
  }

  loadTask(id: string) {
    // Загрузить данные задачи
  }
}

Query-параметры

// Переход с query-параметрами
import { Router } from '@angular/router'

export class TaskListComponent {
  constructor(private router: Router) {}

  filterByDifficulty(difficulty: string) {
    this.router.navigate(['/tasks'], {
      queryParams: { difficulty, page: 1 }
    })
  }
}
// Чтение query-параметров
this.route.queryParamMap.subscribe(params => {
  const difficulty = params.get('difficulty')
  const page = params.get('page')
})
<!-- Ссылка с query-параметрами -->
<a [routerLink]="['/tasks']" [queryParams]="{ difficulty: 'easy' }">
  Простые задачи
</a>

Дочерние маршруты (nested routes)

Дочерние маршруты позволяют вкладывать <router-outlet> друг в друга:

export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardLayoutComponent,
    children: [
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      { path: 'overview', component: OverviewComponent },
      { path: 'analytics', component: AnalyticsComponent },
      { path: 'settings', component: SettingsComponent },
    ]
  }
]
<!-- dashboard-layout.component.html -->
<div class="dashboard">
  <aside>
    <a routerLink="overview">Обзор</a>
    <a routerLink="analytics">Аналитика</a>
    <a routerLink="settings">Настройки</a>
  </aside>
  <main>
    <router-outlet />
  </main>
</div>

Дочерние маршруты рендерятся во внутренний <router-outlet>.

Lazy loading

Lazy loading загружает код маршрута только при переходе на него. Это уменьшает начальный бандл и ускоряет загрузку:

export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'tasks',
    loadChildren: () => import('./features/tasks/task.routes')
      .then(m => m.TASK_ROUTES)
  },
  {
    path: 'profile',
    loadComponent: () => import('./features/profile/profile.component')
      .then(m => m.ProfileComponent)
  },
]
// features/tasks/task.routes.ts
import { Routes } from '@angular/router'

export const TASK_ROUTES: Routes = [
  { path: '', component: TaskListComponent },
  { path: ':id', component: TaskDetailComponent },
  { path: 'new', component: TaskCreateComponent },
]

loadChildren загружает целый набор маршрутов, loadComponent — один компонент. И то, и другое создаёт отдельный чанк (файл), который подгрузится по требованию.

Guards — защита маршрутов

Guards решают, можно ли перейти на маршрут. Начиная с Angular 15 guards — это функции, а не классы:

CanActivate — проверка доступа

// guards/auth.guard.ts
import { CanActivateFn, Router } from '@angular/router'
import { inject } from '@angular/core'
import { AuthService } from '../services/auth.service'

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService)
  const router = inject(Router)

  if (auth.isLoggedIn()) {
    return true
  }

  router.navigate(['/auth/login'])
  return false
}
// Применение к маршруту
{
  path: 'profile',
  component: ProfileComponent,
  canActivate: [authGuard]
}

CanActivateChild — защита дочерних маршрутов

export const adminGuard: CanActivateChildFn = () => {
  const auth = inject(AuthService)
  return auth.hasRole('admin') || inject(Router).parseUrl('/403')
}

// В маршрутах
{
  path: 'admin',
  component: AdminLayoutComponent,
  canActivateChild: [adminGuard],
  children: [
    { path: 'users', component: AdminUsersComponent },
    { path: 'settings', component: AdminSettingsComponent },
  ]
}

CanMatch — условная загрузка lazy-модуля

import { CanMatchFn, Router } from '@angular/router'
import { inject } from '@angular/core'
import { AuthService } from '../services/auth.service'

export const premiumGuard: CanMatchFn = () => {
  const auth = inject(AuthService)
  const router = inject(Router)

  if (auth.hasPremium()) {
    return true
  }

  router.navigate(['/subscribe'])
  return false
}

// Модуль загрузится только если guard вернёт true
{
  path: 'premium',
  canMatch: [premiumGuard],
  loadChildren: () => import('./features/premium/premium.routes')
    .then(m => m.PREMIUM_ROUTES)
}

CanDeactivate — защита от ухода со страницы

export const unsavedChangesGuard: CanDeactivateFn<FormComponent> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('Есть несохранённые изменения. Уйти со страницы?')
  }
  return true
}

// В маршрутах
{
  path: 'tasks/:id/edit',
  component: TaskEditComponent,
  canDeactivate: [unsavedChangesGuard]
}

Resolver — предзагрузка данных

Resolver загружает данные до того, как компонент отрендерится. Маршрут не активируется, пока resolver не вернёт данные:

// resolvers/task.resolver.ts
import { ResolveFn } from '@angular/router'
import { inject } from '@angular/core'
import { TaskService } from '../services/task.service'
import { Task } from '../models/task'

export const taskResolver: ResolveFn<Task> = (route) => {
  return inject(TaskService).getById(
    route.paramMap.get('id')!
  )
}
// В маршрутах
{
  path: 'tasks/:id',
  component: TaskDetailComponent,
  resolve: { task: taskResolver }
}
// В компоненте — данные уже доступны
export class TaskDetailComponent {
  task = this.route.snapshot.data['task'] as Task

  constructor(private route: ActivatedRoute) {}
}

Resolver удобен, когда нужно показать страницу только с готовыми данными. Но в большинстве случаев проще загружать данные прямо в компоненте и показывать спиннер — это даёт лучший UX.

Программная навигация

import { Router, NavigationExtras } from '@angular/router'

export class SomeComponent {
  private router = inject(Router)

  goToTask(id: string) {
    this.router.navigate(['/tasks', id])
  }

  goToTasksWithFilter() {
    this.router.navigate(['/tasks'], {
      queryParams: { difficulty: 'hard', status: 'open' }
    })
  }

  goBack() {
    history.back()
    // или
    inject(Location).back()
  }
}

События навигации

// Отслеживать все переходы (для аналитики, прогресс-бара)
export class AppComponent implements OnInit {
  private router = inject(Router)

  ngOnInit() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log('Начинаем переход к:', event.url)
      }
      if (event instanceof NavigationEnd) {
        console.log('Переход завершён:', event.urlAfterRedirects)
      }
      if (event instanceof NavigationError) {
        console.error('Ошибка навигации:', event.error)
      }
    })
  }
}

Практический пример — полный файл маршрутов

// app.routes.ts
import { Routes } from '@angular/router'
import { authGuard } from './guards/auth.guard'
import { unsavedChangesGuard } from './guards/unsaved-changes.guard'

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'auth',
    loadChildren: () => import('./features/auth/auth.routes')
      .then(m => m.AUTH_ROUTES),
  },
  {
    path: 'tasks',
    canActivate: [authGuard],
    children: [
      {
        path: '',
        loadComponent: () => import('./features/tasks/task-list.component')
          .then(m => m.TaskListComponent),
      },
      {
        path: 'new',
        loadComponent: () => import('./features/tasks/task-create.component')
          .then(m => m.TaskCreateComponent),
      },
      {
        path: ':id',
        loadComponent: () => import('./features/tasks/task-detail.component')
          .then(m => m.TaskDetailComponent),
        resolve: { task: taskResolver },
      },
      {
        path: ':id/edit',
        loadComponent: () => import('./features/tasks/task-edit.component')
          .then(m => m.TaskEditComponent),
        canDeactivate: [unsavedChangesGuard],
      },
    ]
  },
  {
    path: '**',
    loadComponent: () => import('./shared/not-found.component')
      .then(m => m.NotFoundComponent),
  },
]

Итог

КонцепцияДля чего
RoutesОписание структуры страниц
<router-outlet>Место, куда рендерится текущий маршрут
routerLinkНавигация через шаблон
ActivatedRouteЧтение параметров и query
lazy loadingОтложенная загрузка — меньше начальный бандл
CanActivateПроверка доступа к странице
CanDeactivateЗащита от потери данных
CanMatchУсловная загрузка lazy-модуля
ResolveПредзагрузка данных перед показом

Angular Router — один из самых мощных роутеров среди фреймворков. Он поддерживает вложенность любой глубины, несколько outlet-ов (auxiliary routes) и детальный контроль над навигацией.