Использование с Angular

Universal Router — это лёгкая библиотека для маршрутизации, основанная на принципе сопоставления URL с маршрутами через объекты и функции. В отличие от встроенного Angular Router, Universal Router предоставляет более гибкую модель маршрутов и позволяет легко интегрироваться с серверным рендерингом, SPA и различными состояниями приложения.

При использовании Universal Router с Angular важно учитывать архитектуру Angular-приложения: компоненты, сервисы и жизненный цикл Angular. Universal Router работает как чистая логика сопоставления пути и результата, поэтому его интеграция в Angular чаще всего осуществляется через сервисы и RxJS для реактивного обновления состояния.


Создание сервиса маршрутизации

Для управления маршрутизацией удобно создать отдельный сервис:

import { Injectable } from '@angular/core';
import UniversalRouter, { Route } from 'universal-router';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class RouterService {
  private router: UniversalRouter;
  private currentRouteSubject = new BehaviorSubject<string>(window.location.pathname);
  public currentRoute$ = this.currentRouteSubject.asObservable();

  constructor() {
    const routes: Route[] = [
      { path: '/', action: () => 'HomeComponent' },
      { path: '/about', action: () => 'AboutComponent' },
      { path: '/users/:id', action: (context) => `UserComponent with ID: ${context.params.id}` },
      { path: '(.*)', action: () => 'NotFoundComponent' }
    ];

    this.router = new UniversalRouter(routes);
  }

  async navigate(path: string) {
    const result = await this.router.resolve(path);
    this.currentRouteSubject.next(result);
    window.history.pushState({}, '', path);
  }

  async resolveCurrent() {
    const result = await this.router.resolve(window.location.pathname);
    this.currentRouteSubject.next(result);
  }
}

Ключевые моменты:

  • BehaviorSubject используется для хранения текущего маршрута и подписки на изменения из компонентов.
  • Метод navigate отвечает за изменение URL и обновление состояния.
  • Метод resolveCurrent позволяет при инициализации приложения определить текущий маршрут, что особенно важно для серверного рендеринга или при обновлении страницы.

Подключение к компонентам Angular

В Angular компоненты могут подписываться на поток маршрута и отображать соответствующий контент:

import { Component, OnInit } from '@angular/core';
import { RouterService } from './router.service';

@Component({
  selector: 'app-root',
  template: `
    <nav>
      <a (click)="go('/')">Home</a>
      <a (click)="go('/about')">About</a>
      <a (click)="go('/users/42')">User 42</a>
    </nav>
    <div>
      <ng-container [ngSwitch]="currentComponent">
        <app-home *ngSwitchCase="'HomeComponent'"></app-home>
        <app-about *ngSwitchCase="'AboutComponent'"></app-about>
        <app-user *ngSwitchCase="'UserComponent with ID: 42'"></app-user>
        <app-not-found *ngSwitchDefault></app-not-found>
      </ng-container>
    </div>
  `
})
export class AppComponent implements OnInit {
  currentComponent: string = '';

  constructor(private routerService: RouterService) {}

  ngOnInit() {
    this.routerService.currentRoute$.subscribe(component => {
      this.currentComponent = component;
    });
    this.routerService.resolveCurrent();
  }

  go(path: string) {
    this.routerService.navigate(path);
  }
}

Особенности интеграции:

  • ngSwitch используется для динамического отображения компонента в зависимости от результата Universal Router.
  • Подписка на currentRoute$ позволяет автоматически обновлять UI при навигации.
  • Все маршруты и логика маршрутизации централизованы в сервисе, что упрощает тестирование и поддержку.

Работа с параметрами маршрута

Universal Router поддерживает динамические сегменты URL через :param. Для Angular-компонентов параметры можно передавать через сервис:

const routes: Route[] = [
  { path: '/users/:id', action: (context) => ({ component: 'UserComponent', params: context.params }) }
];

Компонент для отображения пользователя:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user',
  template: `<p>User ID: {{ userId }}</p>`
})
export class UserComponent {
  @Input() userId!: string;
}

В AppComponent параметры маршрута извлекаются из объекта:

this.routerService.currentRoute$.subscribe(result => {
  if (typeof result === 'object' && result.component === 'UserComponent') {
    this.currentComponent = 'UserComponent';
    this.userId = result.params.id;
  }
});

Такой подход позволяет отделять отображение данных от логики маршрутизации.


Навигация с сохранением истории

Для SPA важно поддерживать корректную работу кнопок браузера «назад» и «вперед». Universal Router совместим с History API:

window.addEventListener('popstate', () => {
  this.routerService.resolveCurrent();
});
  • popstate срабатывает при изменении истории, что позволяет обновлять компонент без перезагрузки страницы.
  • Использование pushState и replaceState позволяет полностью контролировать историю URL.

Асинхронные действия и lazy loading

Universal Router поддерживает асинхронные функции action, что позволяет реализовать динамическую загрузку модулей:

const routes: Route[] = [
  { 
    path: '/about', 
    action: async () => {
      const module = await import('./about/about.module');
      return 'AboutComponent';
    }
  }
];

Преимущества:

  • Отложенная загрузка уменьшает начальный размер бандла.
  • Можно загружать данные с сервера прямо внутри action, что упрощает интеграцию с API.

В Angular это особенно полезно для больших приложений с множеством ленивых модулей.


Совместная работа с Angular Guard

Universal Router не имеет встроенных Guard, как Angular Router, но аналогичное поведение реализуется через проверку в action:

const routes: Route[] = [
  { 
    path: '/admin', 
    action: async (context) => {
      const isAuthenticated = await authService.checkAuth();
      if (!isAuthenticated) return '/login';
      return 'AdminComponent';
    }
  }
];
  • Редирект возвращается как строка пути.
  • Можно интегрировать с Angular сервисами для аутентификации и авторизации.

Такой подход сохраняет гибкость маршрутизатора и возможность централизованного контроля доступа.


Интеграция с Angular Universal

Для серверного рендеринга важно предварительно вычислить маршрут на сервере и передать компонент с данными в шаблон. Universal Router отлично подходит для Angular Universal, так как не зависит от браузерного History API:

const routeResult = await router.resolve(req.url);
res.render('index', { component: routeResult });
  • На сервере можно выполнять асинхронные операции, запросы к API и вычисления маршрута перед отправкой HTML.
  • Это повышает производительность и улучшает SEO, сохраняя SPA-подход на клиенте.

Резюме архитектурных особенностей

  • Universal Router в Angular лучше использовать через сервис с реактивным потоком маршрутов.
  • Параметры маршрута передаются через объекты, а отображение реализуется динамически через ngSwitch или ngComponentOutlet.
  • Поддерживаются асинхронные действия, ленивые модули и серверный рендеринг.
  • История браузера и редиректы управляются вручную через History API и логику в сервисе.
  • Защита маршрутов и авторизация реализуется в action, заменяя стандартные Guard Angular.

Такой подход позволяет комбинировать преимущества Angular и легковесной гибкости Universal Router, сохраняя централизованную и легко тестируемую архитектуру приложения.