Spec-Zone.ru › Angular 2

Маршрутизация и навигация

Изучите основы навигации по экранам с помощью Angular Router.

Angular Router позволяет переходить от одного представления к другому по мере выполнения пользователем задач приложения.

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

Чтобы увидеть изменения URL в адресной строке браузера в примере, откройте его снова в редакторе Plunker, щелкнув значок в правом верхнем углу, затем выведите окно предварительного просмотра, нажав синюю кнопку 'X' в правом верхнем углу.

pop out the windowpop out the window

Обзор

Браузер — это знакомая модель навигации по приложению:

  • Введите URL в адресную строку, и браузер перейдет на соответствующую страницу.
  • Щелкните ссылки на странице, и браузер перейдет на новую страницу.
  • Нажмите кнопки «Назад» и «Вперед» браузера, и браузер переместится назад и вперед по истории посещенных страниц.

Angular Router («маршрутизатор») заимствует эту модель. Он может интерпретировать URL браузера как инструкцию перехода к клиентскому представлению. Он может передавать необязательные параметры вместе с поддерживающим компонентом представления, которые помогут ему решить, какой именно контент отобразить. Вы можете связать маршрутизатор со ссылками на странице, и при щелчке ссылки пользователь будет перенаправлен на соответствующее представление приложения. Вы можете выполнить навигацию императивно, когда пользователь нажимает кнопку, выбирает из раскрывающегося списка или в ответ на любой другой стимул из любого источника. А маршрутизатор регистрирует действия в журнале истории браузера, поэтому кнопки «Назад» и «Вперед» также работают.

Содержание

  • Основы
    • <base href>
    • Импорт маршрутизатора
    • Настройка
    • Выход маршрутизатора
    • Ссылки маршрутизатора
    • Состояние маршрутизатора
    • Резюме
  • Пример приложения
  • Этап 1: Начало работы с маршрутизатором
    • Установка базового href
    • Импорт из библиотеки маршрутизатора
    • Определение маршрутов
    • Оболочка AppComponent
    • RouterOutlet
    • RouterLink binding
    • RouterLinkActive привязка
    • Маршрут-заполнитель
    • Маршрут по умолчанию к героям
  • Этап 2: Модуль маршрутизации
    • Рефакторинг конфигурации маршрутизации в модуль маршрутизации
    • Нужен ли модуль маршрутизации?
  • Этап 3: Функциональность героев
    • Добавление функциональности героев
    • Требования к маршрутизации функции героя
    • Конфигурация маршрутов функции героя
    • Добавление модуля маршрутизации в HeroesModule
    • Удаление дублируемых маршрутов героя
    • Импорт модуля героя в AppModule
    • Порядок импорта модуля имеет значение
    • Определение маршрута с параметром
    • Переход к подробному представлению героя императивно
    • Установка параметров маршрута в представлении списка
    • ActivatedRoute: единственный источник информации о маршруте
    • Переиспользование компонента с параметрами Observable
    • Снимок: альтернатива no-observable
    • Возврат к компоненту списка
    • Параметры маршрута: обязательные или необязательные?
    • Список героев: необязательный выбор героя
    • Параметры маршрута в сервисе ActivatedRoute
    • Добавление анимации к маршрутизируемому компоненту
    • Заключение этапа 3
  • Этап 4: Функциональность центра кризисов
    • Центр кризисов с дочерними маршрутами
    • Компонент дочерней маршрутизации
    • Конфигурация дочерних маршрутов
    • Импорт модуля центра кризисов в маршруты AppModule
    • Относительная навигация
    • Переход к детальному представлению кризиса с относительным URL
    • Отображение нескольких маршрутов в именованных выходах
    • Вторичные маршруты
    • Добавление вторичного маршрута
    • Навигация по вторичным маршрутам: слияние маршрутов во время навигации
    • Очистка вторичных маршрутов
  • Этап 5: Стражи маршрутов
    • CanActivate: требование аутентификации
    • Компонент-безопасный маршрут: группировка маршрутов без компонента
    • Защита функции администратора
    • Обучение AuthGuard аутентификации
    • Добавление компонента входа
    • CanActivateChild: защита дочерних маршрутов
    • CanDeactivate: обработка несохраненных изменений
    • Отмена и сохранение
    • Resolve: предварительная выборка данных компонента
    • Загрузка данных перед навигацией
    • Параметры запроса и фрагменты
  • Этап 6: Асинхронная маршрутизация
    • Конфигурация ленивой загрузки маршрутов
    • Страж CanLoad: защита от несанкционированной загрузки модулей функций
    • Предварительная загрузка: фоновая загрузка областей функций
    • Как работает предварительная загрузка
    • Ленивая загрузка центра кризисов
    • CanLoad блокирует предварительную загрузку
    • Настройка стратегии предварительной загрузки
    • Проверка конфигурации маршрутизатора
    • Заключение и финальное приложение
  • Приложения
    • Приложение: массив параметров ссылки
    • Приложение: LocationStrategy и стили URL браузера

Основы

Это руководство выполняется поэтапно, отмечено этапами, начиная с простой двухстраничной структуры и переходя к модульной структуре с несколькими представлениями и дочерними маршрутами.

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

<base href>

Большинство приложений с маршрутизацией должны добавить элемент <base> в index.html как первого дочернего элемента тега <head>, чтобы сообщить маршрутизатору, как составлять URL навигации.

Если папка app является корнем приложения, как это сделано в примере приложения, установите значение href точно так, как показано здесь.

src/index.html (base-href)

<base href="/">

Импорт маршрутизатора

Angular Router — это необязательный сервис, который отображает определенное представление компонента для данного URL. Он не входит в ядро Angular. Он находится в собственной библиотечной пачке, @angular/router. Импортируйте необходимые элементы так же, как вы это делаете с любым другим пакетом Angular.

src/app/app.module.ts (import)

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

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

Настройка

Маршрутизированное приложение Angular имеет один экземпляр сервиса Router. Когда URL браузера меняется, этот маршрутизатор ищет соответствующий Route, чтобы определить отображаемый компонент.

У маршрутизатора нет маршрутов, пока вы их не настроите. В следующем примере создаются четыре определения маршрутов, маршрутизатор настраивается с помощью метода RouterModule.forRoot, а результат добавляется в массив AppModule модуля imports.

src/app/app.module.ts (excerpt)

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'hero/:id',      component: HeroDetailComponent },
  {
    path: 'heroes',
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
    // other imports here
  ],
  ...
})
export class AppModule { }

Массив appRoutes маршрутов описывает, как переходить. Передайте его методу RouterModule.forRoot в модуле imports для настройки маршрутизатора.

Каждый Route сопоставляет URL path с компонентом. В пути нет лидирующих слешей. Маршрутизатор анализирует и создает окончательный URL за вас, позволяя использовать как относительные, так и абсолютные пути при переходе между представлениями приложения.

:id в первом маршруте — это маркер для параметра маршрута. В URL, таком как /hero/42, «42» — это значение параметра id. Соответствующий HeroDetailComponent использует это значение для поиска и отображения героя, чей id равен 42. Вы узнаете больше о параметрах маршрута позже в этом руководстве.

Свойство data в третьем маршруте — это место для хранения произвольных данных, связанных с этим конкретным маршрутом. Свойство data доступно в каждом активированном маршруте. Используйте его для хранения таких элементов, как заголовки страниц, текст хлебных крошек и другие статические, только для чтения данные. Позже в руководстве вы будете использовать страж resolve guard для получения динамических данных.

empty path в четвертом маршруте представляет собой путь по умолчанию для приложения, место, куда следует переходить, когда путь в URL пуст, как это обычно бывает в начале. Этот маршрут по умолчанию перенаправляет на маршрут для URL /heroes и, следовательно, отображает HeroesListComponent.

END_OF_DOCUMENT_MARKER

Путь ** в последнем маршруте является подстановкой. Маршрутизатор выберет этот маршрут, если запрашиваемый URL не соответствует ни одному пути для маршрутов, определённых ранее в конфигурации. Это полезно для отображения страницы «404 — Не найдено» или перенаправления на другой маршрут.

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

Выходной элемент маршрутизатора

При указанном выше конфигурации, когда URL браузера для этого приложения станет /heroes, маршрутизатор сопоставит этот URL с маршрутным путём /heroes и отобразит HeroListComponent после RouterOutlet, который вы разместили в HTML компонента-хоста.

<router-outlet></router-outlet>
<!-- Routed views go here -->

Маршрутные ссылки

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

Рассмотрим следующий шаблон:

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

Директивы RouterLink на якорях предоставляют маршрутизатору управление этими элементами. Пути навигации фиксированы, поэтому вы можете назначить строку для routerLink (связывание «один раз»).

Если бы путь навигации был более динамичным, вы могли бы связать его с выражением шаблона, возвращающим массив параметров ссылки маршрута (массив параметров ссылки). Маршрутизатор преобразует этот массив в полный URL.

Директива RouterLinkActive на каждом якоре помогает визуально отличить якорь от текущего выбранного «активного» маршрута. Маршрутизатор добавляет CSS-класс active к элементу, когда связанный RouterLink становится активным. Вы можете добавить эту директиву к якорю или его родительскому элементу.

Состояние маршрутизатора

После завершения каждого успешного цикла навигации маршрутизатор создаёт дерево объектов ActivatedRoute, которые составляют текущее состояние маршрутизатора. Вы можете получить доступ к текущему RouterState из любого места приложения, используя сервис Router и свойство routerState.

Каждый ActivatedRoute в RouterState предоставляет методы для перемещения вверх и вниз по дереву маршрутов для получения информации из родительских, дочерних и братских маршрутов.

Резюме

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

Вот ключевые Router термины и их значения:

Часть маршрутизатора Значение
Router Отображает компонент приложения для активного URL. Управляет переходом от одного компонента к другому.
RouterModule Отдельный модуль Angular, который предоставляет необходимые поставщики сервисов и директивы для навигации по представлениям приложения.
Routes Определяет массив маршрутов, каждый из которых сопоставляет URL-путь с компонентом.
Route Определяет, как маршрутизатор должен перейти к компоненту на основе шаблона URL. Большинство маршрутов состоят из пути и типа компонента.
RouterOutlet Директива (<router-outlet>), которая отмечает место, где маршрутизатор отображает представление.
RouterLink Директива для привязки нажимаемого HTML-элемента к маршруту. Нажатие на элемент с директивой routerLink, привязанной к строке или массиву параметров ссылки, запускает навигацию.
RouterLinkActive Директива для добавления/удаления классов из HTML-элемента, когда связанный routerLink, содержащийся в элементе или внутри него, становится активным/неактивным.
ActivatedRoute Сервис, предоставляемый каждому компоненту маршрута, который содержит информацию о конкретном маршруте, такую как параметры маршрута, статические данные, разрешённые данные, глобальные параметры запроса и глобальный фрагмент.
RouterState Текущее состояние маршрутизатора, включая дерево активированных маршрутов, вместе с удобными методами для перемещения по дереву маршрутов.
Массив параметров ссылки Массив, который маршрутизатор интерпретирует как инструкцию маршрутизации. Вы можете привязать этот массив к RouterLink или передать массив в качестве аргумента методу Router.navigate.
Компонент маршрутизации Компонент Angular с RouterOutlet, который отображает представления на основе навигации маршрутизатора.

Образцовое приложение

Это руководство описывает разработку образцового многостраничного приложения с маршрутизацией. В процессе оно выделяет решения по проектированию и описывает ключевые функции маршрутизатора, такие как:

  • Организация функций приложения в модулях.
  • Переход к компоненту (ссылка Heroes на «Список героев»).
  • Включение параметра маршрута (передача параметра героя id при переходе к «Подробно о герое»).
  • Вложенные маршруты (у Центра кризисов есть свои собственные маршруты).
  • Страж CanActivate (проверка доступа к маршруту).
  • Страж CanActivateChild (проверка доступа к дочернему маршруту).
  • Страж CanDeactivate (запрос разрешения на удаление незасохранённых изменений).
  • Страж Resolve (предварительная загрузка данных маршрута).
  • Ленивая загрузка модулей функций.
  • Страж CanLoad (проверка перед загрузкой активов модуля функций).

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

Полный исходный код окончательной версии приложения можно просмотреть и скачать в живом примере.

Образцовое приложение в действии

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

Приложение имеет три основные функциональные области:

  1. Центр кризисов для ведения списка кризисов для распределения героям.
  2. Область Героев для ведения списка героев, нанятых агентством.
  3. Область Администратора для управления списком кризисов и героев.

Попробуйте его, нажав на эту ссылку на живой пример.

После того, как приложение загрузится, вы увидите строку навигационных кнопок и представление Героев со списком героев.

Hero List

Выберите одного героя, и приложение переведёт вас на экран редактирования героя.

Crisis Center Detail

Измените имя. Нажмите кнопку «Назад», и приложение вернётся к списку героев, где отобразится изменённое имя героя. Обратите внимание, что изменение имени произошло немедленно.

Если бы вы нажали кнопку «Назад» браузера вместо кнопки «Назад», приложение также вернуло бы вас в список героев. Навигация в приложении Angular обновляет историю браузера, как и обычная веб-навигация.

Теперь нажмите ссылку Центр кризисов для просмотра списка текущих кризисов.

Crisis Center List

Выберите кризис, и приложение переведёт вас на экран редактирования кризиса. Подробности о кризисе отобразятся в дочернем представлении на той же странице, под списком.

Измените имя кризиса. Обратите внимание, что соответствующее имя в списке кризисов не изменяется.

Crisis Center Detail

В отличие от Подробностей о герое, которые обновляются по мере ввода, изменения в Подробностях о кризисе временные, пока вы их не сохраните или не отбросите, нажав кнопки «Сохранить» или «Отмена». Обе кнопки перенаправляют вас обратно в Центр кризисов и его список кризисов.

Пока не нажимайте ни одну из кнопок. Вместо этого нажмите кнопку «Назад» браузера или ссылку «Герои».

Появится диалоговое окно.

Confirm Dialog

Вы можете нажать «ОК» и потерять изменения или нажать «Отмена» и продолжить редактирование.

За этим поведением стоит страж навигации CanDeactivate маршрутизатора. Страж даёт вам возможность выполнить очистку или запросить разрешение пользователя перед переходом от текущего представления.

Кнопки Admin и Login иллюстрируют другие возможности маршрутизатора, которые будут рассмотрены позже в руководстве. На данном этапе этого краткого введения будет достаточно.

Переходите к первому этапу приложения.

Этап 1: Начало работы с маршрутизатором

Начните с простой версии приложения, которая переходит между двумя пустыми представлениями.

App in action

Установите <base href>

Маршрутизатор использует history.pushState браузера для навигации. Благодаря pushState, вы можете сделать пути URL в приложении такими, как вам нужно, например, localhost:3000/crisis-center. Пути URL в приложении могут быть неотличимы от серверных URL.

Современные браузеры HTML5 были первыми, кто поддержали pushState, поэтому многие люди называют эти URL «URL в стиле HTML5».

Навигация в стиле HTML5 — это значение маршрутизатора по умолчанию. В приложении Стратегии расположения и стили URL браузера вы узнаете, почему предпочтительно использовать стиль HTML5, как настроить его поведение и как перейти к более старому стилю с хэш (#), если это необходимо.

Вы должны добавить элемент <base href> в index.html приложения для работы pushState маршрутизации. Браузер использует значение <base href> для добавления префикса к относительным URL при ссылке на файлы CSS, скрипты и изображения.

Добавьте элемент <base> сразу после тега <head>. Если папка app является корневой папкой приложения, как это и есть в данном приложении, установите значение href в index.html точно так, как показано здесь.

src/index.html (base-href)

<base href="/">
Примечание к примерам наглядно

Окружение живого кодирования, подобное Plunker, динамически задаёт базовый адрес приложения, поэтому вы не можете указать фиксированный адрес. Вот почему код примера заменяет <base href...> скриптом, который пишет тег <base> на лету.

<script>document.write('<base href="' + document.location + '" />');</script>

Этот приём нужен только для примеров, а не для производственного кода.

Импорт из библиотеки маршрутизатора

Начните с импорта некоторых символов из библиотеки маршрутизатора. Маршрутизатор находится в собственном пакете @angular/router. Он не входит в ядро Angular. Маршрутизатор — это необязательная служба, поскольку не все приложения нуждаются в маршрутизации, и, в зависимости от ваших требований, вам может потребоваться другая библиотека маршрутизации.

Вы обучаете маршрутизатор навигации, настраивая его с помощью маршрутов.

Определение маршрутов

Маршрутизатор должен быть настроен со списком определений маршрутов.

Первая настройка определяет массив из двух маршрутов с простыми путями, ведущими к CrisisListComponent и HeroListComponent.

Каждое определение преобразуется в объект Route, который имеет две вещи: path, сегмент пути URL для этого маршрута; и component, компонент, связанный с этим маршрутом.

Маршрутизатор опирается на свой реестр определений, когда изменяется URL-адрес браузера или когда код приложения сообщает маршрутизатору перейти по пути маршрута.

Проще говоря, вы можете сказать об этом первом маршруте:

  • Когда адрес URL в браузере изменится, чтобы соответствовать сегменту пути /crisis-center, тогда маршрутизатор активирует экземпляр CrisisListComponent и отобразит его представление.

  • Когда приложение запрашивает навигацию по пути /crisis-center, маршрутизатор активирует экземпляр CrisisListComponent, отображает его представление и обновляет адресную строку и историю браузера с URL этого пути.

Вот первая настройка. Передайте массив маршрутов, appRoutes, методу RouterModule.forRoot. Он возвращает модуль, содержащий настроенный поставщик службы Router, а также другие поставщики, необходимые библиотеке маршрутизации. После загрузки приложения Router выполняет начальную навигацию на основе текущего URL-адреса браузера.

src/app/app.module.ts (first-config)

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent }          from './app.component';
import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes', component: HeroListComponent },
];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(appRoutes)
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Добавление настроенного RouterModule в AppModule достаточно для простых конфигураций маршрутов. По мере роста приложения вы захотите переработать конфигурацию маршрутизации в отдельный файл и создать модуль маршрутизации, специальный тип Service Module, посвящённый маршрутизации в модулях функций.

Обеспечение RouterModule в AppModule делает маршрутизатор доступным повсюду в приложении.

Оболочка AppComponent

Корневой AppComponent — это оболочка приложения. В ней есть заголовок, навигационная панель с двумя ссылками и выход маршрутизатора, где маршрутизатор переключает представления на странице. Вот что вы получаете:

Shell

Соответствующий шаблон компонента выглядит так:

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

RouterOutlet

RouterOutlet — это директива из библиотеки маршрутизатора, которая отмечает место в шаблоне, где маршрутизатор должен отображать представления для этого выхода.

Маршрутизатор добавляет элемент <router-outlet> в DOM и впоследствии вставляет элемент отображаемого представления сразу после элемента <router-outlet>.

Привязка RouterLink

Выше выхода, внутри тегов якоря, вы видите привязки атрибутов к директиве RouterLink, которые выглядят как routerLink="...".

Ссылки в этом примере имеют строковый путь, путь маршрута, который вы настраивали ранее. Пока нет параметров маршрута.

Вы также можете добавить больше контекстной информации к RouterLink, предоставив параметры строки запроса или фрагмент URL для перехода к различным областям страницы. Параметры строки запроса предоставляются через привязку [queryParams], которая принимает объект (например, { name: 'value' }), а фрагмент URL принимает одно значение, привязанное к входной привязке [fragment].

Узнайте, как вы также можете использовать массив параметров ссылки в приложении ниже.

Привязка RouterLinkActive

На каждом теге якоря вы также видите привязки свойств к директиве RouterLinkActive, которые выглядят как routerLinkActive="...".

Выражение шаблона справа от знака равно (=) содержит разделенный пробелом строку CSS-классов, которые маршрутизатор добавит, когда эта ссылка активна (и удалит, когда ссылка неактивна). Вы также можете установить директиву RouterLinkActive на строку классов, например [routerLinkActive]="active fluffy", или привязать её к свойству компонента, которое возвращает такую строку.

Директива RouterLinkActive переключает CSS-классы для активных RouterLink в зависимости от текущего RouterState. Это распространяется по всей иерархии маршрута, поэтому родительские и дочерние ссылки на маршруты могут быть активны одновременно. Для отмены этого поведения вы можете привязать входную привязку [routerLinkActiveOptions] с выражением { exact: true }. Используя { exact: true }, данная RouterLink будет активна только в том случае, если её URL точно соответствует текущему URL.

Директивы Router

RouterLink, RouterLinkActive и RouterOutlet — это директивы, предоставляемые пакетом Angular RouterModule. Они готовы к использованию в шаблоне.

Текущее состояние app.component.ts выглядит так:

src/app/app.component.ts (excerpt)

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

@Component({
  selector: 'my-app',
  template: `
    <h1>Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent { }

Маршрут подстановки

Вы создали два маршрута в приложении до сих пор, один для /crisis-center, а другой для /heroes. Любой другой URL-адрес приводит к тому, что маршрутизатор генерирует ошибку и завершает приложение.

Добавьте маршрут подстановки для перехвата недействительных URL-адресов и обработки их должным образом. Маршрут подстановки имеет путь, состоящий из двух звёздочек. Он соответствует любому URL-адресу. Маршрутизатор выберет этот маршрут, если не сможет сопоставить маршрут ранее в конфигурации. Маршрут подстановки может перейти к настраиваемому компоненту «404 Не найдено» или перенаправить на существующий маршрут.

Маршрутизатор выбирает маршрут с помощью стратегии «первое совпадение — победа». Маршруты подстановки — это наименее специфические маршруты в конфигурации маршрутов. Убедитесь, что это последний маршрут в конфигурации.

Для проверки этой функции добавьте кнопку с RouterLink в шаблон HeroListComponent и установите ссылку на "/sidekicks".

src/app/hero-list.component.ts (excerpt)

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

@Component({
  template: `
    <h2>HEROES</h2>
    <p>Get your heroes here</p>

    <button routerLink="/sidekicks">Go to sidekicks</button>
  `
})
export class HeroListComponent { }

Приложение завершится ошибкой, если пользователь нажмёт эту кнопку, потому что вы ещё не определили маршрут "/sidekicks".

Вместо добавления маршрута "/sidekicks" определите вместо этого маршрут wildcard и настройте его на переход к простому компоненту PageNotFoundComponent.

src/app/app.module.ts (wildcard)

{ path: '**', component: PageNotFoundComponent }

Создайте компонент PageNotFoundComponent для отображения, когда пользователи посещают недействительные URL-адреса.

src/app/not-found.component.ts (404 component)

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

@Component({
  template: '<h2>Page not found</h2>'
})
export class PageNotFoundComponent {}

Как и другие компоненты, добавьте PageNotFoundComponent в объявления AppModule.

Теперь, когда пользователь посещает /sidekicks или любой другой недействительный URL-адрес, браузер отображает «Страница не найдена». Адресная строка браузера по-прежнему указывает на недействительный URL.

По умолчанию маршрут к героям

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

localhost:3000

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

Было бы лучше, если бы приложение имело маршрут по умолчанию, который сразу отображал список героев, точно так же, как он будет отображаться при нажатии пользователем ссылки «Герои» или вставлении localhost:3000/heroes в адресную строку.

Перенаправление маршрутов

Предпочтительное решение — добавить маршрут redirect, который преобразует начальный относительный URL ('') в желаемый путь по умолчанию (/heroes). Адресная строка браузера показывает .../heroes, как будто вы перешли по нему напрямую.

Добавьте маршрут по умолчанию где-то над маршрутом подстановки. Он находится как раз над маршрутом подстановки в следующем отрывке, показывающем полную конфигурацию appRoutes для этого этапа.

src/app/app-routing.module.ts (appRoutes)

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes',        component: HeroListComponent },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

Маршрут перенаправления требует свойства pathMatch, чтобы сообщить маршрутизатору, как сопоставлять URL с путём маршрута. Маршрутизатор генерирует ошибку, если вы этого не сделаете. В этом приложении маршрутизатор должен выбрать маршрут к HeroListComponent только тогда, когда весь URL совпадает с '', поэтому установите значение pathMatch в 'full'.

Технически, pathMatch = 'full' приводит к попаданию на маршрут, когда оставшиеся, несопоставленные сегменты URL совпадают с ''. В этом примере переадресация находится на маршруте верхнего уровня, поэтому оставшийся URL и весь URL являются одним и тем же.

Другое возможное значение pathMatch — 'prefix', которое указывает маршрутизатору сопоставлять маршрут переадресации, когда оставшийся URL начинается с префикса пути маршрута переадресации.

Не делайте этого здесь. Если значение pathMatch было 'prefix', каждый URL соответствовал бы ''.

Попробуйте установить его на 'prefix', затем нажмите кнопку Go to sidekicks. Помните, что это плохой URL, и вы должны увидеть страницу "Страница не найдена". Вместо этого вы все еще на странице "Герои". Введите плохой URL в адресную строку браузера. Вы мгновенно перенаправлены на /heroes. Каждый URL, хороший или плохой, который попадает на этот маршрут, будет соответствовать.

Маршрут по умолчанию должен перенаправлять на HeroListComponent только тогда, когда весь URL равен ''. Не забудьте восстановить переадресацию на pathMatch = 'full'.

Дополнительную информацию см. в статье Виктора Савкина о переадресациях.

Краткий итог основ

У вас есть очень простое приложение для навигации, которое может переключаться между двумя представлениями, когда пользователь нажимает на ссылку.

Вы узнали, как сделать следующее:

  • Загрузить библиотеку маршрутизатора.
  • Добавить панель навигации в шаблон оболочки с тегами anchor, директивами routerLink и routerLinkActive.
  • Добавить router-outlet в шаблон оболочки, где будут отображаться представления.
  • Настроить модуль маршрутизатора с RouterModule.forRoot.
  • Установить маршрутизатор для создания HTML5 URL браузера.
  • Обрабатывать недопустимые маршруты с помощью маршрута wildcard.
  • Переходить на маршрут по умолчанию при запуске приложения с пустым путем.

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

Структура стартового приложения выглядит так:

router-sample
  src
    app
      app.component.ts
      app.module.ts
      crisis-list.component.ts
      hero-list.component.ts
      not-found.component.ts
    main.ts
    index.html
    styles.css
    tsconfig.json
  node_modules ...
  package.json

Вот файлы, обсуждавшиеся на этой стадии.

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent { }
app.module.ts
import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent }          from './app.component';
import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes', component: HeroListComponent },

  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(appRoutes)
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
hero-list.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <h2>HEROES</h2>
    <p>Get your heroes here</p>

    <button routerLink="/sidekicks">Go to sidekicks</button>
  `
})
export class HeroListComponent { }
crisis-list.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <h2>CRISIS CENTER</h2>
    <p>Get your crisis here</p>`
})
export class CrisisListComponent { }
not-found.component.ts
import { Component } from '@angular/core';

@Component({
  template: '<h2>Page not found</h2>'
})
export class PageNotFoundComponent {}
index.html
<html>
  <head>
    <!-- Set the base href -->
    <base href="/">
    <title>Angular Router</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <!-- Polyfills -->
    <script src="node_modules/core-js/client/shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js')
            .catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>loading...</my-app>
  </body>

</html>

Веха 2: Модуль маршрутизации

В начальной настройке маршрутов вы предоставили простую настройку с двумя маршрутами, используемыми для настройки приложения для маршрутизации. Это прекрасно подходит для простой маршрутизации. По мере роста приложения и использования большего числа Router функций, таких как гвардии, резольверы и маршрутизация дочерних элементов, вы естественным образом захотите переработать конфигурацию маршрутизации в свой собственный файл. Мы рекомендуем перенести информацию о маршрутизации в специальный модуль, называемый Модулем маршрутизации.

Модуль маршрутизации имеет несколько характеристик:

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

Переработать конфигурацию маршрутизации в модуль маршрутизации

Создайте файл с именем app-routing.module.ts в папке /app для хранения модуля маршрутизации.

Импортируйте компоненты CrisisListComponent и HeroListComponent так же, как вы делали в app.module.ts. Затем перенесите импорты Router и конфигурацию маршрутизации, включая RouterModule.forRoot, в этот модуль маршрутизации.

В соответствии с соглашениями, добавьте класс с именем AppRoutingModule и экспортируйте его, чтобы вы могли импортировать его позже в AppModule.

Наконец, повторно экспортируйте угловой модуль RouterModule, добавив его в массив модулей exports. Повторно экспортируя RouterModule здесь и импортируя AppRoutingModule в AppModule, компоненты, объявленные в AppModule, получат доступ к директивам маршрутизатора, таким как RouterLink и RouterOutlet.

После этих шагов файл должен выглядеть так.

src/app/app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes',        component: HeroListComponent },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

Далее, обновите файл app.module.ts, сначала импортировав только что созданный модуль AppRoutingModule из app-routing.module.ts, а затем заменив RouterModule.forRoot в массиве imports на AppRoutingModule.

src/app/app.module.ts

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

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

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

Нужен ли вам Модуль маршрутизации?

Модуль маршрутизации заменяет конфигурацию маршрутизации в корневом или функциональном модуле. Либо настраивайте маршруты в Модуле маршрутизации, либо внутри самого модуля, но не в обоих.

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

Некоторые разработчики пропускают Модуль маршрутизации (например, AppRoutingModule), когда конфигурация простая, и объединяют конфигурацию маршрутизации непосредственно в модуль-компаньон (например, AppModule).

Выберите один шаблон или другой и следуйте этому шаблону последовательно.

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

Веха 3: Функция «Герои»

Вы видели, как навигация осуществляется с помощью директивы RouterLink. Теперь вы узнаете следующее:

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

В этом примере функция «Герои» воспроизводится в эпизоде «Сервисы» в учебнике по героям, и вы будете копировать большую часть кода оттуда.

Вот как пользователь будет взаимодействовать с этой версией приложения:

App in action

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

Хотя вы можете продолжать добавлять файлы в папку src/app/, это нереально и в конечном итоге не поддерживаемо. Большинство разработчиков предпочитают размещать каждую функциональную область в своей собственной папке.

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

Добавить функциональность героев

Выполните следующие действия:

  • Создайте папку src/app/heroes; туда вы добавите файлы, реализующие управление героями.
  • Удалите заполнитель hero-list.component.ts, находящийся в папке app.
  • Создайте новый модуль hero-list.component.ts в папке src/app/heroes.
  • Скопируйте в него содержимое app.component.ts из учебника "Сервисы".
  • Внесите несколько незначительных, но необходимых изменений:
    • Удалите selector (маршрутизированным компонентам они не нужны).
    • Удалите <h1>.
    • Переименуйте <h2> в <h2>HEROES</h2>.
    • Удалите <hero-detail> в конце шаблона.
    • Переименуйте класс AppComponent в HeroListComponent.
  • Скопируйте файлы hero-detail.component.ts и hero.service.ts в подпапку heroes.
  • Создайте (до маршрутизации) heroes.module.ts в папке героев, который выглядит так:

src/app/heroes/heroes.module.ts (pre-routing)

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

После завершения у вас будут эти файлы управления героями:

src/app/heroes
  hero-detail.component.ts
  hero-list.component.ts
  hero.service.ts
  heroes.module.ts

Требования к маршрутизации функции «Герои»

Функция «Герои» имеет два взаимодействующих компонента — список героев и подробности о герое. Представление списка является автономным; вы переходите к нему, оно получает список героев и отображает их.

Представление деталей отличается. Оно отображает конкретного героя. Оно не может самостоятельно узнать, какого героя отображать. Эта информация должна поступать извне.

Когда пользователь выбирает героя из списка, приложение должно перейти на представление деталей и показать этого героя. Вы сообщаете представлению деталей, какого героя отображать, включив в URL маршрута идентификатор выбранного героя.

Конфигурация маршрута функции «Герои»

Создайте новый heroes-routing.module.ts в папке heroes с помощью тех же техник, что и при создании AppRoutingModule.

src/app/heroes/heroes-routing.module.ts

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

Поместите файл модуля маршрутизации в ту же папку, что и файл модуля-компаньона. Здесь и heroes-routing.module.ts, и heroes.module.ts находятся в одной и той же папке src/app/heroes.

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

Импортируйте компоненты героев из их новых мест в папке src/app/heroes/, определите два маршрута героев и экспортируйте класс HeroRoutingModule.

Теперь, когда у вас есть маршруты для Heroes модуля, зарегистрируйте их в Router через RouterModule почти так же, как вы делали в AppRoutingModule.

Существует небольшое, но важное различие. В AppRoutingModule вы использовали статический метод RouterModule.forRoot для регистрации маршрутов и поставщиков сервисов уровня приложения. В модуле функции вы используете статический метод forChild.

Вызывайте RouterModule.forRoot только в корневом модуле AppRoutingModule (или в модуле AppModule, если там вы регистрируете маршруты приложения верхнего уровня). В любом другом модуле для регистрации дополнительных маршрутов необходимо вызвать метод RouterModule.forChild.

Add the routing module to the HeroesModule

Добавьте HeroRoutingModule в HeroModule так же, как вы добавили AppRoutingModule в AppModule.

Откройте heroes.module.ts. Импортируйте токен HeroRoutingModule из heroes-routing.module.ts и добавьте его в массив imports модуля HeroesModule. Готовый HeroesModule будет выглядеть так:

src/app/heroes/heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

import { HeroRoutingModule } from './heroes-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

Remove duplicate hero routes

Маршруты героев в настоящее время определены в двух местах: в HeroesRoutingModule посредством HeroesModule и в AppRoutingModule.

Маршруты, предоставленные модулями функций, объединяются в маршруты импортированного модуля маршрутизатором. Это позволяет продолжать определять маршруты модуля функции без изменения основной конфигурации маршрутов.

Но вы не хотите определять одни и те же маршруты дважды. Удалите импорт HeroListComponent и маршрут /heroes из app-routing.module.ts.

Оставьте стандартные и универсальные маршруты! Это забота на самом верхнем уровне приложения.

src/app/app-routing.module.ts (v2)

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
// import { HeroListComponent }  from './hero-list.component';  // <-- delete this line
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  // { path: 'heroes',     component: HeroListComponent }, // <-- delete this line
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

Import hero module into AppModule

Модуль героев готов, но приложение ещё не знает о HeroesModule. Откройте app.module.ts и пересмотрите его следующим образом.

Импортируйте модуль HeroesModule и добавьте его в массив imports в метаданных @NgModule модуля AppModule.

Удалите HeroListComponent из AppModule, поскольку теперь его предоставляет HeroesModule. Это важно. Может быть только один владелец объявленного компонента. В этом случае модуль Heroes является владельцем компонентов Heroes и делает их доступными для компонентов в AppModule через HeroesModule.

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

После этих шагов модуль AppModule должен выглядеть следующим образом:

src/app/app.module.ts

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule }     from './heroes/heroes.module';

import { CrisisListComponent }   from './crisis-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Module import order matters

Посмотрите на массив imports модуля. Обратите внимание, что модуль AppRoutingModule стоит в самом конце. И что самое важное, он находится после HeroesModule.

src/app/app.module.ts (module-imports)

imports: [
  BrowserModule,
  FormsModule,
  HeroesModule,
  AppRoutingModule
],

Порядок конфигурации маршрутов имеет значение. Маршрутизатор принимает первый маршрут, который соответствует пути запроса навигации.

Когда все маршруты находились в одном AppRoutingModule, вы помещали стандартные и универсальные маршруты в конец, после маршрута /heroes, чтобы маршрутизатор имел возможность сопоставить URL с маршрутом /heroes до попадания на универсальный маршрут и перехода на страницу "Ошибка 404".

Теперь маршруты находятся не в одном файле. Они распределены по двум модулям: AppRoutingModule и HeroesRoutingModule.

Каждый модуль маршрутизации дополняет конфигурацию маршрутов в порядке импорта. Если вы перечислите AppRoutingModule вначале, универсальный маршрут будет зарегистрирован перед маршрутами героев. Универсальный маршрут — который соответствует любому URL — перехватит попытку перехода на маршрут героя.

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

Route definition with a parameter

Вернитесь к HeroesRoutingModule и посмотрите на определения маршрутов ещё раз. Маршрут к HeroDetailComponent имеет особенность.

src/app/heroes/heroes-routing.module.ts (excerpt)

{ path: 'hero/:id', component: HeroDetailComponent }

Обратите внимание на токен :id в пути. Это создаёт место в пути для Параметра маршрута. В этом случае маршрутизатор вставит идентификатор героя в это место.

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

localhost:3000/hero/15

Если пользователь введёт этот URL в адресную строку браузера, маршрутизатор должен распознать шаблон и перейти к тому же представлению деталей "Магнета".

Параметр маршрута: обязательный или необязательный?

Встраивание маркера параметра маршрута, :id, в путь определения маршрута — хороший выбор в этом сценарии, потому что идентификатор героя id является обязательным для компонента HeroDetailComponent, и значение 15 в пути чётко отличает маршрут к "Магнете" от маршрута к другому герою.

Navigate to hero detail imperatively

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

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

Начните с компонента HeroListComponent. Измените его конструктор, чтобы получить Router и HeroService через зависимость:

src/app/heroes/hero-list.component.ts (constructor)

constructor(
  private router: Router,
  private service: HeroService
) {}

Внесите следующие изменения в шаблон компонента:

template: `
  <h2>HEROES</h2>
  <ul class="items">
    <li *ngFor="let hero of heroes | async"
      (click)="onSelect(hero)">
      <span class="badge">{{ hero.id }}</span> {{ hero.name }}
    </li>
  </ul>

  <button routerLink="/sidekicks">Go to sidekicks</button>
`

Шаблон определяет цикл *ngFor, как вы видели ранее. Есть (click) привязка события к методу компонента onSelect, который вы реализуете следующим образом:

src/app/heroes/hero-list.component.ts (select)

  onSelect(hero: Hero) {
    this.router.navigate(['/hero', hero.id]);
  }

Метод onSelect компонента вызывает метод navigate маршрутизатора с массивом параметров ссылки. Вы можете использовать ту же синтаксическую конструкцию в RouterLink, если позже решите выполнить навигацию в HTML-шаблоне, а не в коде компонента.

Setting the route parameters in the list view

После перехода на HeroDetailComponent вы ожидаете увидеть детали выбранного героя. Вам нужны две части информации: маршрут к компоненту и идентификатор id героя.

Соответственно, массив параметров ссылки имеет два элемента: путь маршрута и параметр маршрута, который определяет id выбранного героя.

src/app/heroes/hero-list.component.ts (link-parameters-array)

['/hero', hero.id] // { 15 }

Маршрутизатор составляет URL назначения из массива следующим образом: localhost:3000/hero/15.

Как целевой HeroDetailComponent узнаёт об этом id? Не анализируйте URL. Позвольте это сделать маршрутизатору.

Маршрутизатор извлекает параметр маршрута (id:15) из URL и предоставляет его компоненту HeroDetailComponent через сервис ActivatedRoute.

ActivatedRoute: the one-stop-shop for route information

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

url: массив строк, представляющих каждый сегмент пути маршрута.

data: объект, содержащий данные, предоставленные для маршрута. Также содержит любые решённые значения из фильтра разрешения.

params: объект, содержащий обязательные и необязательные параметры, специфичные для маршрута.

queryParams: объект, содержащий параметры запроса, доступные для всех маршрутов.

fragment: строка, содержащая фрагмент URL, доступный для всех маршрутов.

outlet: имя RouterOutlet, используемого для отрисовки маршрута. Для неназванного выхода имя выхода — primary.

routeConfig: конфигурация маршрута, используемая для маршрута, который содержит исходный путь.

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

firstChild: содержит первый ActivatedRoute в списке дочерних маршрутов.

children: содержит все вложенные маршруты, активированные в рамках текущего маршрута.

Импортируйте токены Router, ActivatedRoute и Params из пакета маршрутизатора.

src/app/heroes/hero-detail.component.ts (activated route)

import { Router, ActivatedRoute, Params } from '@angular/router';

Импортируйте оператор switchMap, так как вам понадобится он позже для обработки параметров маршрута Observable.

src/app/heroes/hero-detail.component.ts (switchMap operator import)

import 'rxjs/add/operator/switchMap';

Как обычно, вы пишете конструктор, который запрашивает у Angular инъекцию необходимых компоненту сервисов и ссылается на них как на частные переменные.

src/app/heroes/hero-detail.component.ts (конструктор)

constructor(
  private route: ActivatedRoute,
  private router: Router,
  private service: HeroService
) {}

Позже, в методе ngOnInit, вы используете сервис ActivatedRoute для получения параметров маршрута, извлекаете героя id из параметров и получаете героя для отображения.

Поместите эту логику доступа к данным в метод ngOnInit, а не внутри конструктора, чтобы улучшить тестируемость компонента. Angular вызывает метод ngOnInit вскоре после создания экземпляра компонента HeroDetailComponent, поэтому герой будет получен вовремя, чтобы его можно было использовать.

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

src/app/heroes/hero-detail.component.ts (ngOnInit)

ngOnInit() {
  this.route.params
    // (+) converts string 'id' to a number
    .switchMap((params: Params) => this.service.getHero(+params['id']))
    .subscribe((hero: Hero) => this.hero = hero);
}

Поскольку параметры предоставляются как Observable, вы используете оператор switchMap для предоставления их параметру id по имени и говорите сервису HeroService получить героя с этим id.

Оператор switchMap позволяет выполнить действие с текущим значением Observable и отобразить его на новое значение Observable. Как и многие операторы rxjs, switchMap обрабатывает Observable, а также Promise для получения значения, которое они передают.

Оператор switchMap также отменит любые текущие запросы, если пользователь повторно перенаправится на маршрут во время получения героя.

Используйте метод subscribe для обнаружения изменений в id и для (пере)установки полученного Hero.

Наблюдаемый объект params и повторное использование компонента

В этом примере вы получаете параметры маршрута из Observable. Это подразумевает, что параметры маршрута могут измениться в течение срока жизни этого компонента.

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

Предположим, что в навигационной панели родительского компонента есть кнопки «вперед» и «назад», которые прокручивали список героев. Каждый щелчок принудительно перенаправлял на HeroDetailComponent со следующим или предыдущим id.

Вы не хотите, чтобы маршрутизатор удалял текущий HeroDetailComponent экземпляр из DOM только для его повторного создания для следующего id. Это может быть визуально резким. Лучше просто повторно использовать тот же экземпляр компонента и обновить параметр.

К сожалению, ngOnInit вызывается только один раз на экземпляр компонента. Вам нужен способ обнаружить, когда параметры маршрута меняются внутри того же экземпляра. Наблюдаемое свойство params прекрасно справляется с этим.

При подписке на наблюдаемый объект в компоненте вы почти всегда организуете отмену подписки при уничтожении компонента.

Есть несколько исключительных наблюдаемых объектов, где это не требуется. Наблюдаемые объекты ActivatedRoute относятся к исключениям.

ActivatedRoute и его наблюдаемые объекты изолированы от самого Router. Router уничтожает компонент маршрута, когда он больше не нужен, а инжектированный ActivatedRoute погибает вместе с ним.

Вы можете отписаться. Это безопасно и никогда не является плохой практикой.

Снимок: альтернатива без наблюдаемых объектов

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

Когда вы уверены, что экземпляр HeroDetailComponent никогда, никогда, никогда не будет повторно использован, вы можете упростить код с помощью снимка.

route.snapshot предоставляет начальное значение параметров маршрута. Вы можете напрямую получить доступ к параметрам без подписки или добавления операторов наблюдаемого объекта. Это гораздо проще писать и читать:

src/app/heroes/hero-detail.component.ts (ngOnInit снимок)

ngOnInit() {
  // (+) converts string 'id' to a number
  let id = +this.route.snapshot.params['id'];

  this.service.getHero(id)
    .then((hero: Hero) => this.hero = hero);
}

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

Возврат к компоненту списка

В HeroDetailComponent есть кнопка «Назад», привязанная к методу gotoHeroes, который принудительно перенаправляет на компонент HeroListComponent.

Метод маршрутизатора navigate принимает тот же массив параметров ссылок из одного элемента, который можно привязать к директиве [routerLink]. Он содержит путь к HeroListComponent:

src/app/heroes/hero-detail.component.ts (фрагмент)

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

Параметры маршрута: обязательные или необязательные?

Используйте параметры маршрута для указания обязательного значения параметра внутри URL-адреса маршрута, как вы делаете при переходе на HeroDetailComponent, чтобы просмотреть героя с id 15:

localhost:3000/hero/15

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

Selected hero

Вы реализуете эту функцию, включив id просматриваемого героя в URL в качестве необязательного параметра при возвращении из просмотра HeroDetailComponent.

Необязательная информация может принимать другие формы. Критерии поиска часто имеют свободный формат, например, name='wind*'. Часты множественные значения — after='12/31/2015' & before='1/1/2017' — в произвольном порядке — before='1/1/2017' & after='12/31/2015' — в различных форматах — during='currentYear'.

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

Необязательные параметры — идеальный инструмент для передачи произвольно сложной информации во время навигации. Необязательные параметры не участвуют в сопоставлении шаблонов и обеспечивают гибкость выражений.

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

В целом, отдавайте предпочтение обязательному параметру маршрута, когда значение обязательно (например, если необходимо различать один путь маршрута от другого); отдавайте предпочтение необязательному параметру, когда значение необязательно, сложно и/или многовариантно.

Список героев: необязательный выбор героя

При переходе на HeroDetailComponent вы указали обязательный id редактируемого героя в параметре маршрута и сделали его вторым элементом массива параметров ссылок.

src/app/heroes/hero-list.component.ts (link-parameters-array)

['/hero', hero.id] // { 15 }

Маршрутизатор встроил значение id в URL навигации, так как вы определили его как параметр маршрута с маркером :id в маршруте path:

src/app/heroes/heroes-routing.module.ts (hero-detail-route)

{ path: 'hero/:id', component: HeroDetailComponent }

Когда пользователь нажимает кнопку «назад», HeroDetailComponent строит другой массив параметров ссылок, который он использует для возврата к HeroListComponent.

src/app/heroes/hero-detail.component.ts (gotoHeroes)

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

В этом массиве отсутствует параметр маршрута, потому что у вас не было причины отправлять информацию в HeroListComponent.

Теперь у вас есть причина. Вы хотите отправить идентификатор текущего героя с запросом навигации, чтобы HeroListComponent мог выделить этого героя в своём списке. Это полезная функция; список отобразится отлично и без неё.

Отправьте id с объектом, содержащим необязательный параметр id. Для демонстрации в объекте есть дополнительный параметр «мусора» (foo), который HeroListComponent должен игнорировать. Вот переработанное утверждение навигации:

src/app/heroes/hero-detail.component.ts (go to heroes)

gotoHeroes() {
  let heroId = this.hero ? this.hero.id : null;
  // Pass along the hero id if available
  // so that the HeroList component can select that hero.
  // Include a junk 'foo' property for fun.
  this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
}

Приложение по-прежнему работает. Нажатие «назад» возвращает к представлению списка героев.

Посмотрите на адресную строку браузера.

Чтобы увидеть изменения URL в адресной строке браузера живого примера, откройте его снова в редакторе Plunker, нажав на значок в правом верхнем углу, затем разверните окно предварительного просмотра, нажав на синюю кнопку «X» в правом верхнем углу.

pop out the windowpop out the window

Он должен выглядеть примерно так, в зависимости от места, где вы его запустили:

localhost:3000/heroes;id=15;foo=foo

Значение id появляется в URL-адресе как (;id=15;foo=foo), а не в пути URL. Путь для маршрута «Герои» не имеет токена :id.

Необязательные параметры маршрута не разделяются символами «?» и «&», как в строке запроса URL. Они разделены точкой с запятой «;». Это матричная запись URL — возможно, что вы раньше не видели.

Матричная запись URL — идея, впервые предложенная в предложении 1996 года основателем веб-технологий, Тимом Бернерсом-Ли.

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

Синтаксис может показаться вам странным, но пользователи вряд ли заметят или будут обращать внимание, пока URL-адрес можно будет отправлять по электронной почте и вставлять в адресную строку браузера, как в этом случае.

Параметры маршрута в сервисе ActivatedRoute

Список героев не изменился. Ни одна строка героя не выделена.

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

HeroListComponent не ожидает никаких параметров и не знает, что с ними делать. Вы можете это изменить.

Ранее, при переходе от HeroListComponent к HeroDetailComponent, вы подписывались на параметры маршрута Observable и делали их доступными для HeroDetailComponent в сервисе ActivatedRoute. Вы инжектировали этот сервис в конструктор HeroDetailComponent.

На этот раз вы будете переходить в противоположном направлении, от HeroDetailComponent к HeroListComponent.

Сначала вы расширите инструкцию импорта маршрутизатора, чтобы включить символ сервиса ActivatedRoute:

src/app/heroes/hero-list.component.ts (import)

import { Router, ActivatedRoute, Params } from '@angular/router';

Импортируйте оператор switchMap для выполнения операции над Observable параметров маршрута.

src/app/heroes/hero-list.component.ts (rxjs imports)

import 'rxjs/add/operator/switchMap';
import { Observable } from 'rxjs/Observable';

Затем вы инжектируете ActivatedRoute в конструктор HeroListComponent.

src/app/heroes/hero-list.component.ts (constructor and ngOnInit)

export class HeroListComponent implements OnInit {
  heroes: Observable<Hero[]>;

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    this.heroes = this.route.params
      .switchMap((params: Params) => {
        this.selectedId = +params['id'];
        return this.service.getHeroes();
      });
  }
}

Свойство ActivatedRoute.params является Observable параметров маршрута. params испускает новые значения id, когда пользователь переходит к компоненту. В ngOnInit вы подписываетесь на эти значения, устанавливаете selectedId и получаете героев.

Все параметры маршрута/запроса являются строками. (+) перед выражением params['id'] — это трюк JavaScript для преобразования строки в целое число.

Добавьте метод isSelected, который возвращает true, когда id героя совпадает с выбранным id.

src/app/heroes/hero-list.component.ts (isSelected)

isSelected(hero: Hero) { return hero.id === this.selectedId; }

Наконец, обновите шаблон с помощью связывания класса к этому методу isSelected. Связывание добавляет класс CSS selected, когда метод возвращает true, и удаляет его, когда false. Найдите его внутри повторяющегося тега <li>, как показано здесь:

src/app/heroes/hero-list.component.ts (template)

template: `
  <h2>HEROES</h2>
  <ul class="items">
    <li *ngFor="let hero of heroes | async"
      [class.selected]="isSelected(hero)"
      (click)="onSelect(hero)">
      <span class="badge">{{ hero.id }}</span> {{ hero.name }}
    </li>
  </ul>

  <button routerLink="/sidekicks">Go to sidekicks</button>
`

Когда пользователь переходит от списка героев к герою "Magneta" и обратно, "Magneta" отображается как выбранный:

Selected List

Необязательный параметр маршрута foo не вредит и по-прежнему игнорируется.

Добавление анимаций к компоненту маршрута

Модуль функции героев почти завершён, но что такое функция без плавных переходов?

Этот раздел покажет вам, как добавить анимации к HeroDetailComponent.

Создайте файл animations.ts в корневой папке src/app/. Его содержимое выглядит следующим образом:

src/app/animations.ts (excerpt)

import { animate, AnimationEntryMetadata, state, style, transition, trigger } from '@angular/core';

// Component transition animations
export const slideInDownAnimation: AnimationEntryMetadata =
  trigger('routeAnimation', [
    state('*',
      style({
        opacity: 1,
        transform: 'translateX(0)'
      })
    ),
    transition(':enter', [
      style({
        opacity: 0,
        transform: 'translateX(-100%)'
      }),
      animate('0.2s ease-in')
    ]),
    transition(':leave', [
      animate('0.5s ease-out', style({
        opacity: 0,
        transform: 'translateY(100%)'
      }))
    ])
  ]);

Этот файл выполняет следующие действия:

  • Импортирует символы анимации, которые строят триггеры анимации, управляют состоянием и управляют переходами между состояниями.

  • Экспортирует константу с именем slideInDownAnimation, установленную на триггер анимации под названием routeAnimation; компоненты с анимацией будут ссылаться на это имя.

  • Определяет состояние подстановки, *, которое соответствует любому состоянию анимации, в котором находится компонент маршрута.

  • Определяет два перехода: один для плавного входа компонента слева на экран при входе в приложение (:enter), другой для анимации сдвига компонента вниз при выходе из приложения (:leave).

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

Вернувшись в HeroDetailComponent, импортируйте slideInDownAnimation из './animations.ts. Добавьте декоратор HostBinding к импортам из @angular/core; вам это понадобится через некоторое время.

Добавьте массив animations в метаданные @Component, содержащие slideInDownAnimation.

Затем добавьте три свойства @HostBinding в класс для установки анимации и стилей для элемента компонента маршрута.

src/app/heroes/hero-detail.component.ts (host bindings)

@HostBinding('@routeAnimation') routeAnimation = true;
@HostBinding('style.display')   display = 'block';
@HostBinding('style.position')  position = 'absolute';

'@routeAnimation', переданное первому @HostBinding, соответствует имени триггера slideInDownAnimation, routeAnimation. Установите свойство routeAnimation в true, потому что вас интересуют только состояния :enter и :leave.

Другие два свойства @HostBinding задают отображение и позицию компонента.

HeroDetailComponent будет плавно входить слева при переходе и будет скользить вниз при переходе.

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

Заключение этапа 3

Вы узнали, как:

  • Организовать приложение в функциональные области.
  • Перемещаться императивно из одного компонента в другой.
  • Передавать информацию через параметры маршрута и подписываться на них в компоненте.
  • Импортировать модуль функциональной области в AppModule.
  • Применять анимации к компоненту маршрута.

После этих изменений структура папок выглядит так:

router-sample
  src
    app
      heroes
        hero-detail.component.ts
        hero-list.component.ts
        hero.service.ts
        heroes.module.ts
        heroes-routing.module.ts
      app.component.ts
      app.module.ts
      app-routing.module.ts
      crisis-list.component.ts
    main.ts
    index.html
    styles.css
    tsconfig.json
  node_modules ...
  package.json
app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent { }
app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule }     from './heroes/heroes.module';

import { CrisisListComponent }   from './crisis-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
app-routing.module.ts
import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
// import { HeroListComponent }  from './hero-list.component';  // <-- delete this line
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  // { path: 'heroes',     component: HeroListComponent }, // <-- delete this line
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}
hero-list.component.ts
// TODO SOMEDAY: Feature Componetized like CrisisCenter
import 'rxjs/add/operator/switchMap';
import { Observable } from 'rxjs/Observable';
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';

import { Hero, HeroService }  from './hero.service';

@Component({
  template: `
    <h2>HEROES</h2>
    <ul class="items">
      <li *ngFor="let hero of heroes | async"
        [class.selected]="isSelected(hero)"
        (click)="onSelect(hero)">
        <span class="badge">{{ hero.id }}</span> {{ hero.name }}
      </li>
    </ul>

    <button routerLink="/sidekicks">Go to sidekicks</button>
  `
})
export class HeroListComponent implements OnInit {
  heroes: Observable<Hero[]>;

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    this.heroes = this.route.params
      .switchMap((params: Params) => {
        this.selectedId = +params['id'];
        return this.service.getHeroes();
      });
  }

  isSelected(hero: Hero) { return hero.id === this.selectedId; }

  onSelect(hero: Hero) {
    this.router.navigate(['/hero', hero.id]);
  }
}
hero-detail.component.ts
import 'rxjs/add/operator/switchMap';
import { Component, OnInit, HostBinding } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';

import { slideInDownAnimation } from '../animations';

import { Hero, HeroService }  from './hero.service';

@Component({
  template: `
  <h2>HEROES</h2>
  <div *ngIf="hero">
    <h3>"{{ hero.name }}"</h3>
    <div>
      <label>Id: </label>{{ hero.id }}</div>
    <div>
      <label>Name: </label>
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </div>
    <p>
      <button (click)="gotoHeroes()">Back</button>
    </p>
  </div>
  `,
  animations: [ slideInDownAnimation ]
})
export class HeroDetailComponent implements OnInit {
  @HostBinding('@routeAnimation') routeAnimation = true;
  @HostBinding('style.display')   display = 'block';
  @HostBinding('style.position')  position = 'absolute';

  hero: Hero;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: HeroService
  ) {}

  ngOnInit() {
    this.route.params
      // (+) converts string 'id' to a number
      .switchMap((params: Params) => this.service.getHero(+params['id']))
      .subscribe((hero: Hero) => this.hero = hero);
  }

  gotoHeroes() {
    let heroId = this.hero ? this.hero.id : null;
    // Pass along the hero id if available
    // so that the HeroList component can select that hero.
    // Include a junk 'foo' property for fun.
    this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
  }
}
hero.service.ts
import { Injectable } from '@angular/core';

export class Hero {
  constructor(public id: number, public name: string) { }
}

let HEROES = [
  new Hero(11, 'Mr. Nice'),
  new Hero(12, 'Narco'),
  new Hero(13, 'Bombasto'),
  new Hero(14, 'Celeritas'),
  new Hero(15, 'Magneta'),
  new Hero(16, 'RubberMan')
];

let heroesPromise = Promise.resolve(HEROES);

@Injectable()
export class HeroService {
  getHeroes() { return heroesPromise; }

  getHero(id: number | string) {
    return heroesPromise
      .then(heroes => heroes.find(hero => hero.id === +id));
  }
}
heroes.module.ts
import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

import { HeroRoutingModule } from './heroes-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}
heroes-routing.module.ts
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

Этап 4: Функциональная область центра кризисных ситуаций

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

Начните с имитации функциональной области героев:

  • Удалите файл заглушки центра кризисных ситуаций.
  • Создайте папку app/crisis-center.
  • Скопируйте файлы из app/heroes в новую папку центра кризисных ситуаций.
  • В новых файлах замените каждое упоминание "hero" на "crisis", а "heroes" на "crises".

Вы превратите CrisisService в поставщика имитационных кризисных ситуаций вместо имитационных героев:

src/app/crisis-center/crisis.service.ts (mock-crises)

export class Crisis {
  constructor(public id: number, public name: string) { }
}

const CRISES = [
  new Crisis(1, 'Dragon Burning Cities'),
  new Crisis(2, 'Sky Rains Great White Sharks'),
  new Crisis(3, 'Giant Asteroid Heading For Earth'),
  new Crisis(4, 'Procrastinators Meeting Delayed Again'),
];

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

В соответствии с принципом разделения обязанностей, изменения в Центре кризисных ситуаций не повлияют на AppModule или на компонент какой-либо другой функциональной области.

Центр кризисных ситуаций с вложенными маршрутами

Этот раздел покажет вам, как организовать центр кризисных ситуаций в соответствии с рекомендуемой схемой для приложений Angular:

  • Каждая функциональная область находится в собственной папке.
  • Каждая функциональная область имеет свой собственный модуль Angular.
  • Каждая область имеет свой собственный корневой компонент области.
  • Корневой компонент каждой области имеет собственный выход маршрутизатора и вложенные маршруты.
  • Маршруты функциональных областей редко (если вообще) пересекаются с маршрутами других функциональных областей.

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

Component Tree

Компонент вложенного маршрутирования

Добавьте следующий crisis-center.component.ts в папку crisis-center:

src/app/crisis-center/crisis-center.component.ts (minus imports)

@Component({
  template:  `
    <h2>CRISIS CENTER</h2>
    <router-outlet></router-outlet>
  `
})
export class CrisisCenterComponent { }

У CrisisCenterComponent есть следующие общие черты с AppComponent:

  • Он является корневым компонентом области центра кризисных ситуаций, так же как AppComponent является корневым компонентом всего приложения.
  • Он является оболочкой для функциональной области управления кризисами, так же как AppComponent — оболочкой для управления высокоуровневым рабочим процессом.

Как и большинство оболочек, класс CrisisCenterComponent очень прост, даже проще, чем AppComponent: он не содержит логики приложения, а его шаблон не имеет ссылок, только заголовок и <router-outlet> для вложенных представлений центра кризисных ситуаций.

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

Конфигурация вложенного маршрута

CrisisCenterComponent — это компонент маршрутизации, как и AppComponent. Он имеет свои собственные RouterOutlet и свои собственные вложенные маршруты.

Добавьте следующий crisis-center-home.component.ts в папку crisis-center.

src/app/crisis-center/crisis-center-home.component.ts (minus imports)

@Component({
  template: `
    <p>Welcome to the Crisis Center</p>
  `
})
export class CrisisCenterHomeComponent { }

Создайте файл crisis-center-routing.module.ts, как вы сделали файл heroes-routing.module.ts. На этот раз вы определите вложенные маршруты внутри родительского маршрута crisis-center.

src/app/crisis-center/crisis-center-routing.module.ts (Routes)

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class CrisisCenterRoutingModule { }

Обратите внимание, что родительский маршрут crisis-center имеет свойство children с единственным маршрутом, содержащим CrisisListComponent. Маршрут CrisisListComponent также имеет массив children с двумя маршрутами.

Эти два маршрута переходят к вложенным компонентам центра кризисных ситуаций, CrisisCenterHomeComponent и CrisisDetailComponent, соответственно.

Существуют важные различия в том, как маршрутизатор обрабатывает эти дочерние маршруты.

Маршрутизатор отображает компоненты этих маршрутов в RouterOutlet CrisisCenterComponent, а не в RouterOutlet AppComponent оболочки.

CrisisListComponent содержит список кризисов и RouterOutlet для отображения компонентов маршрутов Crisis Center Home и Crisis Detail.

Маршрут Crisis Detail является дочерним по отношению к маршруту Crisis List. Поскольку маршрутизатор по умолчанию переиспользует компоненты, компонент Crisis Detail будет повторно использован при выборе разных кризисов. В отличие от этого, в маршруте Hero Detail компонент каждый раз создавался заново при выборе другого героя.

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

Примените эту логику к навигации внутри центра кризисных ситуаций, родительский путь которого — /crisis-center.

  • Чтобы перейти к CrisisCenterHomeComponent, полный URL — /crisis-center (/crisis-center + '' + '').

  • Чтобы перейти к CrisisDetailComponent для кризиса с id=2, полный URL — /crisis-center/2 (/crisis-center + '' + '/2').

Абсолютный URL для последнего примера, включая localhost origin, равен

localhost:3000/crisis-center/2

Вот полный файл crisis-center-routing.module.ts с его импортами.

src/app/crisis-center/crisis-center-routing.module.ts (excerpt)

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class CrisisCenterRoutingModule { }

Импорт модуля CrisisCenter в маршруты модуля AppModule

Как и с HeroesModule, вы должны добавить CrisisCenterModule в массив imports модуля AppModule перед AppRoutingModule:

src/app/app.module.ts (import CrisisCenterModule)

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { AppComponent }            from './app.component';
import { PageNotFoundComponent }   from './not-found.component';

import { AppRoutingModule }        from './app-routing.module';
import { HeroesModule }            from './heroes/heroes.module';
import { CrisisCenterModule }      from './crisis-center/crisis-center.module';

import { DialogService }           from './dialog.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroesModule,
    CrisisCenterModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Удалите начальный маршрут центра кризисных ситуаций из app-routing.module.ts. Функциональные маршруты теперь предоставляются модулями HeroesModule и CrisisCenter.

Файл app-routing.module.ts сохраняет маршруты приложения верхнего уровня, такие как маршрут по умолчанию и маршрут с подстановочным знаком.

src/app/app-routing.module.ts (v3)

import { NgModule }                from '@angular/core';
import { RouterModule, Routes }    from '@angular/router';

import { ComposeMessageComponent } from './compose-message.component';
import { PageNotFoundComponent }   from './not-found.component';

const appRoutes: Routes = [
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

Относительная навигация

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

Маршрутизатор сопоставляет такие абсолютные пути с маршрутами, начиная с верха конфигурации маршрутизации.

Вы можете продолжить использование абсолютных путей для навигации внутри функциональности Центр кризисных ситуаций, но это привязывает ссылки к родительской структуре маршрутизации. Если вы измените родительский /crisis-center путь, вам придется изменить массив параметров ссылки.

Вы можете освободить ссылки от этой зависимости, определив пути, которые являются относительными по отношению к текущему сегменту URL. Навигация внутри области функциональности сохранится, даже если вы измените родительский маршрут функциональности.

Вот пример:

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

./ или no leading slash относительны к текущему уровню.

../ для перехода на один уровень выше в пути маршрута.

Вы можете объединять синтаксис относительной навигации с предковым путем. Если вам нужно перейти на братский маршрут, вы можете использовать соглашение ../<sibling>, чтобы подняться на один уровень, а затем перейти к брату и вниз по пути брата.

Для навигации по относительному пути с помощью метода Router.navigate вы должны предоставить ActivatedRoute, чтобы маршрутизатор знал, где вы находитесь в текущем дереве маршрутов.

После списка параметров ссылки добавьте объект с свойством relativeTo, установленным в ActivatedRoute. Затем маршрутизатор вычисляет целевой URL на основе местоположения активного маршрута.

Всегда указывайте полный абсолютный путь при вызове метода navigateByUrl маршрутизатора.

Переход к детали кризиса с относительным URL

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

Вы уже внедрили ActivatedRoute, который необходим для составления пути относительной навигации.

src/app/crisis-center/crisis-list.component.ts (constructor)

constructor(
  private service: CrisisService,
  private route: ActivatedRoute,
  private router: Router
) {}

При посещении Центра кризисных ситуаций предковый путь равен /crisis-center, поэтому вам нужно только добавить id Центра кризисных ситуаций к существующему пути.

src/app/crisis-center/crisis-list.component.ts (relative navigation)

onSelect(crisis: Crisis) {
  this.selectedId = crisis.id;

  // Navigate with relative link
  this.router.navigate([crisis.id], { relativeTo: this.route });
}

Если вы использовали RouterLink для навигации вместо сервиса Router, вы бы использовали тот же массив параметров ссылки, но не предоставили бы объект со свойством relativeTo. ActivatedRoute подразумевается в директиве RouterLink.

src/app/crisis-center/crisis-list.component.ts (relative routerLink)

template: `
  <ul class="items">
    <li *ngFor="let crisis of crises | async">
      <a [routerLink]="[crisis.id]"
         [class.selected]="isSelected(crisis)">
        <span class="badge">{{ crisis.id }}</span>
        {{ crisis.name }}
      </a>
    </li>
  </ul>`

Обновите метод gotoCrises класса CrisisDetailComponent для перехода обратно в список Центра кризисных ситуаций с помощью относительной навигации по пути.

src/app/crisis-center/crisis-detail.component.ts (relative navigation)

// Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

Обратите внимание, что путь поднимается на один уровень с использованием синтаксиса ../. Если текущий кризис id равен 3, полученный путь обратно в список кризисов равен /crisis-center/;id=3;foo=foo.

Отображение нескольких маршрутов в именованных выводах

Вы решаете предоставить пользователям возможность связаться с центром кризисных ситуаций. Когда пользователь нажимает кнопку «Связаться», вы хотите отобразить сообщение в всплывающем окне.

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

До сих пор вы определяли единственный вывод и вкладывали дочерние маршруты под этот вывод для группирования маршрутов. Маршрутизатор поддерживает только один основной неименованный вывод на шаблон.

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

Добавьте вывод с именем «popup» в AppComponent, непосредственно под неименованным выводом.

src/app/app.component.ts (outlets)

<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>

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

Вторичные маршруты

Именованные выводы являются целевыми для вторичных маршрутов.

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

  • Они независимы друг от друга.
  • Они работают в сочетании с другими маршрутами.
  • Они отображаются в именованных выводах.

Создайте новый компонент с именем ComposeMessageComponent в src/app/compose-message.component.ts. Он отображает простую форму с заголовком, текстовым полем для сообщения и двумя кнопками «Отправить» и «Отмена».

Contact popup

Вот компонент и его шаблон:

src/app/compose-message.component.ts
import { Component, HostBinding } from '@angular/core';
import { Router }                 from '@angular/router';

import { slideInDownAnimation }   from './animations';

@Component({
  templateUrl: './compose-message.component.html',
  styles: [ ':host { position: relative; bottom: 10%; }' ],
  animations: [ slideInDownAnimation ]
})
export class ComposeMessageComponent {
  @HostBinding('@routeAnimation') routeAnimation = true;
  @HostBinding('style.display')   display = 'block';
  @HostBinding('style.position')  position = 'absolute';

  details: string;
  sending: boolean = false;

  constructor(private router: Router) {}

  send() {
    this.sending = true;
    this.details = 'Sending Message...';

    setTimeout(() => {
      this.sending = false;
      this.closePopup();
    }, 1000);
  }

  cancel() {
    this.closePopup();
  }

  closePopup() {
    // Providing a `null` value to the named outlet
    // clears the contents of the named outlet
    this.router.navigate([{ outlets: { popup: null }}]);
  }
}
src/app/compose-message.component.html
<h3>Contact Crisis Center</h3>
<div *ngIf="details">
  {{ details }}
</div>
<div>
  <div>
    <label>Message: </label>
  </div>
  <div>
    <textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea>
  </div>
</div>
<p *ngIf="!sending">
  <button (click)="send()">Send</button>
  <button (click)="cancel()">Cancel</button>
</p>

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

Обратите внимание, что метод send() имитирует задержку, ожидая секунду перед «отправкой» сообщения и закрытием всплывающего окна.

Метод closePopup() закрывает представление всплывающего окна, перенаправляя на вывод всплывающего окна с null. Это особенность, о которой говорится ниже.

Как и другие компоненты приложения, вы добавляете ComposeMessageComponent в declarations NgModule. Сделайте это в AppModule.

Добавление вторичного маршрута

Откройте AppRoutingModule и добавьте новый маршрут compose в массив appRoutes.

src/app/app-routing.module.ts (compose route)

{
  path: 'compose',
  component: ComposeMessageComponent,
  outlet: 'popup'
},

Свойства path и component вам должны быть знакомы. Есть новое свойство, outlet, установленное в 'popup'. Этот маршрут теперь нацелен на вывод «popup», и компонент ComposeMessageComponent будет отображаться там.

Пользователю нужен способ открыть всплывающее окно. Откройте AppComponent и добавьте ссылку «Связаться».

src/app/app.component.ts (contact-link)

<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>

Хотя маршрут compose привязан к выводу «popup», этого недостаточно для подключения маршрута к директиве RouterLink. Вы должны указать именованный вывод в списке параметров ссылки и привязать его к RouterLink с помощью связывания свойства.

Список параметров ссылки содержит объект с единственным свойством outlets, значение которого является другим объектом, имеющим ключи одного (или нескольких) имен вывода. В данном случае существует только свойство вывода «popup» со значением другого списка параметров ссылки, который указывает маршрут compose.

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

Этот outlets объект внутри внешнего объекта был совершенно излишним, когда нужно было учитывать только один маршрут и один неименованный разъем.

Маршрутизатор предполагал, что ваша спецификация маршрута направлена на неименованный первичный разъем и создал эти объекты для вас.

Маршрутизация на именованный разъем раскрыла ранее скрытую истину маршрутизатора: вы можете нацеливаться на несколько разъемов с помощью нескольких маршрутов в одном и том же RouterLink директиве.

Вы на самом деле этого не делаете здесь. Но чтобы нацелиться на именованный разъем, вы должны использовать более богатый и подробный синтаксис.

Навигация по второстепенному маршруту: слияние маршрутов во время навигации

Перейдите к Центру кризисных ситуаций и нажмите «Связаться». В адресной строке браузера вы должны увидеть что-то вроде следующего URL.

http://.../crisis-center(popup:compose)

Интересная часть URL следует за ...:

  • crisis-center — это первичная навигация.
  • Скобки окружают второстепенный маршрут.
  • Второстепенный маршрут состоит из имени разъема (popup), разделителя (colon) и пути второстепенного маршрута (compose).

Нажмите ссылку Герои и посмотрите на URL еще раз.

http://.../heroes(popup:compose)

Часть первичной навигации изменилась; второстепенный маршрут остался прежним.

Маршрутизатор отслеживает две отдельные ветви в дереве навигации и генерирует представление этого дерева в URL.

Вы можете добавить еще больше разъемов и маршрутов, на верхнем уровне и на вложенных уровнях, создав дерево навигации со многими ветвями. Маршрутизатор сгенерирует соответствующий URL.

Вы можете сообщить маршрутизатору о навигации по всему дереву сразу, заполнив объект outlets, упомянутый выше. Затем передайте этот объект в массив параметров ссылки методу router.navigate.

Экспериментируйте с этими возможностями по своему усмотрению.

Очистка второстепенных маршрутов

Как вы узнали, компонент в разъеме сохраняется до тех пор, пока вы не перейдете к новому компоненту. Второстепенные разъемы в этом отношении ничем не отличаются.

Каждый второстепенный разъем имеет свою навигацию, независимую от навигации, управляющей первичным разъемом. Изменение текущего маршрута, отображаемого в первичном разъеме, не влияет на всплывающий разъем. Вот почему всплывающее окно остается видимым при навигации между кризисами и героями.

Нажатие кнопок «отправить» или «отмена» очищает представление всплывающего окна. Чтобы увидеть, как это происходит, снова посмотрите на метод closePopup():

src/app/compose-message.component.ts (closePopup)

closePopup() {
  // Providing a `null` value to the named outlet
  // clears the contents of the named outlet
  this.router.navigate([{ outlets: { popup: null }}]);
}

Он выполняет навигацию императивно с помощью метода Router.navigate(), передавая массив параметров ссылки.

Как и массив, привязанный к Связаться RouterLink в AppComponent, этот включает в себя объект со свойством outlets. Значение свойства outlets — это другой объект с именами разъемов в качестве ключей. Единственный именованный разъем — 'popup'.

На этот раз значение 'popup' равно null. Это не маршрут, но это допустимое значение. Установка свойства всплывающего окна RouterOutlet в null очищает разъем и удаляет второстепенный маршрут всплывающего окна из текущего URL.

Этап 5: Стражи маршрутов

В настоящее время любой пользователь может переходить в любое место приложения в любое время. Это не всегда правильно.

  • Возможно, пользователь не имеет права перехода к целевому компоненту.
  • Возможно, пользователю необходимо сначала выполнить вход (аутентифицироваться).
  • Возможно, вам необходимо извлечь данные перед отображением целевого компонента.
  • Вы можете сохранить незавершенные изменения перед выходом из компонента.
  • Вы можете спросить пользователя, разрешено ли отбрасывать незавершенные изменения, а не сохранять их.

Вы можете добавить стражей к конфигурации маршрута для обработки этих сценариев.

Возвращаемое значение стража управляет поведением маршрутизатора:

  • Если оно возвращает true, процесс навигации продолжается.
  • Если оно возвращает false, процесс навигации останавливается, и пользователь остается на месте.

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

Страж может вернуть свой булево ответ синхронно. Но во многих случаях страж не может дать ответ синхронно. Страж может задать пользователю вопрос, сохранить изменения на сервере или получить свежие данные. Все это асинхронные операции.

Соответственно, страж маршрутизации может вернуть Observable<boolean> или Promise<boolean>, и маршрутизатор будет ждать, пока наблюдаемое значение не разрешится до true или false.

Маршрутизатор поддерживает несколько типов стражей:

  1. CanActivate для управления навигацией к маршруту.

  2. CanActivateChild() для управления навигацией к дочернему маршруту.

  3. CanDeactivate для управления навигацией от текущего маршрута.

  4. Resolve для выполнения извлечения данных маршрута перед активацией маршрута.

  5. CanLoad для управления навигацией к модулю функциональности, загружаемому асинхронно.

Вы можете иметь несколько стражей на каждом уровне иерархии маршрутизации. Маршрутизатор сначала проверяет стражей CanDeactivate() и CanActivateChild(), от самого глубокого дочернего маршрута до верхнего. Затем он проверяет стражей CanActivate() сверху вниз до самого глубокого дочернего маршрута. Если любой страж возвращает false, ожидающие стражи, которые не завершили свою работу, будут отменены, и вся навигация будет отменена.

В следующих разделах представлены несколько примеров.

CanActivate: требование аутентификации

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

Страж CanActivate — это инструмент для управления этими правилами навигации.

Добавление модуля функциональности для администраторов

В этом следующем разделе вы расширите центр кризисных ситуаций некоторыми новыми административными функциями. Эти функции еще не определены. Но вы можете начать с добавления нового модуля функциональности под названием AdminModule.

Создайте папку admin с файлом модуля функциональности, файлом конфигурации маршрутизации и поддерживаемыми компонентами.

Структура файла модуля для администраторов выглядит так:

src/app/admin
  admin-dashboard.component.ts
  admin.component.ts
  admin.module.ts
  admin-routing.module.ts
  manage-crises.component.ts
  manage-heroes.component.ts

Модуль функциональности администраторов содержит AdminComponent, используемые для маршрутизации внутри модуля функциональности, маршрут панели мониторинга и два незавершенных компонента для управления кризисами и героями.

src/app/admin/admin-dashboard.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <p>Dashboard</p>
  `
})
export class AdminDashboardComponent { }
src/app/admin/admin.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <h3>ADMIN</h3>
    <nav>
      <a routerLink="./" routerLinkActive="active"
        [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
      <a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
      <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AdminComponent {
}
src/app/admin/admin.module.ts
import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';

import { AdminComponent }           from './admin.component';
import { AdminDashboardComponent }  from './admin-dashboard.component';
import { ManageCrisesComponent }    from './manage-crises.component';
import { ManageHeroesComponent }    from './manage-heroes.component';

import { AdminRoutingModule }       from './admin-routing.module';

@NgModule({
  imports: [
    CommonModule,
    AdminRoutingModule
  ],
  declarations: [
    AdminComponent,
    AdminDashboardComponent,
    ManageCrisesComponent,
    ManageHeroesComponent
  ]
})
export class AdminModule {}
src/app/admin/manage-crises.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <p>Manage your crises here</p>
  `
})
export class ManageCrisesComponent { }
src/app/admin/manage-heroes.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <p>Manage your heroes here</p>
  `
})
export class ManageHeroesComponent { }

Поскольку панель мониторинга администраторов RouterLink — это пустой маршрут в AdminComponent, она считается соответствием любому маршруту в области функциональности администраторов. Вы хотите, чтобы ссылка Dashboard была активной только при посещении пользователем этого маршрута. Добавление дополнительной привязки к Dashboard routerLink, [routerLinkActiveOptions]="{ exact: true }", отмечает ссылку ./ как активную при навигации пользователя к URL /admin, а не при навигации по дочерним маршрутам.

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

src/app/admin/admin-routing.module.ts (admin routing)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

Компонентный маршрут без компонента: группирование маршрутов без компонента

Смотря на дочерний маршрут под AdminComponent, есть свойства path и children, но он не использует component. Вы не ошиблись в конфигурации. Вы определили маршрут без компонента.

Цель заключается в том, чтобы сгруппировать маршруты управления Crisis Center под путём admin. Вам не нужен компонент для этого. Маршрут без компонента упрощает контроль за дочерними маршрутами.

Далее, импортируйте AdminModule в app.module.ts и добавьте его в массив imports для регистрации маршрутов администраторов.

src/app/app.module.ts (admin module)

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { AppComponent }            from './app.component';
import { PageNotFoundComponent }   from './not-found.component';

import { AppRoutingModule }        from './app-routing.module';
import { HeroesModule }            from './heroes/heroes.module';
import { CrisisCenterModule }      from './crisis-center/crisis-center.module';
import { AdminModule }             from './admin/admin.module';

import { DialogService }           from './dialog.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroesModule,
    CrisisCenterModule,
    AdminModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Добавьте ссылку «Админ» в оболочку AppComponent, чтобы пользователи могли перейти к этой функциональности.

src/app/app.component.ts (template)

template: `
  <h1 class="title">Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    <a routerLink="/admin" routerLinkActive="active">Admin</a>
    <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
  </nav>
  <router-outlet></router-outlet>
  <router-outlet name="popup"></router-outlet>
  `

Защита области функциональности администратора

В настоящее время каждый маршрут в Центре кризисных ситуаций открыт для всех. Новая функциональность администратора должна быть доступна только аутентифицированным пользователям.

Можно скрыть ссылку до входа пользователя. Но это сложно и трудно поддерживать.

Вместо этого вы напишете стража CanActivate() для перенаправления анонимных пользователей на страницу входа при попытке входа в область администратора.

Это страж общего назначения — вы можете представить себе другие функциональности, требующие аутентифицированных пользователей, поэтому вы создаете auth-guard.service.ts в корневой папке приложения.

Пока вы заинтересованы в том, как работают стражи, первая версия ничего полезного не делает. Она просто регистрирует в консоли и сразу возвращает returns true, позволяя навигации продолжить:

src/app/auth-guard.service.ts (excerpt)

import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

Далее, откройте admin-routing.module.ts, импортируйте класс AuthGuard и обновите маршрут администратора со свойством стража CanActivate(), которое ссылается на него:

src/app/admin/admin-routing.module.ts (guarded admin route)

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

Функциональность администратора теперь защищена стражем, хотя и защищена не очень хорошо.

Обучение AuthGuard аутентификации

Сделайте AuthGuard, по крайней мере, притворитесь, что выполняет аутентификацию.

Компонент AuthGuard должен вызывать сервис приложения, который может выполнить вход пользователя и сохранить информацию о текущем пользователе. Вот демонстрационный пример AuthService:

src/app/auth.service.ts (фрагмент)

import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/delay';

@Injectable()
export class AuthService {
  isLoggedIn: boolean = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string;

  login(): Observable<boolean> {
    return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true);
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}

Хотя он фактически не выполняет вход, он содержит необходимые для этого обсуждения данные. У него есть флаг isLoggedIn, указывающий, аутентифицирован ли пользователь. Его метод login имитирует вызов API к внешнему сервису, возвращая Observable, который успешно разрешается после короткой паузы. Свойство redirectUrl будет хранить запрошенный URL, чтобы вы могли перейти по нему после аутентификации.

Пересмотрите компонент AuthGuard, чтобы вызвать его.

src/app/auth-guard.service.ts (v2)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Navigate to the login page with extras
    this.router.navigate(['/login']);
    return false;
  }
}

Обратите внимание, что вы вводите AuthService и Router в конструкторе. Вы ещё не предоставили AuthService, но полезно знать, что вы можете вводить полезные сервисы в страже маршрутизации.

Эта стража возвращает синхронное булево значение. Если пользователь авторизован, она возвращает true, и навигация продолжается.

ActivatedRouteSnapshot содержит будущий маршрут, который будет активирован, а RouterStateSnapshot содержит будущий RouterState приложения, если вы пройдёте проверку стражи.

Если пользователь не авторизован, вы сохраняете запрошенный URL, откуда пришёл пользователь, с помощью RouterStateSnapshot.url и указываете маршрутизатору перейти на страницу входа — страницу, которую вы ещё не создали. Эта вторичная навигация автоматически отменяет текущую навигацию; checkLogin() возвращает false, чтобы прояснить это.

Добавление компонента LoginComponent

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

Зарегистрируйте маршрут /login в login-routing.module.ts и добавьте необходимые провайдеры в массив providers. В app.module.ts импортируйте LoginComponent и добавьте его в массив AppModule declarations. Импортируйте и добавьте LoginRoutingModule в импорты AppModule.

src/app/app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';
import { Router } from '@angular/router';

import { AppComponent }            from './app.component';
import { AppRoutingModule }        from './app-routing.module';

import { HeroesModule }            from './heroes/heroes.module';
import { ComposeMessageComponent } from './compose-message.component';
import { LoginRoutingModule }      from './login-routing.module';
import { LoginComponent }          from './login.component';
import { PageNotFoundComponent }   from './not-found.component';

import { DialogService }           from './dialog.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    LoginRoutingModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    ComposeMessageComponent,
    LoginComponent,
    PageNotFoundComponent
  ],
  providers: [
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  }
}
src/app/login.component.ts
import { Component }   from '@angular/core';
import { Router }      from '@angular/router';
import { AuthService } from './auth.service';

@Component({
  template: `
    <h2>LOGIN</h2>
    <p>{{message}}</p>
    <p>
      <button (click)="login()"  *ngIf="!authService.isLoggedIn">Login</button>
      <button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button>
    </p>`
})
export class LoginComponent {
  message: string;

  constructor(public authService: AuthService, public router: Router) {
    this.setMessage();
  }

  setMessage() {
    this.message = 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out');
  }

  login() {
    this.message = 'Trying to log in ...';

    this.authService.login().subscribe(() => {
      this.setMessage();
      if (this.authService.isLoggedIn) {
        // Get the redirect URL from our auth service
        // If no redirect has been set, use the default
        let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/crisis-center/admin';

        // Redirect the user
        this.router.navigate([redirect]);
      }
    });
  }

  logout() {
    this.authService.logout();
    this.setMessage();
  }
}
src/app/login-routing.module.ts
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard }            from './auth-guard.service';
import { AuthService }          from './auth.service';
import { LoginComponent }       from './login.component';

const loginRoutes: Routes = [
  { path: 'login', component: LoginComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(loginRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    AuthGuard,
    AuthService
  ]
})
export class LoginRoutingModule {}

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

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

Вы также можете защитить дочерние маршруты с помощью стражи CanActivateChild. Стража CanActivateChild похожа на стражу CanActivate. Ключевое различие заключается в том, что она выполняется до активации любого дочернего маршрута.

Вы защитили модуль функциональности администратора от несанкционированного доступа. Вам также следует защитить дочерние маршруты внутри модуля функциональности.

Расширьте AuthGuard, чтобы защитить навигацию между маршрутами admin. Откройте auth-guard.service.ts и добавьте интерфейс CanActivateChild в импортированные токены из пакета маршрутизатора.

Далее, реализуйте метод CanActivateChild, который принимает те же аргументы, что и метод CanActivate: ActivatedRouteSnapshot и RouterStateSnapshot. Метод CanActivateChild может возвращать Observable<boolean> или Promise<boolean> для асинхронных проверок и boolean для синхронных проверок. Этот возвращает boolean:

src/app/auth-guard.service.ts (фрагмент)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

/* . . . */
}

Добавьте ту же стражу AuthGuard к маршруту component-less администратора, чтобы защитить все остальные дочерние маршруты сразу вместо добавления AuthGuard к каждому маршруту индивидуально.

src/app/admin/admin-routing.module.ts (фрагмент)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

CanDeactivate: обработка несохранённых изменений

В рабочем процессе «Герои» приложение принимает все изменения героя сразу без колебаний или проверки.

В реальном мире вам, возможно, придётся накапливать изменения пользователя. Возможно, вам придётся проверять данные по всем полям. Возможно, вам придётся выполнить проверку на сервере. Возможно, вам придётся хранить изменения в состоянии ожидания до тех пор, пока пользователь не подтвердит их вместе или не отменит и не вернёт все изменения.

Что делать с неутверждёнными, несохранёнными изменениями, когда пользователь переходит к другому экрану? Вы не можете просто отпустить изменения и рискнуть потерять их; это было бы ужасным опытом.

Лучше приостановить процесс и позволить пользователю принять решение. Если пользователь отменяет, приложение останется на месте и позволит внести больше изменений. Если пользователь подтверждает, приложение может сохранить изменения.

Вы по-прежнему можете отложить навигацию до успешного сохранения. Если вы позволите пользователю перейти к следующему экрану сразу, а сохранение не пройдёт (например, данные будут признаны невалидными), вы потеряете контекст ошибки.

Вы не можете заблокировать выполнение операции, ожидая ответа сервера — это невозможно в браузере. Вам нужно приостановить навигацию, пока вы асинхронно ждёте ответа сервера.

Вам нужна стража CanDeactivate.

Отмена и сохранение

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

Пользователи обновляют информацию о кризисах в CrisisDetailComponent. В отличие от HeroDetailComponent, изменения пользователя не обновляют сущность кризиса немедленно. Вместо этого приложение обновляет сущность, когда пользователь нажимает кнопку «Сохранить», и отбрасывает изменения, когда пользователь нажимает кнопку «Отмена».

Обе кнопки перенаправляют пользователя обратно в список кризисов после сохранения или отмены.

src/app/crisis-center/crisis-detail.component.ts (методы отмены и сохранения)

cancel() {
  this.gotoCrises();
}

save() {
  this.crisis.name = this.editName;
  this.gotoCrises();
}

Что произойдёт, если пользователь попытается перейти к другому экрану без сохранения или отмены? Пользователь может нажать кнопку «Назад» в браузере или перейти по ссылке «герои». Оба действия вызывают навигацию. Приложение должно автоматически сохранить или отменить изменения?

В этом примере ничего не делается. Вместо этого, он просит пользователя явно принять это решение в диалоговом окне подтверждения, которое асинхронно ждёт ответа пользователя.

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

Стража DialogService, предоставленная в AppModule для использования во всём приложении, задаёт вопрос.

Она возвращает обещание, которое разрешается, когда пользователь, в конце концов, решит, что делать: либо отбросить изменения и перейти к другому экрану (true), либо сохранить ожидаемые изменения и остаться в редакторе кризисов (false).

Создайте стражу, которая проверяет наличие метода canDeactivate в компоненте — любом компоненте. Компонент CrisisDetailComponent будет иметь этот метод. Но страже это знать не обязательно. Стража не должна знать подробности метода деактивации любого компонента. Ей нужно только обнаружить, что у компонента есть метод canDeactivate(), и вызвать его. Этот подход делает стражу многоразовой.

src/app/can-deactivate-guard.service.ts

import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

В качестве альтернативы вы можете создать стражу CanDeactivate, специфичную для компонента, для компонента CrisisDetailComponent. Метод canDeactivate() предоставляет вам текущий экземпляр компонента component, текущий ActivatedRoute и RouterStateSnapshot, если вам нужно получить доступ к некоторой внешней информации. Это может быть полезно, если вы хотите использовать эту стражу только для этого компонента и вам нужно получить свойства компонента или подтвердить, должен ли маршрутизатор разрешить навигацию от него.

src/app/can-deactivate-guard.service.ts (специфичный для компонента)

import { Injectable }           from '@angular/core';
import { CanDeactivate,
         ActivatedRouteSnapshot,
         RouterStateSnapshot }  from '@angular/router';

import { CrisisDetailComponent } from './crisis-center/crisis-detail.component';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {

  canDeactivate(
    component: CrisisDetailComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<boolean> | boolean {
    // Get the Crisis Center ID
    console.log(route.params['id']);

    // Get the current URL
    console.log(state.url);

    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // promise which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}

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

src/app/crisis-center/crisis-detail.component.ts (фрагмент)

canDeactivate(): Promise<boolean> | boolean {
  // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
  if (!this.crisis || this.crisis.name === this.editName) {
    return true;
  }
  // Otherwise ask the user with the dialog service and return its
  // promise which resolves to true or false when the user decides
  return this.dialogService.confirm('Discard changes?');
}

Обратите внимание, что метод canDeactivate может возвращать синхронно; он возвращает true немедленно, если кризиса нет или нет ожидаемых изменений. Но он также может возвращать Promise или Observable, и маршрутизатор будет ждать, пока это разрешится в правдивое (перейти) или ложное (остаться на месте).

Добавьте Guard в маршрут детали кризиса в crisis-center-routing.module.ts с помощью массива canDeactivate.

src/app/crisis-center/crisis-center-routing.module.ts (стража деактивации)

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

import { CanDeactivateGuard }    from '../can-deactivate-guard.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard]
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class CrisisCenterRoutingModule { }

Добавьте Guard в основной массив AppRoutingModule providers, чтобы Router мог ввести его во время процесса навигации.

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { ComposeMessageComponent } from './compose-message.component';
import { CanDeactivateGuard }      from './can-deactivate-guard.service';
import { PageNotFoundComponent }   from './not-found.component';

const appRoutes: Routes = [
  {
    path: 'compose',
    component: ComposeMessageComponent,
    outlet: 'popup'
  },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CanDeactivateGuard
  ]
})
export class AppRoutingModule {}

Теперь вы предоставили пользователю защиту от несохранённых изменений.

Resolve: предварительная выборка данных компонента

В Hero Detail и Crisis Detail приложение ждало, пока маршрут не активируется, чтобы получить соответствующего героя или кризис.

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

Предпочтительнее предварительно извлечь данные с сервера, чтобы они были готовы в момент активации маршрута. Это также позволяет обрабатывать ошибки до перехода к компоненту. Нет смысла переходить к детали кризиса для id, который не имеет записи. Лучше вернуться к Crisis List, отображающему только валидные центры кризисов.

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

Вам нужен резолвер.

Извлечение данных до навигации

В данный момент CrisisDetailComponent извлекает выбранный кризис. Если кризис не найден, он возвращается к представлению списка кризисов.

Опыт может быть лучше, если все это будет обработано сначала, до активации маршрута. Сервис CrisisDetailResolver может извлечь Crisis или перейти к другому маршруту, если Crisis не существует до активации маршрута и создания CrisisDetailComponent.

Создайте файл crisis-detail-resolver.service.ts в области функциональности Crisis Center.

src/app/crisis-center/crisis-detail-resolver.service.ts

import { Injectable }             from '@angular/core';
import { Router, Resolve, RouterStateSnapshot,
         ActivatedRouteSnapshot } from '@angular/router';

import { Crisis, CrisisService } from './crisis.service';

@Injectable()
export class CrisisDetailResolver implements Resolve<Crisis> {
  constructor(private cs: CrisisService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Crisis> {
    let id = route.params['id'];

    return this.cs.getCrisis(id).then(crisis => {
      if (crisis) {
        return crisis;
      } else { // id not found
        this.router.navigate(['/crisis-center']);
        return null;
      }
    });
  }
}

Возьмите соответствующие части логики извлечения кризиса в CrisisDetailComponent.ngOnInit и перенесите их в CrisisDetailResolver. Импортируйте модель Crisis, CrisisService и Router, чтобы вы могли перейти на другой маршрут, если не удается извлечь кризис.

Будьте явными. Реализуйте интерфейс Resolve с типом Crisis.

Внедрите CrisisService и Router и реализуйте метод resolve(). Этот метод может возвращать Promise, Observable или синхронное значение.

Метод CrisisService.getCrisis возвращает промис. Верните этот промис, чтобы предотвратить загрузку маршрута до извлечения данных. Если он не возвращает допустимое значение Crisis, перенаправьте пользователя обратно на CrisisListComponent, отменив предыдущее перенаправление на CrisisDetailComponent.

Импортируйте этот резолвер в crisis-center-routing.module.ts и добавьте объект resolve в конфигурацию маршрута CrisisDetailComponent.

Не забудьте добавить сервис CrisisDetailResolver в массив CrisisCenterRoutingModule's providers.

src/app/crisis-center/crisis-center-routing.module.ts (resolver)

import { CrisisDetailResolver }   from './crisis-detail-resolver.service';

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CrisisDetailResolver
  ]
})
export class CrisisCenterRoutingModule { }

CrisisDetailComponent больше не должен извлекать кризис. Обновите CrisisDetailComponent, чтобы получить кризис из свойства ActivatedRoute.data.crisis вместо этого; вы указали, что он должен быть там, когда вы переконфигурировали маршрут. Он будет там, когда CrisisDetailComponent запросит его.

src/app/crisis-center/crisis-detail.component.ts (ngOnInit v2)

ngOnInit() {
  this.route.data
    .subscribe((data: { crisis: Crisis }) => {
      this.editName = data.crisis.name;
      this.crisis = data.crisis;
    });
}

Два важных момента

  1. Интерфейс Resolve маршрутизатора является необязательным. CrisisDetailResolver не наследуется от базового класса. Маршрутизатор ищет этот метод и вызывает его, если он найден.

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

Следующий код Центра кризисов для этого этапа.

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1 class="title">Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
      <a routerLink="/admin" routerLinkActive="active">Admin</a>
      <a routerLink="/login" routerLinkActive="active">Login</a>
      <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
    </nav>
    <router-outlet></router-outlet>
    <router-outlet name="popup"></router-outlet>
  `
})
export class AppComponent {
}
crisis-center-home.component.ts
// #docplaster
import { Component } from '@angular/core';

@Component({
  template: `
    <p>Welcome to the Crisis Center</p>
  `
})
export class CrisisCenterHomeComponent { }
crisis-center.component.ts
// #docplaster
import { Component } from '@angular/core';

@Component({
  template:  `
    <h2>CRISIS CENTER</h2>
    <router-outlet></router-outlet>
  `
})
export class CrisisCenterComponent { }
crisis-center-routing.module.ts
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

import { CanDeactivateGuard }     from '../can-deactivate-guard.service';

import { CrisisDetailResolver }   from './crisis-detail-resolver.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard],
            resolve: {
              crisis: CrisisDetailResolver
            }
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CrisisDetailResolver
  ]
})
export class CrisisCenterRoutingModule { }
crisis-list.component.ts
import 'rxjs/add/operator/switchMap';
import { Component, OnInit }      from '@angular/core';
import { ActivatedRoute, Router, Params } from '@angular/router';

import { Observable }            from 'rxjs/Observable';

import { Crisis, CrisisService } from './crisis.service';

@Component({
  template: `
    <ul class="items">
      <li *ngFor="let crisis of crises | async"
        (click)="onSelect(crisis)"
        [class.selected]="isSelected(crisis)">
          <span class="badge">{{ crisis.id }}</span>
          {{ crisis.name }}
      </li>
    </ul>

    <router-outlet></router-outlet>
  `
})
export class CrisisListComponent implements OnInit {
  crises: Observable<Crisis[]>;
  selectedId: number;

  constructor(
    private service: CrisisService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  isSelected(crisis: Crisis) {
    return crisis.id === this.selectedId;
  }

  ngOnInit() {
    this.crises = this.route.params
      .switchMap((params: Params) => {
        this.selectedId = +params['id'];
        return this.service.getCrises();
      });
  }

  onSelect(crisis: Crisis) {
    this.selectedId = crisis.id;

    // Navigate with relative link
    this.router.navigate([crisis.id], { relativeTo: this.route });
  }
}
crisis-detail.component.ts
import { Component, OnInit, HostBinding } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { slideInDownAnimation }   from '../animations';
import { Crisis }         from './crisis.service';
import { DialogService }  from '../dialog.service';

@Component({
  template: `
  <div *ngIf="crisis">
    <h3>"{{ editName }}"</h3>
    <div>
      <label>Id: </label>{{ crisis.id }}</div>
    <div>
      <label>Name: </label>
      <input [(ngModel)]="editName" placeholder="name"/>
    </div>
    <p>
      <button (click)="save()">Save</button>
      <button (click)="cancel()">Cancel</button>
    </p>
  </div>
  `,
  styles: ['input {width: 20em}'],
  animations: [ slideInDownAnimation ]
})
export class CrisisDetailComponent implements OnInit {
  @HostBinding('@routeAnimation') routeAnimation = true;
  @HostBinding('style.display')   display = 'block';
  @HostBinding('style.position')  position = 'absolute';

  crisis: Crisis;
  editName: string;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    public dialogService: DialogService
  ) {}

  ngOnInit() {
    this.route.data
      .subscribe((data: { crisis: Crisis }) => {
        this.editName = data.crisis.name;
        this.crisis = data.crisis;
      });
  }

  cancel() {
    this.gotoCrises();
  }

  save() {
    this.crisis.name = this.editName;
    this.gotoCrises();
  }

  canDeactivate(): Promise<boolean> | boolean {
    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!this.crisis || this.crisis.name === this.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // promise which resolves to true or false when the user decides
    return this.dialogService.confirm('Discard changes?');
  }

  gotoCrises() {
    let crisisId = this.crisis ? this.crisis.id : null;
    // Pass along the crisis id if available
    // so that the CrisisListComponent can select that crisis.
    // Add a totally useless `foo` parameter for kicks.
    // Relative navigation back to the crises
    this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
  }
}
crisis-detail-resolver.service.ts
import { Injectable }             from '@angular/core';
import { Router, Resolve, RouterStateSnapshot,
         ActivatedRouteSnapshot } from '@angular/router';

import { Crisis, CrisisService } from './crisis.service';

@Injectable()
export class CrisisDetailResolver implements Resolve<Crisis> {
  constructor(private cs: CrisisService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Crisis> {
    let id = route.params['id'];

    return this.cs.getCrisis(id).then(crisis => {
      if (crisis) {
        return crisis;
      } else { // id not found
        this.router.navigate(['/crisis-center']);
        return null;
      }
    });
  }
}
crisis.service.ts
export class Crisis {
  constructor(public id: number, public name: string) { }
}

const CRISES = [
  new Crisis(1, 'Dragon Burning Cities'),
  new Crisis(2, 'Sky Rains Great White Sharks'),
  new Crisis(3, 'Giant Asteroid Heading For Earth'),
  new Crisis(4, 'Procrastinators Meeting Delayed Again'),
];

let crisesPromise = Promise.resolve(CRISES);

import { Injectable } from '@angular/core';

@Injectable()
export class CrisisService {

  static nextCrisisId = 100;

  getCrises() { return crisesPromise; }

  getCrisis(id: number | string) {
    return crisesPromise
      .then(crises => crises.find(crisis => crisis.id === +id));
  }

}
auth-guard.service.ts
import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Navigate to the login page
    this.router.navigate(['/login']);
    return false;
  }
}
can-deactivate-guard.service.ts
import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

Параметры запроса и фрагменты

В примере с параметрами маршрута вы обрабатывали только параметры, специфичные для маршрута, но что, если вам нужны необязательные параметры, доступные для всех маршрутов? Здесь вступают в игру параметры запроса.

Фрагменты относятся к определённым элементам на странице, идентифицированным с помощью атрибута id.

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

Добавьте элемент anchor, чтобы вы могли перейти к определённой точке на странице.

Добавьте объект NavigationExtras в метод router.navigate, который перенаправляет вас на маршрут /login.

src/app/auth-guard.service.ts (v3)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  NavigationExtras
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Create a dummy session id
    let sessionId = 123456789;

    // Set our navigation extras object
    // that contains our global query params and fragment
    let navigationExtras: NavigationExtras = {
      queryParams: { 'session_id': sessionId },
      fragment: 'anchor'
    };

    // Navigate to the login page with extras
    this.router.navigate(['/login'], navigationExtras);
    return false;
  }
}

Вы также можете сохранять параметры запроса и фрагменты при переходах, не предоставляя их заново при переходе. В LoginComponent вы добавите объект в качестве второго аргумента в функцию router.navigate и предоставите preserveQueryParams и preserveFragment, чтобы передать текущие параметры запроса и фрагмент в следующий маршрут.

src/app/login.component.ts (preserve)

// Set our navigation extras object
// that passes on our global query params and fragment
let navigationExtras: NavigationExtras = {
  preserveQueryParams: true,
  preserveFragment: true
};

// Redirect the user
this.router.navigate([redirect], navigationExtras);

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

src/app/admin/admin-dashboard.component.ts (v2)

import { Component, OnInit }  from '@angular/core';
import { ActivatedRoute }     from '@angular/router';
import { Observable }         from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Component({
  template:  `
    <p>Dashboard</p>

    <p>Session ID: {{ sessionId | async }}</p>
    <a id="anchor"></a>
    <p>Token: {{ token | async }}</p>
  `
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParams
      .map(params => params['session_id'] || 'None');

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .map(fragment => fragment || 'None');
  }
}

Параметры запроса и Фрагменты также доступны через сервис ActivatedRoute. Как и параметры маршрута, параметры запроса и фрагменты предоставляются в виде Observable. Обновлённый компонент Администратор кризисов передаёт Observable напрямую в шаблон с помощью AsyncPipe.

Чтобы увидеть изменения URL в адресной строке браузера в примере, откройте его снова в редакторе Plunker, нажав на значок в правом верхнем углу, затем разверните окно предварительного просмотра, нажав на синюю кнопку 'X' в правом верхнем углу.

pop out the windowpop out the window

Теперь вы можете нажать кнопку Админ, которая перенаправит вас на страницу Вход с предоставленными query params и fragment. После нажатия кнопки входа в систему вы перенаправлены на страницу Admin Dashboard с query params и fragment, которые по-прежнему действуют.

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

query params и fragment также можно сохранить с помощью RouterLink с preserveQueryParams и preserveFragment привязками соответственно.

Этап 6: Асинхронный маршрутизация

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

Как справиться с этой проблемой? С помощью асинхронного маршрутизирования, которое загружает функциональные модули лениво, по запросу. Ленивая загрузка имеет много преимуществ.

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

Вы уже прошли часть пути. Организуя приложение в модули—AppModule, HeroesModule, AdminModule и CrisisCenterModule—у вас есть естественные кандидаты для ленивой загрузки.

Некоторые модули, например AppModule, должны загружаться с самого начала. Но другие можно и следует загружать лениво. Например, AdminModule нужен лишь нескольким авторизованным пользователям, поэтому вы должны загружать его только тогда, когда его запрашивают соответствующие пользователи.

Конфигурация ленивой загрузки маршрутов

Измените путь admin в admin-routing.module.ts с 'admin' на пустую строку, '', пустой путь.

Router поддерживает маршруты с пустым путём; используйте их для группировки маршрутов без добавления дополнительных сегментов пути в URL. Пользователи всё равно перейдут на /admin, и AdminComponent по-прежнему служит компонентом маршрутизации, содержащим дочерние маршруты.

Откройте AppRoutingModule и добавьте новый маршрут admin в массив appRoutes.

Задайте свойство loadChildren (а не свойство children!), равное адресу модуля AdminModule. Адрес — это расположение файла AdminModule (относительно корня приложения), после которого идёт разделитель #, а затем имя экспортированного класса модуля AdminModule.

app-routing.module.ts (load children)

{
  path: 'admin',
  loadChildren: 'app/admin/admin.module#AdminModule',
},

Когда маршрутизатор переходит на этот маршрут, он использует строку loadChildren для динамической загрузки AdminModule. Затем он добавляет маршруты AdminModule к текущей конфигурации маршрута. Наконец, он загружает запрошенный маршрут в целевой компонент администратора.

Ленивая загрузка и переконфигурация происходят только один раз, когда маршрут запрашивается в первый раз; модуль и маршруты становятся доступны сразу для последующих запросов.

Angular предоставляет встроенный загрузчик модулей, который поддерживает SystemJS для асинхронной загрузки модулей. Если вы используете другой инструмент сборки, такой как Webpack, вы будете использовать механизм Webpack для асинхронной загрузки модулей.

Осуществите заключительный шаг и отсоедините функциональную область администрирования от основного приложения. Корневой AppModule не должен загружать и не должен ссылаться на AdminModule или его файлы.

В app.module.ts удалите оператор импорта AdminModule из верхней части файла и удалите AdminModule из массива imports модуля Angular.

CanLoad Защитник: защита от несанкционированной загрузки функциональных модулей

Вы уже защищаете AdminModule с помощью защитника CanActivate, который предотвращает доступ неавторизованных пользователей к функциональной области администрирования. Он перенаправляет пользователя на страницу входа, если он не авторизован.

Но маршрутизатор по-прежнему загружает AdminModule, даже если пользователь не может посетить ни один из его компонентов. В идеале, вы бы загружали AdminModule только в том случае, если пользователь авторизован.

Добавьте защитник CanLoad, который загружает AdminModule только тогда, когда пользователь авторизован и пытается получить доступ к функциональной области администрирования.

Существующий AuthGuard уже имеет необходимую логику в методе checkLogin() для поддержки защитника CanLoad.

Откройте auth-guard.service.ts. Импортируйте интерфейс CanLoad из @angular/router. Добавьте его в список implements класса AuthGuard. Затем реализуйте canLoad следующим образом:

src/app/auth-guard.service.ts (CanLoad страж)

canLoad(route: Route): boolean {
  let url = `/${route.path}`;

  return this.checkLogin(url);
}

Маршрутизатор устанавливает параметр route метода canLoad() в целевой URL. Метод checkLogin() перенаправляет на этот URL после входа пользователя.

Теперь импортируйте AuthGuard в AppRoutingModule и добавьте AuthGuard в массив canLoad для маршрута admin. Завершенный маршрут администратора выглядит так:

app-routing.module.ts (ленивый маршрут администратора)

  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
    canLoad: [AuthGuard]
  },

Предварительная загрузка: фоновая загрузка областей функциональности

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

Это может показаться тем, что приложение делает всё это время. Не совсем так. AppModule загружается при запуске приложения; это жадная загрузка. Сейчас AdminModule загружается только при нажатии пользователем на ссылку; это ленивая загрузка.

Предварительная загрузка находится где-то посередине. Рассмотрим Центр кризисов. Это не первый просматриваемый пользователем вид. По умолчанию Герои — это первый вид. Для наименьшего начального объёма данных и самого быстрого времени запуска вы должны загрузить жадно AppModule и HeroesModule.

Вы могли бы загрузить Центр кризисов по требованию. Но вы практически уверены, что пользователь посетит Центр кризисов в течение нескольких минут после запуска приложения. В идеале приложение запускалось бы с загруженными AppModule и HeroesModule, а затем, практически сразу, загружало бы CrisisCenterModule в фоновом режиме. К тому времени, когда пользователь перейдёт в Центр кризисов, его модуль будет загружен и готов к работе.

Это и есть предварительная загрузка.

Как работает предварительная загрузка

После каждой успешной навигации маршрутизатор ищет в своей конфигурации не загруженный модуль, который можно загрузить в предварительном режиме. Загрузит ли он модуль и какие модули загрузит, зависит от стратегии предварительной загрузки.

Router предоставляет две стратегии предварительной загрузки "из коробки":

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

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

В этом следующем разделе вы обновите CrisisCenterModule, чтобы он загружался по требованию по умолчанию, и используйте стратегию PreloadAllModules, чтобы загрузить его (и все остальные модули, загружаемые по требованию) как можно скорее.

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

Обновите конфигурацию маршрута, чтобы загрузить CrisisCenterModule по требованию. Выполните те же действия, которые вы использовали для настройки AdminModule для ленивой загрузки.

  1. Измените путь crisis-center в CrisisCenterRoutingModule на пустую строку.

  2. Добавьте маршрут crisis-center в AppRoutingModule.

  3. Установите строку loadChildren для загрузки CrisisCenterModule.

  4. Удалите все упоминания о CrisisCenterModule из app.module.ts.

Вот обновлённые модули до включения предварительной загрузки:

app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';
import { Router } from '@angular/router';

import { AppComponent }            from './app.component';
import { AppRoutingModule }        from './app-routing.module';

import { HeroesModule }            from './heroes/heroes.module';
import { ComposeMessageComponent } from './compose-message.component';
import { LoginRoutingModule }      from './login-routing.module';
import { LoginComponent }          from './login.component';
import { PageNotFoundComponent }   from './not-found.component';

import { DialogService }           from './dialog.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    LoginRoutingModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    ComposeMessageComponent,
    LoginComponent,
    PageNotFoundComponent
  ],
  providers: [
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  }
}
app-routing.module.ts
import { NgModule }     from '@angular/core';
import {
  RouterModule, Routes,
} from '@angular/router';

import { ComposeMessageComponent } from './compose-message.component';
import { PageNotFoundComponent }   from './not-found.component';

import { CanDeactivateGuard }      from './can-deactivate-guard.service';
import { AuthGuard }               from './auth-guard.service';

const appRoutes: Routes = [
  {
    path: 'compose',
    component: ComposeMessageComponent,
    outlet: 'popup'
  },
  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
    canLoad: [AuthGuard]
  },
  {
    path: 'crisis-center',
    loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule'
  },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes
    )
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CanDeactivateGuard
  ]
})
export class AppRoutingModule {}
crisis-center-routing.module.ts
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

import { CanDeactivateGuard }     from '../can-deactivate-guard.service';
import { CrisisDetailResolver }   from './crisis-detail-resolver.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard],
            resolve: {
              crisis: CrisisDetailResolver
            }
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CrisisDetailResolver
  ]
})
export class CrisisCenterRoutingModule { }

Вы можете попробовать это сейчас и подтвердить, что CrisisCenterModule загружается после нажатия кнопки "Центр кризисов".

Чтобы включить предварительную загрузку всех модулей, загружаемых по требованию, импортируйте токен PreloadAllModules из пакета Angular Router.

Второй аргумент в методе RouterModule.forRoot принимает объект для дополнительных параметров конфигурации. preloadingStrategy — один из таких параметров. Добавьте токен PreloadAllModules в вызов forRoot:

src/app/app-routing.module.ts (предварительная загрузка всех)

    RouterModule.forRoot(
      appRoutes
      , { preloadingStrategy: PreloadAllModules }
    )

Это сообщает загрузчику предварительной загрузки Router немедленно загрузить все маршруты, загружаемые по требованию (маршруты с свойством loadChildren).

При посещении http://localhost:3000 маршрут /heroes загружается сразу при запуске, и маршрутизатор начинает загружать CrisisCenterModule сразу после загрузки HeroesModule.

Удивительно, что AdminModule не загружается в предварительном режиме. Что-то блокирует его.

CanLoad блокирует предварительную загрузку

Стратегия PreloadAllModules не загружает области функциональности, защищённые стражем CanLoad. Это сделано по дизайну.

Вы добавили стража CanLoad в маршрут в AdminModule несколько шагов назад, чтобы заблокировать загрузку этого модуля до авторизации пользователя. Этот страж CanLoad имеет приоритет над стратегией предварительной загрузки.

Если вы хотите загрузить модуль в предварительном режиме и защитить его от несанкционированного доступа, удалите стража canLoad и полагайтесь только на стража CanActivate.

Настройка стратегии предварительной загрузки

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

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

В этом разделе вы добавите стратегию, которая только загружает маршруты, чья метка data.preload установлена в true. Вспомните, что вы можете добавить что угодно в свойство data маршрута.

Установите метку data.preload в маршруте crisis-center в AppRoutingModule.

src/app/app-routing.module.ts (данные маршрута preload)

{
  path: 'crisis-center',
  loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
  data: { preload: true }
},

Добавьте в проект новый файл под названием selective-preloading-strategy.ts и определите в нём класс сервиса SelectivePreloadingStrategy следующим образом:

src/app/selective-preloading-strategy.ts (фрагмент)

import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data['preload']) {
      // add the route path to the preloaded module array
      this.preloadedModules.push(route.path);

      // log the route path to the console
      console.log('Preloaded: ' + route.path);

      return load();
    } else {
      return Observable.of(null);
    }
  }
}

SelectivePreloadingStrategy реализует интерфейс PreloadingStrategy, который имеет один метод, preload.

Маршрутизатор вызывает метод preload с двумя аргументами:

  1. Маршрут для рассмотрения.
  2. Функция загрузки, которая может асинхронно загрузить маршрутизируемый модуль.

Реализация preload должна вернуть Observable. Если маршрут следует загрузить в предварительном режиме, он возвращает observable, возвращаемое вызовом функции загрузки. Если маршрут не следует загружать в предварительном режиме, он возвращает Observable типа null.

В этом примере метод preload загружает маршрут, если метка data.preload маршрута имеет истинное значение.

Он также имеет побочное действие. SelectivePreloadingStrategy записывает path выбранного маршрута в свой публичный массив preloadedModules.

Вскоре вы расширите AdminDashboardComponent, чтобы добавить в него этот сервис и отобразить его массив preloadedModules.

Но сначала внесите несколько изменений в AppRoutingModule.

  1. Импортируйте SelectivePreloadingStrategy в AppRoutingModule.
  2. Замените стратегию PreloadAllModules в вызове forRoot на эту стратегию SelectivePreloadingStrategy.
  3. Добавьте стратегию SelectivePreloadingStrategy в массив поставщиков AppRoutingModule, чтобы он мог быть инжектирован в другие части приложения.

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

  1. Импортируйте сервис SelectivePreloadingStrategy.
  2. Внедрите его в конструктор панели управления.
  3. Обновите шаблон, чтобы отобразить массив preloadedModules сервиса стратегии.

После завершения это выглядит так.

src/app/admin/admin-dashboard.component.ts (предварительно загруженные модули)

import { Component, OnInit }    from '@angular/core';
import { ActivatedRoute }       from '@angular/router';
import { Observable }           from 'rxjs/Observable';

import { SelectivePreloadingStrategy } from '../selective-preloading-strategy';

import 'rxjs/add/operator/map';

@Component({
  template:  `
    <p>Dashboard</p>

    <p>Session ID: {{ sessionId | async }}</p>
    <a id="anchor"></a>
    <p>Token: {{ token | async }}</p>

    Preloaded Modules
    <ul>
      <li *ngFor="let module of modules">{{ module }}</li>
    </ul>
  `
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;
  modules: string[];

  constructor(
    private route: ActivatedRoute,
    private preloadStrategy: SelectivePreloadingStrategy
  ) {
    this.modules = preloadStrategy.preloadedModules;
  }

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParams
      .map(params => params['session_id'] || 'None');

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .map(fragment => fragment || 'None');
  }
}

После загрузки приложения начального маршрута CrisisCenterModule загружается в предварительном режиме. Проверьте это, войдя в область функциональности Admin и отметьте, что crisis-center находится в списке Preloaded Modules. Он также записывается в консоль браузера.

Проверка конфигурации маршрутизатора

Вы приложили много усилий к настройке маршрутизатора в нескольких файлах модулей маршрутизации и позаботились о том, чтобы перечислить их в правильном порядке. Действительно ли маршруты оцениваются так, как вы планировали? Как на самом деле сконфигурирован маршрутизатор?

Вы можете проверить текущую конфигурацию маршрутизатора в любое время, внедрив его и изучив его свойство config. Например, обновите AppModule следующим образом и посмотрите в окне консоли браузера, чтобы увидеть завершённую конфигурацию маршрута.

src/app/app.module.ts (проверка конфигурации маршрутизатора)

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

export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  }
}

Заключение и окончательное приложение

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

Приложения

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

Материалы приложений не являются обязательными. Дальнейшее чтение — для любопытных.

Приложение: массив параметров ссылки

Массив параметров ссылки содержит следующие ингредиенты для навигации по маршрутизатору:

  • Путь маршрута к компоненту назначения.
  • Обязательные и необязательные параметры маршрута, которые попадают в URL маршрута.

Вы можете привязать директиву RouterLink к такому массиву следующим образом:

<a [routerLink]="['/heroes']">Heroes</a>

Вы написали массив из двух элементов при указании параметра маршрута так:

this.router.navigate(['/hero', hero.id]);

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

<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>

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

Вспомните, что вы определили маршрут дочерней ветви по умолчанию для центра кризисов, поэтому это простое RouterLink подходит.

<a [routerLink]="['/crisis-center']">Crisis Center</a>

Обработайте его.

  • Первый элемент в массиве определяет родительский маршрут (/crisis-center).
  • Для этого родительского маршрута нет параметров, поэтому вы закончили с ним.
  • Для дочернего маршрута нет значения по умолчанию, поэтому вам нужно выбрать одно.
  • Вы переходите к CrisisListComponent, путь маршрута которого равен /, но вам не нужно явно добавлять слеш.
  • Вот и все! ['/crisis-center'].

Перейдем к следующему шагу. Рассмотрим следующую ссылку маршрутизатора, которая перемещается от корня приложения к Кризису дракона:

<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>
  • Первый элемент в массиве определяет родительский маршрут (/crisis-center).
  • Для этого родительского маршрута нет параметров, поэтому вы закончили с ним.
  • Второй элемент определяет детали дочернего маршрута о конкретном кризисе (/:id).
  • Дочерний маршрут деталей требует параметра маршрута id.
  • Вы добавили id Кризиса дракона как второй элемент в массив (1).
  • Полученный путь равен /crisis-center/1.

Если бы вы захотели, вы могли бы переопределить AppComponent шаблон исключительно для маршрутов Центра кризисов:

template: `
  <h1 class="title">Angular Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/crisis-center/1', { foo: 'foo' }]">Dragon Crisis</a>
    <a [routerLink]="['/crisis-center/2']">Shark Crisis</a>
  </nav>
  <router-outlet></router-outlet>
`

В итоге, вы можете создавать приложения с одним, двумя или более уровнями маршрутизации. Массив параметров ссылок обеспечивает гибкость для представления любой глубины маршрутизации и любой допустимой последовательности путей маршрутов, (обязательных) параметров маршрутизатора и (необязательных) объектов параметров маршрута.

Приложение: LocationStrategy и стили URL браузера

Когда маршрутизатор переходит к новому представлению компонента, он обновляет расположение и историю браузера с помощью URL этого представления. Это строго локальный URL. Браузер не должен отправлять этот URL на сервер и не должен перезагружать страницу.

Современные браузеры HTML5 поддерживают history.pushState, техника, которая изменяет расположение и историю браузера, не вызывая запроса к серверу страницы. Маршрутизатор может составить "естественный" URL, неотличимый от URL, который в противном случае потребовал бы загрузки страницы.

Вот URL Центра кризисов в этом стиле "HTML5 pushState":

localhost:3002/crisis-center/

Старые браузеры отправляют запросы на сервер, когда изменяется URL расположения, кроме случаев, когда изменение происходит после "#" (называемого "хешем"). Маршрутизаторы могут использовать это исключение, составляя URL маршрутов внутри приложения с хешами. Вот "хеш URL", который перенаправляет на Центр кризисов.

localhost:3002/src/#/crisis-center/

Маршрутизатор поддерживает оба стиля с двумя LocationStrategy поставщиками:

  1. PathLocationStrategy — стиль по умолчанию "HTML5 pushState".
  2. HashLocationStrategy — стиль "хеш URL".

Функция RouterModule.forRoot устанавливает LocationStrategy в PathLocationStrategy, делая её стратегией по умолчанию. Вы можете переключиться на HashLocationStrategy с заменой во время процесса запуска, если вы предпочитаете это.

Узнайте о поставщиках и процессе запуска в руководстве по инъекции зависимостей.

Какая стратегия лучше?

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

Практически все проекты Angular должны использовать стиль HTML5 по умолчанию. Он генерирует URL, которые легче понять пользователям. И он сохраняет возможность выполнения рендеринга на стороне сервера позже.

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

Этот вариант доступен только в том случае, если URL приложения выглядят как обычные веб-URL без хешей (#) посередине.

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

HTML5 URL и <base href>

Хотя маршрутизатор по умолчанию использует стиль HTML5 pushState, вы обязательно должны настроить эту стратегию с помощью base href.

Предпочтительный способ настройки стратегии — добавление тега <base href> в <head> index.html.

<base href="/">

Без этого тега браузер, возможно, не сможет загрузить ресурсы (изображения, CSS, скрипты) при "глубоком переходе" в приложение. Плохие вещи могут произойти, когда кто-то скопирует ссылку приложения в адресную строку браузера или нажмет на такую ссылку в электронном письме.

Некоторые разработчики могут не иметь возможности добавить элемент <base>, возможно, потому, что у них нет доступа к <head> или index.html.

Эти разработчики все еще могут использовать HTML5 URL, выполнив два исправительных шага:

  1. Предоставьте маршрутизатору соответствующее значение APP_BASE_HREF.
  2. Используйте корневые URL для всех веб-ресурсов: CSS, изображения, скрипты и HTML-файлы шаблонов.

HashLocationStrategy

Вы можете использовать старый подход с HashLocationStrategy, предоставив useHash: true в объекте в качестве второго аргумента функции RouterModule.forRoot в AppModule.

src/app/app.module.ts (стратегия хеш URL)

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { AppComponent }          from './app.component';
import { PageNotFoundComponent } from './not-found.component';

const routes: Routes = [

];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(routes, { useHash: true })  // .../#/crisis-center/
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [

  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/guide/router.html

Spec-Zone.ru

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