Маршрутизация: страницы без перезагрузки

Роутер превращает одностраничное приложение в многостраничное на вид: URL меняется, контент подменяется, а перезагрузки нет.

«Пользователь видит разные страницы. Браузер видит одну. Между ними стоит роутер и виртуозно жонглирует компонентами».

SPA живёт в одной HTML-странице, но пользователю нужны разделы: каталог, корзина, профиль. Роутер Angular сопоставляет URL с компонентами и подменяет их без перезагрузки страницы. Настраивается он списком маршрутов, который передаётся при старте приложения:

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { CartComponent } from './cart.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'cart', component: CartComponent },
  { path: '**', component: NotFoundComponent }, // 404
];
// main.ts — подключаем роутер
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app.component';
import { routes } from './app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes)],
});

Где рисуется текущий маршрут? В месте, помеченном <router-outlet>. А переходы делаются не через <a href> (она перезагрузит страницу), а через директиву routerLink:

template: `
  <nav>
    <a routerLink="">Главная</a>
    <a routerLink="/cart">Корзина</a>
  </nav>
  <router-outlet />
`

Как работает под капотом

Роутер слушает изменения URL (через History API браузера). При смене адреса он перебирает массив маршрутов сверху вниз, ищет первое совпадение пути и создаёт соответствующий компонент внутри <router-outlet>. Звёздочка ** ловит всё, что не совпало, — это маршрут 404. Порядок важен: более конкретные пути идут выше общих.

   URL: /cart
        |
   роутер перебирает routes сверху вниз:
     '' ............ нет
     'cart' ........ СОВПАЛО -> создать CartComponent
        |
   вставить в <router-outlet>  (без перезагрузки страницы)

Запускаемая врезка: роутер-матчер на JS

Сердце роутера — сопоставление пути с маршрутом. «Попробуй сам ▶».

// упрощённый матчер: ищет первый подходящий маршрут
function matchRoute(routes, url) {
  for (const route of routes) {
    if (route.path === '**') return route;          // ловит всё
    if (route.path === url) return route;           // точное совпадение
  }
  return null;
}

const routes = [
  { path: '', component: 'Home' },
  { path: 'cart', component: 'Cart' },
  { path: '**', component: 'NotFound' },
];

console.log(matchRoute(routes, '').component);       // Home
console.log(matchRoute(routes, 'cart').component);   // Cart
console.log(matchRoute(routes, 'xyz').component);    // NotFound

Частые ошибки

  • Использовать <a href> для внутренних ссылок. Это перезагрузит всё приложение; нужен routerLink.
  • Поставить ** не последним. Тогда он перехватит все пути, и остальные маршруты не сработают.
  • Забыть <router-outlet>. Без него роутеру некуда вставлять компоненты.

Best practices

  • Располагайте маршруты от конкретных к общим, ** — в самом конце.
  • Для навигации всегда используйте routerLink, а программно — Router.navigate().
  • Выносите маршруты в отдельный файл app.routes.ts для читаемости.

Итоги. Роутер сопоставляет URL с компонентами и рисует их в <router-outlet>; переходы — через routerLink. provideRouter(routes) включает всё это. Дальше — параметры маршрута и ленивая загрузка.

Закрепляем

Роутер создаёт иллюзию многостраничности поверх одностраничного приложения. Он слушает изменения URL, сопоставляет адрес с массивом маршрутов и рисует подходящий компонент в <router-outlet> — всё без перезагрузки страницы. Три вещи, которые должны стать рефлексом: маршруты задаются массивом и подключаются через provideRouter, текущий маршрут рендерится в <router-outlet>, а переходы делаются через routerLink, а не через обычный href.

Порядок маршрутов важен, и это частый источник недоумения. Роутер перебирает массив сверху вниз и берёт первое совпадение, поэтому более конкретные пути должны стоять выше общих, а перехватывающий всё подстановочный маршрут ** — обязательно последним. Если поставить ** в начало, он проглотит все адреса, и ни один реальный маршрут не сработает. Этот же **-маршрут — удобное место для страницы 404. Держите маршруты в отдельном файле app.routes.ts, чтобы навигация приложения читалась с одного взгляда.

ЭлементРоль
provideRouter(routes)Подключить роутер
<router-outlet>Место рендера маршрута
routerLinkНавигация без перезагрузки
path: '**'Маршрут 404 (всегда последний)
Проверьте себя
1. Куда роутер вставляет компонент текущего маршрута?
AВ тег body
BВ место, помеченное <router-outlet>
CВ <head>
DВ новый таб браузера
2. Почему для внутренней навигации используют routerLink, а не <a href>?
ArouterLink красивее
Bhref перезагружает всю страницу, а routerLink меняет маршрут без перезагрузки (SPA)
Chref не работает в Angular
DРазницы нет