Spec-Zone.ru › Angular

Резолверы данных

Резолверы данных позволяют получать данные до перехода на маршрут, чтобы компоненты получили необходимые данные до отрисовки. Это помогает избежать состояний загрузки и улучшить взаимодействие с пользователем за счёт предварительной загрузки важных данных.

Что такое резолверы данных?

Резолвер данных — это сервис, реализующий функцию ResolveFn. Он запускается до активации маршрута и может получать данные из API, баз данных или других источников. Разрешённые данные становятся доступны компоненту через ActivatedRoute.

Резолверам данных доступны сервисы, предоставленные на уровне маршрута, а также сведения о конкретном маршруте через аргумент route.

Зачем использовать резолверы данных?

Резолверы данных помогают решать распространённые задачи маршрутизации:

  • Предотвращение пустых состояний: компоненты сразу получают данные при загрузке
  • Улучшение взаимодействия с пользователем: никаких индикаторов загрузки для критически важных данных
  • Обработка ошибок: обработка ошибок при получении данных до перехода
  • Согласованность данных: обеспечение наличия необходимых данных до отрисовки, что важно для SSR

Создание резолвера

Чтобы создать резолвер, напишите функцию с типом ResolveFn.

Она получает в качестве параметров ActivatedRouteSnapshot и RouterStateSnapshot.

Ниже приведён резолвер, который получает сведения о пользователе до отрисовки маршрута с помощью функции inject:

import {inject} from '@angular/core';
import {UserStore, SettingsStore} from './user-store';
import type {ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot} from '@angular/router';
import type {User, Settings} from './types';

export const userResolver: ResolveFn<User> = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
) => {
  const userStore = inject(UserStore);
  const userId = route.paramMap.get('id')!;
  return userStore.getUser(userId);
};

export const settingsResolver: ResolveFn<Settings> = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
) => {
  const settingsStore = inject(SettingsStore);
  const userId = route.paramMap.get('id')!;
  return settingsStore.getUserSettings(userId);
};

Настройка маршрутов с резолверами

Чтобы добавить к маршруту один или несколько резолверов данных, укажите их в ключе resolve конфигурации маршрута. Тип Routes определяет структуру конфигураций маршрутов:

import {Routes} from '@angular/router';

export const routes: Routes = [
  {
    path: 'user/:id',
    component: UserDetail,
    resolve: {
      user: userResolver,
      settings: settingsResolver,
    },
  },
];

Подробнее о конфигурации resolve можно узнать в документации API.

Доступ к разрешённым данным в компонентах

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

Получить доступ к разрешённым данным в компоненте можно, обратившись к данным снимка из ActivatedRoute с помощью функции signal:

import {Component, inject, computed} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {toSignal} from '@angular/core/rxjs-interop';
import type {User, Settings} from './types';

@Component({
  template: `
    <h1>{{ user().name }}</h1>
    <p>{{ user().email }}</p>
    <div>Theme: {{ settings().theme }}</div>
  `,
})
export class UserDetail {
  private route = inject(ActivatedRoute);
  private data = toSignal(this.route.data);
  user = computed(() => this.data().user as User);
  settings = computed(() => this.data().settings as Settings);
}

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

Другой способ получить доступ к разрешённым данным — использовать withComponentInputBinding() при настройке маршрутизатора с помощью provideRouter. Благодаря этому разрешённые данные передаются напрямую в качестве входных параметров компонента:

import {bootstrapApplication} from '@angular/platform-browser';
import {provideRouter, withComponentInputBinding} from '@angular/router';
import {routes} from './app.routes';

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

При такой конфигурации можно объявить в компоненте входные параметры, соответствующие ключам резолверов, используя функцию input и input.required для обязательных входных параметров:

import {Component, input} from '@angular/core';
import type {User, Settings} from './types';

@Component({
  template: `
    <h1>{{ user().name }}</h1>
    <p>{{ user().email }}</p>
    <div>Theme: {{ settings().theme }}</div>
  `,
})
export class UserDetail {
  user = input.required<User>();
  settings = input.required<Settings>();
}

Этот подход обеспечивает более строгую типизацию и позволяет не внедрять ActivatedRoute только для доступа к разрешённым данным.

Обработка ошибок в резолверах

При сбоях навигации важно корректно обрабатывать ошибки в резолверах данных. В противном случае возникнет NavigationError, и переход на текущий маршрут завершится неудачей, что ухудшит взаимодействие с пользователями.

Есть три основных способа обрабатывать ошибки в резолверах данных:

  1. Централизация обработки ошибок с помощью withNavigationErrorHandler
  2. Обработка ошибок через подписку на события маршрутизатора
  3. Обработка ошибок непосредственно в резолвере

Централизация обработки ошибок с помощью withNavigationErrorHandler

Средство withNavigationErrorHandler позволяет централизованно обрабатывать все ошибки навигации, в том числе возникающие из-за сбоев резолверов данных. Этот подход позволяет хранить логику обработки ошибок в одном месте и не дублировать код обработки ошибок в разных резолверах.

import {bootstrapApplication} from '@angular/platform-browser';
import {provideRouter, withNavigationErrorHandler} from '@angular/router';
import {inject} from '@angular/core';
import {Router} from '@angular/router';
import {routes} from './app.routes';

bootstrapApplication(App, {
  providers: [
    provideRouter(
      routes,
      withNavigationErrorHandler((error) => {
        const router = inject(Router);

        if (error?.message) {
          console.error('Navigation error occurred:', error.message);
        }

        router.navigate(['/error']);
      }),
    ),
  ],
});

При такой конфигурации резолверы могут сосредоточиться на получении данных, а централизованный обработчик будет управлять сценариями ошибок:

export const userResolver: ResolveFn<User> = (route) => {
  const userStore = inject(UserStore);
  const userId = route.paramMap.get('id')!;
  // No need for explicit error handling - let it bubble up
  return userStore.getUser(userId);
};

Обработка ошибок через подписку на события маршрутизатора

Ошибки резолверов также можно обрабатывать, подписавшись на события маршрутизатора и отслеживая события NavigationError. Этот подход обеспечивает более точный контроль над обработкой ошибок и позволяет реализовать пользовательскую логику восстановления после сбоев.

import {Component, inject, signal} from '@angular/core';
import {Router, NavigationError} from '@angular/router';
import {toSignal} from '@angular/core/rxjs-interop';
import {map} from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    @if (errorMessage()) {
      <div class="error-banner">
        {{ errorMessage() }}
        <button (click)="retryNavigation()">Retry</button>
      </div>
    }
    <router-outlet />
  `,
})
export class App {
  private router = inject(Router);
  private lastFailedUrl = signal('');

  private navigationErrors = toSignal(
    this.router.events.pipe(
      map((event) => {
        if (event instanceof NavigationError) {
          this.lastFailedUrl.set(event.url);

          if (event.error) {
            console.error('Navigation error', event.error);
          }

          return 'Navigation failed. Please try again.';
        }
        return '';
      }),
    ),
    {initialValue: ''},
  );

  errorMessage = this.navigationErrors;

  retryNavigation() {
    if (this.lastFailedUrl()) {
      this.router.navigateByUrl(this.lastFailedUrl());
    }
  }
}

Этот подход особенно полезен, если необходимо:

  • Реализовать собственную логику повторных попыток при сбое навигации
  • Показывать определённые сообщения об ошибках в зависимости от типа сбоя
  • Отслеживать сбои навигации для аналитики

Обработка ошибок непосредственно в резолвере

Ниже приведён обновлённый пример userResolver, который записывает ошибку в журнал и возвращает пользователя на общую страницу /users с помощью сервиса Router:

import {inject} from '@angular/core';
import {ResolveFn, RedirectCommand, Router} from '@angular/router';
import {catchError, of, EMPTY} from 'rxjs';
import {UserStore} from './user-store';
import type {User} from './types';

export const userResolver: ResolveFn<User | RedirectCommand> = (route) => {
  const userStore = inject(UserStore);
  const router = inject(Router);
  const userId = route.paramMap.get('id')!;

  return userStore.getUser(userId).pipe(
    catchError((error) => {
      console.error('Failed to load user:', error);
      return of(new RedirectCommand(router.parseUrl('/users')));
    }),
  );
};

Особенности загрузки при навигации

Резолверы данных помогают избежать состояний загрузки внутри компонентов, но создают другую особенность взаимодействия с пользователем: навигация блокируется на время выполнения резолверов. Пользователи могут столкнуться с задержкой между нажатием ссылки и отображением нового маршрута, особенно при медленном сетевом соединении.

Отображение состояния навигации

Чтобы улучшить взаимодействие с пользователем во время выполнения резолверов, можно отслеживать события маршрутизатора и показывать индикаторы загрузки:

import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';

@Component({
  selector: 'app-root',
  template: `
    @if (isNavigating()) {
      <div class="loading-bar">Loading...</div>
    }
    <router-outlet />
  `,
})
export class App {
  private router = inject(Router);
  isNavigating = computed(() => !!this.router.currentNavigation());
}

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

Рекомендации

  • Не перегружайте резолверы: резолверы должны получать только необходимые данные, а не всё, что может понадобиться странице
  • Обрабатывайте ошибки: всегда корректно обрабатывайте ошибки, чтобы обеспечить пользователям наилучшее взаимодействие
  • Используйте кэширование: рассмотрите возможность кэширования разрешённых данных для повышения производительности
  • Учитывайте взаимодействие при навигации: добавьте индикаторы загрузки на время выполнения резолверов, поскольку получение данных блокирует навигацию
  • Устанавливайте разумные тайм-ауты: избегайте резолверов, которые могут зависнуть на неопределённое время и заблокировать навигацию
  • Строгая типизация: используйте интерфейсы TypeScript для разрешённых данных

Чтение разрешённых данных родительского маршрута во вложенных резолверах

Резолверы выполняются от родительского маршрута к дочернему. Если для родительского маршрута задан резолвер, его разрешённые данные будут доступны дочерним резолверам, которые запускаются после него.

import { inject } from '@angular/core';
import { provideRouter , ActivatedRouteSnapshot } from '@angular/router';
import { userResolver } from './resolvers';
import { UserPosts } from './pages';
import { PostService } from './services',
import type { User } from './types';

provideRouter([
  {
    path: 'users/:id',
    resolve: { user: userResolver }, // user resolver in the parent route
    children: [
      {
        path: 'posts',
        component: UserPosts,
        // route.data.user is available here while this resolver runs
        resolve: {
          posts: (route: ActivatedRouteSnapshot) => {
            const postService = inject(PostService);
            const user = route.parent?.data['user'] as User; // parent data
            const userId = user.id;
            return postService.getPostByUser(userId);
          },
        },
      },
    ],
  },
]);

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/routing/data-resolvers

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API