Книга: Angular и TypeScript. Сайтостроение для профессионалов
Назад: 2. Приступаем к работе с Angular
Дальше: 4. Внедрение зависимостей

3. Навигация с помощью маршрутизатора Angular

В этой главе:

конфигурация маршрутов;

• передача данных при переходе от одного маршрута к другому;

• создание более одной области для навигации (так называемая область отображения) на одной странице с использованием auxiliary;

• ленивая загрузка модулей с помощью маршрутизатора.

В главе 2 вы создали главную страницу онлайн-аукциона, намереваясь создать одностраничное приложение (single-page application, SPA): главная страница не перезагружается целиком, но ее части могут меняться. Теперь нужно добавить навигацию для этого приложения, чтобы область с содержимым изменялась в зависимости от действий пользователя. Ему нужно увидеть подробную информацию о продукте, ставки на продукты, а также пообщаться с продавцами. Маршрутизатор Angular позволяет сконфигурировать и реализовать подобную навигацию без перезагрузки страницы.

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

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

Сначала мы рассмотрим основные особенности маршрутизатора, а затем вы добавите в приложение-аукцион второе представление — Product Details (Информация о продукте). Когда пользователь выберет какой-то продукт на главной странице, это представление будет отображать подробную информацию о нем.

3.1. Основы маршрутизации

Одностраничные приложения можно рассматривать как коллекцию состояний, например, состояние Home, Product Details и Shipping. Каждое из них является каким-то представлением одного приложения. До этого момента мы работали только с одним представлением состояния: главной страницей.

Онлайн-аукцион (см. рис. 2.4) имеет в верхней части страницы панели навигации (компонент), слева — форму поиска (еще один компонент), в нижней части — нижний колонтитул (еще один компонент), и вам нужно, чтобы все они были видны постоянно. Остальная часть страницы состоит из области содержимого, в которой отображаются элементы <auction-carousel> и <auction-product>. Вы будете повторно использовать эту область содержимого (область отображения) для демонстрации разных представлений в зависимости от действий пользователя.

Для этого нужно сконфигурировать маршрутизатор так, чтобы он мог отображать разные представления в области отображения, заменяя одно представление другим. Данная область содержимого представлена тегом <router-outlet>. На рис. 3.1 показана область, которую вы будете использовать для отображения разных представлений.

72853.png 

Рис. 3.1. Выделение области для изменения представлений

примечание

На странице может находиться больше одной области отображения. Мы рассмотрим этот вопрос в разделе 3.5.

Вы будете присваивать компоненты для каждого представления, которое хотите отобразить в данной области. В главе 2 вы не создавали родительский компонент, который бы инкапсулировал карусель и продукты аукциона, но к концу этой главы перепишете код, создав HomeComponent, — он послужит предком для карусели и продуктов. Кроме того, вы создадите ProductDetailComponent; его задача — представлять подробную информацию о каждом продукте. В любой момент времени пользователь будет видеть либо HomeComponent, либо ProductDetailComponent в области <router-outlet>.

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

По мере того, как пользователь перемещается по приложению, оно может выполнять запросы на сервер для получения или отправки данных. Иногда представление (комбинация кода интерфейса и данных) уже загрузило в браузер все необходимое, но в некоторых случаях оно станет общаться с сервером, отправляя запросы Ajax или используя WebSockets. Каждое представление будет иметь уникальный URL, который демонстрируется в адресной строке браузера. Эту тему мы рассмотрим чуть позже.

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

3.1.1. Стратегии расположения

В любой момент времени адресная строка браузера отображает URL текущего представления. URL может состоять из разных частей (сегментов). Он начинается с протокола, за ним идет доменное имя и иногда номер порта. Параметры, которые нужно передать на сервер, обычно следуют за знаком вопроса (верно для HTTP-запросов GET), и выражение может выглядеть так:

Изменение любого символа в предшествующем URL приведет к генерации нового запроса на сервер.

<router-outlet></router-outlet>

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

HashLocationStrategy — символ решетки (#) добавляется к URL, и сегмент, стоящий после этого символа, уникальным образом определяет маршрут, который будет использован для фрагмента веб-страницы. Такая стратегия работает для всех браузеров, включая старые;

• PathLocationStrategy — стратегия, основанная на History API, работает только в браузерах, поддерживающих HTML5. Применяется в Angular по умолчанию.

Навигация с помощью символа решетки

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

72862.png 

Рис. 3.2. Составные части URL

Попробуйте поперемещаться по одностраничному приложению, такому как Gmail, и проследите за URL. Когда вы находитесь в папке Inbox (Входящие), он выглядит так: . Теперь перейдите в папку Sent (Отправленные), фрагмент URL с решеткой изменится с inbox на sent. Код JavaScript клиентской стороны вызывает необходимые функции для отображения представления Sent (Отправленные).

Но почему приложение Gmail демонстрирует сообщение Loading, когда вы переключаетесь на представление Sent (Отправленные)? Код JavaScript этого представления все еще может выполнять запросы Ajax на сервер для того, чтобы получить новые данные, но не загружает дополнительный код, разметку или код CSS.

В данной книге мы будем использовать именно такой способ навигации, а @NgMo­dule будет включать следующее значение providers (эта тема рассматривается в главе 4):

providers:[{provide: LocationStrategy, useClass: HashLocationStrategy}]

Навигация, основанная на History API

History API браузера позволяет перемещаться по истории посещений, а также программно манипулировать стеком с историей (см. раздел «Манипуляции с историей браузера» в Mozilla Developer Network, ). В частности, метод pushState() используется для того, чтобы прикрепить сегмент к основному URL, когда пользователь перемещается по одностраничному приложению.

Рассмотрим следующий URL: . Сегмент products/page/3 может быть прикреплен к основному URL программно без использования символа решетки. Если пользователь перейдет со страницы 3 на страницу 4, то код приложения прикрепит к основному URL конструкцию products/page/4, сохранив предыдущее состояние products/page/3 в историю браузера.

Angular избавляет от необходимости вызывать метод pushState() явно — нужно лишь сконфигурировать сегменты URL и соотнести их с необходимыми компонентами. Если вы используете стратегию навигации, основанную на History API, то должны указать Angular, что именно является основным URL в вашем приложении, чтобы он мог корректно добавлять сегменты URL клиентской стороны. Вы можете сделать это одним из следующих способов.

Добавить тег <base> в заголовок файла index.html, например, <base href="/">.

• Присвоить значение константы APP_BASE_HREF в корневом модуле и использовать его как значение свойства providers. В следующем фрагменте кода в качестве основного URL применяется /, но можно задействовать любой сегмент URL, который указывает на конец основного URL:

import { APP_BASE_HREF } from '@angular/common';

...

@NgModule({

...

providers:[{provide: APP_BASE_HREF, useValue: '/'}]

})

class AppModule { }

3.1.2. Составные части механизма навигации на стороне клиента

Ознакомимся с основными концепциями реализации навигации на стороне клиента с помощью маршрутизатора Angular. Во фреймворке Angular реализация функциональности маршрутизации располагается в отдельном модуле RouterModule. Если вашему приложению нужна маршрутизация, то убедитесь, что ваш файл package.json содержит зависимость @angular/router. Наш файл package.json включает следующую строку:

"@angular/router": "3.0.0".

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

Angular предлагает использовать следующие основные элементы при реализации маршрутизации в приложении.

Router — объект, который представляет собой маршрутизатор во время выполнения программы. Вы можете применять его методы navigate() и navigateByUrl() для того, чтобы переместиться либо по сконфигурированному пути маршрута, либо по сегменту URL соответственно.

• RouterOutlet — директива, служащая заполнителем внутри вашей веб-страницы (<router-outlet>), где router должен отрисовать элемент.

• Routes — массив маршрутов, соотносящих адреса URL и компоненты, которые должны быть отрисованы внутри <router-outlet>.

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

• ActivatedRoute — объект, представляющий маршрут или маршруты, активные в данный момент.

Маршруты конфигурируются в отдельном массиве объектов типа Route. Рассмотрим пример:

const routes: Routes = [

    {path: '', component: HomeComponent},

    {path: 'product', component: ProductDetailComponent}

];

Поскольку конфигурация маршрута выполняется на уровне модулей, нужно импортировать маршруты в декораторе @NgModule. Если вы объявляете маршруты для корневых модулей, то должны использовать метод forRoot(), например, так:

import { BrowserModule } from '@angular/platform-browser';

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

...

@NgModule({

  imports: [ BrowserModule, RouterModule.forRoot(routes)],

    ...

})

Если вы конфигурируете маршруты для модуля функциональности (не для корневого), то используйте метод forChild():

import { CommonModule } from '@angular/common';

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

...

@NgModule({

  imports: [ CommonModule, RouterModule.forChild(routes)],

    ...

})

Обратите внимание: в модулях функциональности вы импортируете модуль CommonModul, а не BrowserModule.

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

1. Сконфигурируйте маршруты для ваших приложений так, чтобы соотнести сегменты URL и соответствующие им компоненты, и передайте объект конфигурации либо методу RouterModule.forRoot(), либо RouterModule.forChild() в качестве аргумента. Если некоторые компоненты должны принимать входные значения, то можете использовать параметры маршрута.

2. Импортируйте возвращенное значение метода forRoot() или метода forChild() в декоратор @NgModule.

3. Определите область отображения, где маршрутизатор будет отрисовывать компоненты с помощью тега <router-outlet>.

4. Добавьте якорные теги HTML, к которым привязаны свойства [routerLink] (квадратные скобки указывают на привязку свойств). Теперь, когда пользователь нажмет ссылку, маршрутизатор отрисует соответствующий компонент. Свойство [routerLink] можно рассматривать как замену атрибуту href якорного тега HTML на клиентской стороне.

Вызов метода маршрутизатора navigate() — альтернатива использованию [routerLink] для навигации по маршруту. В любом из этих случаев маршрутизатор найдет соответствующий предоставленному пути компонент, создаст (или найдет) объект указанного компонента и исходя из этого обновит URL.

Рассмотрим пример приложения, иллюстрирующий эти шаги (его можно найти в каталоге router_samples). Предположим, вы хотите создать в верхней части страницы корневой компонент, который имеет две ссылки, Home и Product Details. Приложение должно отрисовывать один из этих компонентов в зависимости от того, какую ссылку нажмет пользователь.

Компонент HomeComponent будет отрисовывать текст Home Component на красном фоне, а ProductDetailComponent — текст Product Details Component на голубом. Изначально веб-страница должна отображать компонент HomeComponent, как показано на рис. 3.3. После того, как пользователь выберет ссылку Product Details, маршрутизатор должен отобразить компонент ProductDetailComponent, как показано на рис. 3.4.

Основная цель данного упражнения заключается в том, чтобы познакомить вас с маршрутизатором, поэтому элементы будут очень простыми. Рассмотрим код компонента HomeComponent (листинг 3.1).

72884.png 

Рис. 3.3. Маршрут Home приложения basic_routing

72895.png 

Рис. 3.4. Маршрут Product Details приложения basic_routing

Листинг 3.1. Код компонента HomeComponent

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

 

@Component({

    selector: 'home',

    template: '<h1 class="home">Home Component</h1>',

    styles: ['.home {background: red}']})

export class HomeComponent {}

Код компонента ProductDetailComponent выглядит похоже, но вместо красного цвета фона в нем используется голубой (листинг 3.2).

Листинг 3.2. Код компонента ProductDetailComponent

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

 

@Component({

    selector: 'product',

    template: '<h1 class="product">Product Details Component</h1>',

    styles: ['.product {background: cyan}']})

export class ProductDetailComponent {}

Сконфигурируем маршруты в отдельном файле, который называется app.rou­ting.ts (листинг 3.3).

Листинг 3.3. Содержимое файла app.routing.ts

72909.png 

Элемент HomeComponent соотносится с путем, содержащим пустую строку, что неявно делает его маршрутом по умолчанию.

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

export interface Route {

    path?: string;

    pathMatch?: string;

    component?: Type | string;

    redirectTo?: string;

    outlet?: string;

    canActivate?: any[];

    canActivateChild?: any[];

    canDeactivate?: any[];

    canLoad?: any[];

    data?: Data;

    resolve?: ResolveData;

    children?: Route[];

    loadChildren?: string;

}

Интерфейсы TypeScript описаны в приложении Б, но мы хотели бы напомнить: вопросительный знак, стоящий после свойства, подразумевает, что значение этого свойства задавать необязательно. Можно передать функциям forRoot() или forChild() объект конфигурации, в котором заполнено лишь несколько свойств. В простом приложении вы будете использовать только два свойства класса Route: path и component.

Следующий шаг заключается в создании корневого компонента, который будет содержать ссылки для навигации между представлениями Home (Главная страница) и Product Details (Информация о продукте). Корневой компонент AppComponent расположится в файле app.component.ts (листинг 3.4).

Листинг 3.4. Содержимое файла app.component.ts

72927.png 

Обратите внимание на использование скобок в тегах <a>. Квадратные скобки, окружающие routerLink, указывают на привязку свойств, а скобки справа представляют массив с одним элементом (например, ['/']). Мы покажем вам примеры массивов, содержащих два элемента и более, далее в этой главе. Второй якорный тег имеет свойство routerLink, привязанное к компоненту, сконфигурированному так, что на него указывает путь /product path. Соответствующие элементы будут отрисованы в области, помеченной тегом <router-outlet>, которая в данном приложении находится под якорными тегами.

Ни один компонент не знаком с конфигурацией маршрутизатора, поскольку за нее отвечает модуль. Объявим и загрузим корневой модуль. Для простоты реализуем оба эти действия в одном файле — main.ts (листинг 3.5).

Листинг 3.5. Содержимое файла main.ts

72937.png 

76101.png 

Свойство модуля providers представляет собой массив зарегистрированных провайдеров (в нашем примере показан только один) для внедрения зависимостей; эту тему мы рассмотрим в главе 4.

Сейчас же вам нужно знать лишь одно: несмотря на то, что стратегией навигации по умолчанию является PathLocationStrategy, Angular должен использовать для маршрутизации класс HashLocationStrategy (обратите внимание на символ решетки в URL на рис. 3.4).

примечание

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

Запуск примеров приложений из этой книги

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

Для запуска этого приложения с помощью кода, поставляемого вместе с данной книгой, убедитесь, что основной сценарий, выполняющий изначальную загрузку вашего корневого модуля, верно соотнесен в файле systemjs.config.js.

Например, именно так можно указать, что основной сценарий находится в файле main-param.ts:

packages: {

  'app': {main: 'main-param', defaultExtension: 'ts'}

}

Данное выражение верно и для других приложений этой и других глав.

Основной сценарий данного приложения находится в файле main.ts в каталоге samples. Для запуска приложения убедитесь, что в файле systemjs.config.js указан файл main.ts в пакете app, а затем запустите live-сервер из корневого каталога проекта.

SystemJS и динамическая компиляция

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

Но если приложение работает не так, как вы того ожидаете? Открыв панель Developer Tools (Инструменты разработчика) в своем браузере, вы увидите, что для каждого файла с расширением .ts имеется соответствующий скомпилированный файл с расширением .ts!transpiled. Он содержит скомпилированную версию кода, которая может быть полезна, если вам нужно увидеть реальный код JavaScript, запускающийся в браузере. На следующем рисунке показана панель Developer Tools (Инструменты разработчика) браузера Chrome, отображающая исходный код файла product.ts!transpiled.

pic03_01.tif 

примечание

В Angular имеется класс Location, позволяющий выполнять навигацию для абсолютного URL, вызывая методы go(), forward() и back() (а также некоторые другие методы). Этот класс следует применять только в том случае, если нужно взаимодействовать с URL, не затрагивая маршрутизатор Angular. Вы увидите пример использования класса Location в главе 9, где будете писать сценарии для модульного тестирования.

3.1.3. Навигация по маршрутам с помощью метода navigate()

В примере кода приложения basic_routing, показанного в предыдущем разделе, вы настроили навигацию с помощью свойств routerLink, расположенных в якорных тегах HTML. Но что если нужно настроить навигацию программно, не требуя от пользователя нажимать ссылки? Модифицируем этот пример кода так, чтобы навигация совершалась с использованием метода navigate(). Вы добавите кнопку, после нажатия которой тоже будет выполняться переход к компоненту ProductDetailComponent, но в этот раз мы не будем пользоваться якорями HTML.

В листинге 3.6 (файл main-navigate.ts) будет вызываться метод navigate() экземпляра класса Router, который будет внедрен в компонент RootComponent через конструктор. Для простоты мы поместим модули, объявление маршрутов, начальную загрузку и компонент AppComponent в один файл, но в реальных проектах следует держать их отдельно друг от друга, как мы делали это в предыдущих разделах.

Листинг 3.6. Содержимое файла main-navigate.ts

72959.png 

76153.png 

В данном примере для навигации по маршруту продукта применяется кнопка, но это можно сделать и программно, не требуя от пользователя совершения каких-либо действий. Просто при необходимости вызовите метод navigate() (или navigateByUrl()) из кода приложения. Вы увидите еще один пример использования этого API в главе 9, где мы объясним, как выполнять модульное тестирование маршрутизатора.

СОВЕТ

Наличие ссылки на экземпляр класса Router позволяет проверить, является ли заданный маршрут активным, путем вызова метода isRouteActive().

Обработка ошибок 404

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

Например, можно создать компонент с именем _404Component и сконфигурировать его так, чтобы он использовал универсальный путь **:

[

  {path: '',        component: HomeComponent},

  {path: 'product', component: ProductDetailComponent},

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

])

Теперь, когда маршрутизатор не сможет найти компонент, соответствующий заданному URL, он отрисует компонент _404Component. Вы можете посмотреть, как это работает, запустив приложение main-with-404.ts, которое поставляется с данной книгой. Просто введите в браузер несуществующий URL, например, .

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

3.2. Передача данных маршрутам

Простое приложение для маршрутизации показало, как можно отображать разные компоненты в заранее определенной области окна, но зачастую нужно не только отобразить элемент, но еще и передать ему какие-то данные. Например, при переходе от представления Home (Главная страница) к представлению Product Details (Информация о продукте) нужно передать идентификатор продукта компоненту, который представляет собой место назначения, например, ProductDetailComponent.

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

3.2.1. Извлечение параметров из объекта ActivatedRoute

Когда пользователь переходит по маршруту Product Details, вам нужно передать этому маршруту его идентификатор, чтобы отобразить подробную информацию об определенном продукте. Изменим код приложения из предыдущего раздела так, чтобы компонент RootComponent мог передавать идентификатор продукта компоненту ProductDetailComponent.

Новая версия этого компонента будет называться ProductDetailComponentParam, и Angular внедрит в него объект типа ActivatedRoute (листинг 3.7). Он будет содержать информацию о компоненте, загруженном в область отображения.

Листинг 3.7. Содержимое параметра ProductDetailComponentParam

72969.png 

Объект класса ActivatedRoute будет содержать все параметры, передаваемые компонентам. Вам лишь нужно объявить аргумент конструктора, указав его тип, и Angular узнает, как создавать и внедрять этот объект. Более подробно тему внедрения зависимостей мы рассмотрим в главе 4.

В листинге 3.8 вы измените конфигурацию маршрута product и routerLink для гарантии того, что значение идентификатора продукта будет передано в компонент ProductDetailComponentParam, если пользователь выберет пойти по этому маршруту. Новая версия приложения называется main-param.ts.

Листинг 3.8. Содержимое файла main-param.ts

72976.png 

Свойство routerLink ссылки Product Details инициализировано с помощью двухэлементного массива. Элементы массива являются составными частями пути, указанного в конфигурации маршрута, переданной в метод RouterModule.forRoot(). Первый элемент массива представляет собой статическую часть пути маршрута: product. Второй элемент представляет собой переменную часть пути: /:id.

Для простоты мы жестко закодируем значение идентификатора 1234, но если класс RootComponent имеет переменную productID, указывающую на соответствующий объект, то вы можете использовать конструкцию { productID} вместо значения 1234. Для маршрута Product Details (Информация о продукте) Angular создаст сегмент URL /product/1234. На рис. 3.5 показано, как представление Product Details (Информация о продукте) будет отрисовано в браузере. Обратите внимание на URL: маршрутизатор заменил путь product/:id на путь /product/1234.

72985.png 

Рис. 3.5. Маршрут Product Details получил идентификатор продукта 1234

Рассмотрим действия, которые выполнил Angular для отрисовки основной страницы приложения.

1. Проверил содержимое каждого элемента routerLink, чтобы найти соответствующие конфигурации маршрута.

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

3. Создал теги <a href="">, которые понимает браузер.

На рис. 3.6 показан снимок главной страницы и открытой панели Developer Tools (Инструменты разработчика) браузера Chrome. Поскольку свойство path сконфигурированного маршрута Home содержит пустую строку, Angular ничего не добавляет в основной URL страницы. Но якорь под ссылкой Product Details уже был сконвертирован в обычный тег HTML. Когда пользователь нажмет эту ссылку, маршрутизатор прикрепит к базовому URL символ решетки и конструкцию /product/1234. Абсолютный URL представления Product Details (Информация о продукте) будет выглядеть так: .

72992.png 

Рис. 3.6. Якорный тег для Product Details готов

3.2.2. Передача статических данных маршруту

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

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

{path: 'product/:id', component: ProductDetailComponentParam , data:

  [{isProd: true}]}

Свойство data может содержать массив произвольных пар «ключ — значение». Когда маршрутизатор открывает ProductDetailComponentParam, данные будут находиться в свойстве data ActivatedRoute.snapshot:

export class ProductDetailComponentParam {

  productID: string;

  isProdEnvironment: string;

  constructor(route: ActivatedRoute) {

    this.productID = route.snapshot.params['id'];

    this.isProdEnvironment = route.snapshot.data[0]['isProd'];

    console.log("this.isProdEnvironment = " + this.isProdEnvironment);

  }

}

Передача данных маршруту с помощью свойства data не является альтернативой конфигурированию параметров в свойстве path, как, например, здесь: 'product/:id'. Но такая функциональность может пригодиться, если вам нужно передать какие-то данные маршруту на этапе конфигурирования, например, информацию о том, в какой среде работает программа. Приложение, которое реализует эту функциональность, находится в файле main-param-data.ts.

3.3. Маршруты-потомки

Angular-приложение представляет собой дерево компонентов, связанных отношениями предок — потомок. Каждый компонент инкапсулирован, и вы сами решаете, какие элементы сделать доступными остальным сценариям приложения, а какие — закрытыми в рамках компонента. Любой элемент может иметь собственные стили, которые не будут смешиваться со стилями предка. Компонент также может иметь собственные инъекторы для зависимостей. Компонент-потомок может иметь собственные маршруты, но все маршруты конфигурируются за пределами элементов.

В предыдущем разделе вы сконфигурировали маршруты, чтобы показывать содержимое либо HomeComponent, либо ProductDetailComponent в router-outlet компонента AppComponent.

Допустим, вам нужно дать возможность ProductDetailComponent (потомку) показывать либо описание продукта, либо информацию о продавце. Это значит, что вам нужно добавить конфигурацию маршрутов-потомков в компонент ProductDetailComponent. Здесь следует использовать свойство children интерфейса Route:

[ {path: '',            component: HomeComponent},

  {path: 'product/:id', component: ProductDetailComponent,

    children: [

      {path: '', component: ProductDescriptionComponent},

      {path: 'seller/:id', component: SellerInfoComponent}

    ]}

]

На рис. 3.7 показано, как приложение будет выглядеть после того, как пользователь нажмет ссылку Product Details в корневом компоненте, который отрисует элемент ProductDetailComponent (потомка), показывая ProductDescription. Это маршрут по умолчанию для потомка, поскольку его свойство path содержит пустую строку.

ch03_07.tif 

Рис. 3.7. Маршрут Product Description

На рис. 3.8 показан внешний вид приложения после того, как пользователь нажмет ссылку Product Details, а затем ссылку Seller Info.

ch03_08.tif 

Рис. 3.8. Маршрут-потомок отрисовывает SellerInfo

примечание

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

Для реализации представлений, показанных на рис. 3.7 и 3.8, нужно модифицировать компонент ProductDetailComponent так, чтобы у него было два потомка, а также собственный <router-outlet>. На рис. 3.9 показана иерархия компонентов, которые вы реализуете.

73035.png 

Рис. 3.9. Иерархия маршрутов в приложении basic_routing

Весь код этой главы, включая маршруты-потомки, расположен в файле main-child.ts, показанном далее (листинг 3.9).

Листинг 3.9. Содержимое файла main-child.ts

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

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

import { BrowserModule } from '@angular/platform-browser';

import {LocationStrategy, HashLocationStrategy} from '@angular/common';

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

import {HomeComponent} from "./components/home";

import {ProductDetailComponent} from './components/product-child';

import {ProductDescriptionComponent} from './components/product-description';

import {SellerInfoComponent} from './components/seller';

 

const routes: Routes = [

    {path: '',            component: HomeComponent},

    {path: 'product/:id', component: ProductDetailComponent,

        children: [

          {path: '',           component: ProductDescriptionComponent},

          {path: 'seller/:id', component: SellerInfoComponent}

        ]}

];

@Component({

    selector: 'app',

    template: `

        <a [routerLink]="['/']">Home</a>

        <a [routerLink]="['/product', 1234]">Product Details</a>

        <router-outlet></router-outlet>

    `

})

class AppComponent {}

 

@NgModule({

    imports:      [ BrowserModule, RouterModule.forRoot(routes)],

    declarations: [ AppComponent, HomeComponent, ProductDetailComponent,

                    ProductDescriptionComponent, SellerInfoComponent],

    providers:[{provide: LocationStrategy, useClass: HashLocationStrategy}],

    bootstrap: [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Еще раз взглянем на URL, показанный на рис. 3.8. Когда пользователь нажимает ссылку, к URL добавляется сегмент product/1234. Маршрутизатор находит соответствие этому пути в объекте конфигурации и отрисовывает в области отображения компонент ProductDetailComponent.

Новая версия компонента ProductDetailComponent (product-child.ts) имеет собственную область отображения, где он может отобразить либо компонент ProductDescriptionComponent (по умолчанию), либо компонент SellerInfoCom­ponent (листинг 3.10).

Листинг 3.10. Новая версия компонента ProductDetailComponent

73045.png 

примечание

Элементы-потомки не нужно импортировать. Компоненты ProductDescrip­tionComponent и SellerInfoComponent не упоминаются явно в шаблоне компонента ProductDetailComponent, и вам не нужно указывать их в свойстве directives. Они включены в AppModule.

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

Когда пользователь нажимает ссылку Seller Info (Информация о продавце), URL будет содержать сегмент product/1234/seller/5678 (см. рис. 3.8). Маршрутизатор найдет соответствие в объекте конфигурации и отобразит компонент SellerInfoComponent.

примечание

Эта версия компонента ProductDetailComponent может переходить только по одной ссылке к информации о продавце. Для перехода от информации о продавце к маршруту /product пользователь может просто нажать кнопку Back (Назад) в своем браузере.

Компонент ProductDescriptionComponent выглядит тривиально (листинг 3.11).

Листинг 3.11. Содержимое компонента ProductDescriptionComponent

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

 

@Component({

    selector: 'product-description',

    template: '<p>This is a great product!</p>'

})

export class ProductDescriptionComponent {}

Поскольку компонент SellerInfoComponent ожидает получить идентификатор продавца, его конструктор должен иметь аргумент типа ActivatedRoute (листинг 3.12); вы уже делали это для компонента ProductDetailComponent.

Листинг 3.12. Содержимое компонента SellerInfoComponent

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

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

 

@Component({

    selector: 'seller',

    template: 'The seller of this product is Mary Lou (98%)',

    styles: [':host {background: yellow}']

})

export class SellerInfoComponent {

  sellerID: string;

  constructor(route: ActivatedRoute){

    this.sellerID = route.snapshot.params['id'];

    console.log(`The SellerInfoComponent got the seller id ${this.sellerID}`);

  }

}

Вы применяете псевдокласс :host, чтобы отобразить содержимое этого компонента на желтом фоне. Сейчас кратко рассмотрим Shadow DOM.

Селектор псевдокласса :host может быть использован вместе с элементами, созданными с помощью Shadow DOM, предоставляющим более качественную инкапсуляцию для компонентов (см. врезку «Поддержка Shadow DOM в Angu­lar» ниже). Несмотря на то, что еще не все браузеры поддерживают Shadow DOM, Angular эмулирует его по умолчанию и создает теневой корневой элемент. Элемент HTML, связанный с этим теневым корневым элементом, называется теневым хостом.

В листинге 3.12 вы используете :host для того, чтобы сделать желтый фон для компонента SellerInfoComponent, который служит теневым хостом. Стили элементов Shadow DOM не объединяются со стилями глобального DOM, а идентификаторы тегов HTML вашего компонента не пересекаются с идентификаторами DOM.

Глубокое связывание

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

URL указывает не просто на страницу Product Details, но на отдельное представление, содержащее информацию о продукте с идентификатором 1234.

URL указывает на еще более глубокую страницу. Он показывает информацию о продавце с идентификатором 5678, который продает продукт с идентификатором 1234.

Вы можете увидеть глубокое связывание в действии, скопировав ссылку из приложения, запущенного в браузере Chrome, и вставив ее в браузеры Firefox или Safari.

Поддержка Shadow DOM в Angular

Shadow DOM — часть стандарта Web Components. Каждая веб-страница представлена деревом объектов DOM, но Shadow DOM позволяет инкапсулировать поддерево элементов HTML для того, чтобы создать границы между компонентами. Такое поддерево отрисовывается как часть документа HTML, но его элементы не прикреплены к основному дереву DOM. Другими словами, Shadow DOM размещает стену между содержимым DOM и внутренностями компонента HTML.

В том случае когда вы добавляете пользовательский тег на веб-страницу, он содержит фрагмент HTML, а благодаря Shadow DOM этот фрагмент виден только в рамках одного компонента, он не объединяется с DOM веб-страницы. С помощью Shadow DOM стили CSS пользовательского компонента не объединяются с основным CSS модели DOM, что предотвращает возможные конфликты при отрисовке стилей.

Откройте любой видеоролик на YouTube в браузере Chrome, нативно поддерживающем Shadow DOM. На момент написания этой книги видеопроигрыватель представлен тегом video, который вы можете найти, открыв панель Developer Tools (Инструменты разработчика) и взглянув на содержимое вкладки Elements (Элементы), как показано на следующем рисунке.

pic03_02.tif 

Несмотря на то, что видеопроигрыватель состоит из области содержимого и панели инструментов, включающей десяток кнопок (кнопка Play, ползунок звука и т.д.), все они инкапсулированы внутри теневого корневого элемента. С точки зрения основного DOM, эта страница содержит «деталь Lego» <video>. Чтобы заглянуть внутрь данного тега, нужно выбрать действие Show User Agent Shadow DOM в настройках панели Developer Tools (Инструменты разработчика).

В компонентах Angular вы указываете разметку HTML в свойствах template или templateUrl аннотации @Component. Если браузер нативно поддерживает Shadow DOM или вы указали, что Angular должен эмулировать его, код HTML-компонента не объединяется с глобальным объектом DOM веб-страницы. В Angular можно дать команду использовать режим Shadow DOM, установив свойству encapsulation аннотации @Component одно из следующих значений:

ViewEncapsulation.Emulated — эмулирует инкапсуляцию Shadow DOM (значение по умолчанию). Указывает Angular сгенерировать уникальные атрибуты для стилей элемента и не объединять его стили со стилями модели DOM веб-страницы. Например, если вы откроете панель Developer Tools (Инструменты разработчика) в браузере Chrome при навигации по компоненту SellerInfoComponent, то разметка HTML этого компонента будет выглядеть так:

<head>

...

  <style>[_nghost-yls-7] {background: yellow;}</style>

</head>

...

<seller _nghost-yls-7="" _ngcontent-yls-6="">

  <p _ngcontent-yls-7=""></p>

  The seller of this product is Mary Lou (98% positive feedback)

</seller>

ViewEncapsulation.Native — использует Shadow DOM, который нативно поддерживается браузером. HTML и стили не объединяются с моделью DOM веб-страницы.

Этот вариант следует применять только в том случае, если вы уверены, что браузер пользователя поддерживает Shadow DOM; в противном случае генерируется ошибка. В таком режиме стили компонента SellerInfoComponent не будут добавлены в раздел <head> вашей страницы, но все стили компонента и его предков будут инкапсулированы внутри компонента.

pic03_03.tif 

ViewEncapsulation.None — не использует инкапсуляцию Shadow DOM. Вся разметка и стили будут интегрированы в глобальную модель DOM веб-страницы. Селектор :host не станет работать в этом режиме, поскольку не будет никакого теневого хоста. Вы все еще можете задать стиль для компонента SellerInfoComponent, ссылаясь на него с помощью его селектора:

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

 

@Component({

    selector: 'seller',

    template: 'The seller of this product is Mary Lou (98%)',

    styles: ['seller {background: yellow}'],

    encapsulation: ViewEncapsulation.None

})

export class SellerInfoComponent {}

Angular не будет генерировать дополнительные атрибуты стиля и добавит следующую строку в раздел <head> вашей страницы:

<style>seller {background: yellow}</style>

В подразделе 6.2.3 вы увидите, как ViewEncapsulation влияет на отрисовку пользовательского интерфейса как с помощью Shadow DOM, так и без него.

3.4. Граничные маршруты

Теперь, когда вы умеете настраивать базовую навигацию с помощью маршрути­затора, рассмотрим несколько сценариев, которые требуют выполнять валидацию, чтобы решить, может ли пользователь (или программа) перейти по маршруту:

открытие маршрута только в том случае, если пользователь аутентифицирован и авторизован;

• отображение составной формы, которая состоит из нескольких компонентов, и пользователю можно переходить на следующий раздел формы только в том случае, если данные, введенные в этом разделе, корректны;

• напоминание пользователю о несохраненных изменениях, если он пробует уйти с маршрута.

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

примечание

Angular содержит несколько привязок жизненного цикла компонентов, позволяющих обрабатывать важные события в жизни элемента. Мы рассмотрим их в главе 6.

В разделе 3.1 мы говорили, что тип Routes представляет собой массив элементов, который соответствует интерфейсу Route, показанному ниже:

export interface Route {

    path?: string;

    pathMatch?: string;

    component?: Type | string;

    redirectTo?: string;

    outlet?: string;

    canActivate?: any[];

    canActivateChild?: any[];

    canDeactivate?: any[];

    canLoad?: any[];

    data?: Data;

    resolve?: ResolveData;

    children?: Route[];

    loadChildren?: string;

}

При конфигурации предыдущих маршрутов вы использовали три свойства из этого интерфейса: path, component и data. Теперь познакомимся со свойствами canActivate и canDeactivate, которые позволяют связывать маршруты и граничные операторы (охранников). По сути, вам нужно написать функцию для реализации логики проверки, которая вернет значение true или false, и присвоить ее одному из указанных свойств. Если вызов canActivate() граничного оператора вернет значение true, то пользователь сможет перейти по данному маршруту. Если вызов canDeactivate() вернет значение true, то пользователь сможет уйти с этого маршрута. Поскольку свойства canActivate и canDeactivate типа Route принимают в качестве своего значения массив, можно присвоить несколько функций (граничных операторов), когда нужно проверить более одного условия для того, чтобы разрешить или запретить навигацию по маршруту.

Обновим пример из подраздела 3.1.2 (имеющий ссылки Home и Product Details), для демонстрации того, как можно предохранить маршрут product от неавторизованных пользователей. Чтобы этот пример оставался простым, вы не будете использовать настоящий сервис авторизации, статус авторизации будет генерироваться случайным образом.

Создайте граничный класс, реализующий интерфейс CanActivate, в котором объявлена всего одна функция: canActivate() (листинг 3.13). Она должна содержать логику приложения, которая возвращает значение true или false. Если функция возвращает значение false (пользователь не авторизован), то приложение не перейдет по маршруту и выведет сообщение об ошибке в консоли.

Листинг 3.13. Содержимое класса LoginGuard

import {CanActivate} from "@angular/router";

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

 

@Injectable()

export class LoginGuard implements CanActivate{

  canActivate() {

      return this.checkIfLoggedIn();

  }

private checkIfLoggedIn(): boolean{

      // Здесь будет находиться вызов сервиса авторизации

      // Сейчас же мы будем возвращать значение true или false,

      // выбранное случайным образом

      let loggedIn:boolean = Math.random() <0.5;

      if(!loggedIn){

          console.log("LoginGuard:

            The user is not logged in and can't navigate product details");

      }

      return loggedIn;

  }

}

Как видите, эта реализация функции canActivate() будет случайным образом возвращать значение true или false, эмулируя авторизацию пользователя.

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

[

  {path: '',        component: HomeComponent},

  {path: 'product', component: ProductDetailComponent, canActivate:

    [LoginGuard]}

]

Добавление одного или нескольких граничных операторов в массив, передаваемый свойству canActivate, автоматически вызовет всех операторов одного за другим. Если хотя бы один из них вернет значение false, то переход по маршруту будет запрещен.

Но кто же будет создавать объект класса LoginGuard? Angular сделает это за вас с помощью механизма внедрения зависимостей (он описан в главе 4), но вам нужно упомянуть этот класс в списке поставщиков, которые нужны для того, чтобы внедрение сработало. Добавьте имя LoginGuard к списку поставщиков в @NgModule:

@NgModule({

    imports:      [ BrowserModule, RouterModule.forRoot(routes)],

    declarations: [ AppComponent, HomeComponent, ProductDetailComponent],

    providers:[{provide: LocationStrategy, useClass: HashLocationStrategy}

                LoginGuard],

    bootstrap:    [ AppComponent ]

})

Ниже представлен полный код основного сценария приложения (файл main-with-guard.ts) (листинг 3.14).

Листинг 3.14. Содержимое файла main-with-guard.ts

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

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

import { BrowserModule } from '@angular/platform-browser';

import {LocationStrategy, HashLocationStrategy} from '@angular/common';

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

import {HomeComponent} from "./components/home";

import {ProductDetailComponent} from "./components/product";

import {LoginGuard} from "./guards/login.guard";

 

const routes: Routes = [

    {path: '',        component: HomeComponent},

    {path: 'product', component: ProductDetailComponent,

        canActivate:[LoginGuard]}

];

 

@Component({

    selector: 'app',

    template: `

        <a [routerLink]="['/']">Home</a>

        <a [routerLink]="['/product']">Product Details</a>

        <router-outlet></router-outlet>

    `

})

class AppComponent {}

 

@NgModule({

    imports:      [ BrowserModule, RouterModule.forRoot(routes)],

    declarations: [ AppComponent, HomeComponent, ProductDetailComponent],

    providers:[{provide: LocationStrategy, useClass: HashLocationStrategy},

                LoginGuard],

    bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Если вы запустите это приложение и нажмете ссылку Product Details, то приложение либо перейдет по данному маршруту, либо выведет сообщение об ошибке в консоли браузера в зависимости от сгенерированного случайным образом значения в LoginGuard. На рис. 3.10 показан снимок экрана, сделанный после того, как пользователь нажал ссылку Product Details, но граничный оператор LoginGuard решил, что тот не авторизован.

ch03_10.tif 

Рис. 3.10. Переход по ссылке Product Details охраняется

В данном примере вы реализовали метод canActivate(), не предоставляя ему никаких аргументов. Но этот метод можно использовать со следующей сигнатурой:

canActivate(destination: ActivatedRouteSnapshot, state: RouterStateSnapshot)

Значения ActivatedRouteSnapshot и RouterStateSnapshot Angular внедрит автоматически, и это может оказаться полезным, если вы хотите проанализировать текущее состояние маршрутизатора. Например, чтобы узнать имя маршрута, по которому пользователь пробует перейти, можно сделать так:

canActivate(destination: ActivatedRouteSnapshot, state: RouterStateSnapshot)

    {

    console.log(destination.component.name);

    ...

}

СОВЕТ

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

Реализация интерфейса CanDeactivate, который управляет процессом навигации по маршруту, выполняется аналогичным образом. Просто создайте граничный класс, реализующий метод canDeactivate(), например, так:

import {CanDeactivate, Router} from "@angular/router";

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

 

@Injectable()

export class UnsavedChangesGuard implements CanDeactivate{

    constructor(private _router:Router){}

    canDeactivate(){

        return window.confirm("You have unsaved changes.

          Still want to leave?");

    }

}

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

@NgModule({

    imports:      [ BrowserModule, RouterModule.forRoot(routes)],

    declarations: [ AppComponent, HomeComponent, ProductDetailComponent],

    providers:[{provide: LocationStrategy, useClass: HashLocationStrategy},

                LoginGuard, UnsavedChangesGuard],

    bootstrap:    [ AppComponent ]

})

СОВЕТ

Более красивое отображение оповещения и диалогов подтверждения возможно с помощью компонента MdDialog, поставляющегося в библиотеке Material Design 2 ().

Для получения более подробной информации о привязках жизненного цикла, применимых к навигации, обратитесь к разделу @angular/router документации к Angular API (). Мы рассмотрим жизненные циклы компонентов в главе 6.

3.5. Создание одностраничного приложения с несколькими областями отображения

Из предыдущего раздела вы узнали, что маршрут-потомок представлен URL, состоящим из сегментов-предков и сегментов-потомков. Ваше одностраничное приложение имеет один тег, <router-outlet>, где Angular отрисует компонент, сконфигурированный для предка или потомка. Теперь обсудим конфигурирование и отрисовку маршрутов одного уровня, что означает их отрисовку в отдельных областях отображения в одно и то же время. Рассмотрим несколько вариантов использования.

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

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

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

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

Чтобы отделить отрисовку компонентов основных и вспомогательных маршрутов, нужно добавить еще один тег <router-outlet>, но эта область отображения должна иметь имя. Например, в следующем фрагменте кода определены основной outlet и outlet для чата:

<router-outlet></router-outlet>

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

Добавим именованный маршрут для чата в наше приложение. На рис. 3.11 показаны два маршрута, открытые одновременно после того, как пользователь нажал ссылку Home, а затем и ссылку Open Chat. Слева показан отрисованный компонент HomeComponent в основной области отображения, а справа — ChatComponent, отрисованный в именованной области отображения. Нажатие ссылки Close Chat очистит содержимое именованной области отображения. (Мы добавили поля HTML <input> в компонент HomeComponent и <textarea> в компонент ChatComponent, чтобы было проще заметить, какой элемент имеет фокус, когда пользователь переключается между маршрутами Home и Chat.)

ch03_11.tif 

Рис. 3.11. Отрисовка представления чата с помощью вспомогательного маршрута

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

Код, реализующий данный фрагмент, располагается в файле main_aux.ts, он показан в листинге 3.15. Мы разместили все требуемые компоненты в одном файле для простоты. Компоненты HomeComponent и ChatComponent имеют встроенные стили, которые позволяют разместить их рядом друг с другом в окне. Компонент HomeComponent займет 70 % доступного пространства, а ChatComponent — остальные 30 %.

Листинг 3.15. Содержимое файла main_aux.ts

73103.png 

76243.png 

примечание

Поскольку объявления классов не подняты («Поднятие» объясняется в приложении A), убедитесь, что объявляете компоненты до того, как используете их в routerLink.

Если хотите выполнить навигацию именованных областей отображения (или закрыть их) программно, используйте метод Router.navigate(), описанный в подразделе 3.1.3. Рассмотрим пример:

navigate([{outlets: {aux: 'chat'}}]);

Немного передохнем и вспомним, что мы уже узнали из этой главы:

маршруты конфигурируются на уровне модулей;

• каждый маршрут имеет путь, соотнесенный с компонентом;

• область, где отрисовывается содержимое маршрута, определена расположением области <router-outlet> в шаблоне компонента;

• routerLink может быть использован при навигации по именованному маршруту;

• метод navigate() может применятся для навигации по именованному маршруту;

• если для маршрута требуется параметр, то нужно сконфигурировать его в свойстве path в конфигурации маршрута и передать его значение в routerLink или метод navigate();

• если маршрут принимает параметр, то лежащий в его основе компонент должен иметь конструктор с аргументом типа ActivatedRoute;

• если компонент-потомок имеет собственную конфигурацию маршрутов, то он называется маршрутом-потомком и конфигурируется с помощью свойства children, определенного в интерфейсе Route;

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

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

3.6. Разбиение приложения на модули

Angular позволяет разбить приложение на несколько модулей, где каждый из них будет реализовывать определенную функциональность. В действительности каждый фрагмент кода, показанный в данной главе, уже содержит более одного модуля, например, AppModule, BrowserModule и RouterModule. Модуль AppModule является корневым модулем приложения, а BrowserModule и RouterModule — это модули функциональности. Обратите внимание на главное различие между ними: корневой модуль загружается предварительно, а модули функциональности импортируются, как показано в следующем фрагменте кода:

@NgModule({

    imports:[ BrowserModule,

              RouterModule.forRoot(routes)],

    ...

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Каждый модуль может предоставлять и скрывать определенную функциональность, все модули выполняются в одном контексте, так что могут иметь общие объекты. Модули RootModule и BrowserModule были созданы командой разработчиков Angular, но вы тоже можете разбить свое приложение на модули.

Для модулей функциональности декоратор @NgModule нужен для импортирования модуля CommonModule вместо BrowserModule. Возьмем приложение с двумя ссылками — Home и Product Details — и добавим еще одну: Luxury Items. Представьте, что предметы роскоши должны обрабатываться не так, как обычные продукты, и вам нужно вынести эту функциональность в отдельный модуль, который называется LuxuryModule и включает всего один компонент — LuxuryComponent. Модули функциональности и поддерживаемые ими элементы, сервисы и другие ресурсы рекомендуется размещать в отдельном каталоге. В нашем примере они будут находиться в каталоге с именем luxury.

Код модуля LuxuryModule находится в файле luxury.module.ts, показанном далее (листинг 3.16).

Листинг 3.16. Содержимое файла luxury.model.ts

73113.png 

Когда вы конфигурируете корневой модуль, используйте метод forRoot, для модулей функциональности применяйте метод forChild().

Код компонента LuxuryComponent всего лишь отобразит текст Luxury Component на желтом (под цвет золота) фоне:

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

 

@Component({

    selector: 'luxury',

    template: `<h1 class="gold">Luxury Component</h1>`,

    styles: ['.gold {background: yellow}']

})

export class LuxuryComponent {}

Обратите внимание: вы экспортируете компонент LuxuryComponent для того, чтобы он стал доступным остальным членам корневого модуля. Код AppComponent, AppModule и функции предварительной загрузки находятся в файле main-luxury.ts (листинг 3.17).

Листинг 3.17. Содержимое файла main-luxury.ts

73123.png 

76248.png 

Обратите внимание на то, что корневой модуль не знает о содержимом модуля LuxuryModule и даже не упоминает LuxuryComponent.

ch03_12.tif 

Рис. 3.12. Отрисовка модуля LuxuryModule

Когда маршрутизатор анализирует конфигурацию маршрутов для корневых модулей и модулей функциональности, он корректно соотнесет путь luxury с компонентом LuxuryComponent, который был экспортирован модулем LuxuryModule. После запуска этого приложения и нажатия ссылки Luxury Items (Предметы роскоши) вы увидите окно, показанное на рис. 3.12.

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

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

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

3.7. «Ленивая» загрузка модулей

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

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

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

В листинге 3.18 реализована ленивая загрузка модуля. Этот фрагмент выглядит практически так же, как и листинг 3.17, но мы внесем небольшое изменение в основной модуль, а также изменим способ экспортирования модуля LuxuryModule. Данный код находится в файле main-luxury-lazy.ts.

Листинг 3.18. Содержимое файла main-luxury-lazy.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

import { BrowserModule } from '@angular/platform-browser';

import {LocationStrategy, HashLocationStrategy} from '@angular/common';

import {RouterModule} from "@angular/router";

import {HomeComponent} from "./components/home";

import {ProductDetailComponent} from "./components/product";

 

@Component({

    selector: 'app',

    template: `

        <a [routerLink]="['/']">Home</a>

        <a [routerLink]="['/product']">Product Details</a>

        <a [routerLink]="['/luxury']">Luxury Items</a>

        <router-outlet></router-outlet>

    `

})

export class AppComponent {}

 

@NgModule({

    imports: [ BrowserModule,

               RouterModule.forRoot([

                   {path: '', component: HomeComponent},

                   {path: 'product', component: ProductDetailComponent},

                   {path: 'luxury', loadChildren:

                     'app/components/luxury/luxury.lazy.module'}

                   ])

             ],

    declarations: [ AppComponent, HomeComponent, ProductDetailComponent],

    providers:[{provide: LocationStrategy, useClass: HashLocationStrategy}],

    bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

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

{path: 'luxury', loadChildren: 'app/components/luxury/luxury.lazy.module'}

Вместо того чтобы соотносить путь с компонентом, вы используете свойство loadChildren, предоставляя путь загружаемому модулю. Обратите внимание: значением loadChildren является не тип модуля, а строка. Корневой модуль не знает типа LuxuryModule, но, когда пользователь нажмет ссылку Luxury Items (Предметы роскоши), загрузчик модулей проанализирует эту строку и загрузит модуль LuxuryModule из файла luxury.lazy.module.ts (листинг 3.19), внешний вид которого отличается от версии, показанной в предыдущем разделе.

Листинг 3.19. Содержимое файла luxury.lazy.module.ts

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

import { CommonModule } from '@angular/common';

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

import {LuxuryComponent} from "./luxury.component";

 

@NgModule({

    imports:      [ CommonModule,

        RouterModule.forChild([

        {path: '', component: LuxuryComponent}

    ]) ],

    declarations: [ LuxuryComponent ]

})

export default class LuxuryModule { }

Здесь вы указываете, что пустой путь будет использоваться как маршрут по умолчанию. Поскольку этот модуль будет загружаться лениво и вы не объявляли тип LuxuryModule в корневом модуле, придется применить ключевое слово default при экспорте данного класса. Когда пользователь нажмет ссылку Luxury Items (Предметы роскоши) в корневом модуле, загрузчик загрузит содержимое файла luxury.lazy.module.ts и определит, что модуль LuxuryModule является точкой входа по умолчанию для сценария из этого файла.

Теперь, если вы запустите приложение main-luxury-lazy при открытой вкладке Network (Сеть) панели Developer Tools (Инструменты разработчика), то не увидите модуль luxury в списке загруженных файлов. Нажмите ссылку Luxury Items (Предметы роскоши), и вы увидите, как браузер делает дополнительный запрос на сервер для загрузки модуля LuxuryModule и компонента LuxuryComponent.

Для нашего крайне простого примера эти манипуляции снизили размер загружаемых данных всего на 1 Кбайт. Но при создании крупных приложений использование ленивой загрузки может снизить размер загружаемых данных на несколько сотен килобайтов (или даже больше), улучшая субъективную производительность приложения. Субъективная производительность — то, как пользователь воспринимает производительность приложения; улучшать данный параметр очень важно, особенно если приложение загружается с мобильного устройства, работающего в медленной сети.

3.8. Практикум: добавление навигации в онлайн-аукцион

Упражнение начинается там, где мы остановились в предыдущей главе. К данному моменту вы создали главную страницу аукциона (см. рис. 2.3). Цель проекта заключается в добавлении навигации в приложение, чтобы пользователь мог выбрать название продукта; это вызовет замену представления, показывающего карусель и миниатюры товаров, на представление ProductItemComponent.

В настоящей главе вы не увидите финальную версию представления Product Details (Информация о продукте). Несмотря на то, что код, показанный в главе 2, имеет класс ProductService, содержащий всю подробную информацию о продукте, мы используем его в главе 4 для иллюстрации внедрения зависимостей. На рис. 3.13 показано, как онлайн-аукцион будет выглядеть в этой главе после того, как пользователь нажмет ссылку First Product на главной странице.

ch03_13.tif 

Рис. 3.13. Навигация по маршруту Product Details

В данном разделе нужно сделать следующие шаги.

1. Создать компонент ProductDetailComponent, который отображает только заголовок продукта.

2. Переписать код так, чтобы в компоненте HomeComponent появились карусель и сетка с элементами, представляющими продукты.

3. Сконфигурировать маршрут для пути products, принимающего в качестве аргумента название продукта. Этот маршрут должен переносить пользователя к компоненту ProductDetailComponent, который будет принимать в качестве аргумента название продукта с помощью объекта ActivatedRoute.

4. Изменить код компонента ApplicationComponent так, чтобы тот отрисовывал либо HomeComponent, либо ProductDetailComponent в зависимости от выбранного маршрута.

5. Добавить тег <route-outlet> в основное приложение для отрисовки HomeCompo­nent и ProductDetailComponent.

6. Добавить ссылку, содержащую [routerLink], в шаблон ProductItemComponent, чтобы в момент, когда пользователь нажимает название продукта, приложение переходило по маршруту Product Details.

примечание

Если хотите увидеть только финальную версию этого проекта, то откройте каталог auction в командной строке и запустите команды npm install и npm start. Можно также скопировать каталог auction из главы 2 в другое место и следовать инструкциям, приведенным в следующих подразделах.

3.8.1. Создание ProductDetailComponent

Создайте новый каталог app/components/product-detail и добавьте в него файл product-detail.ts со следующим содержимым (листинг 3.20).

Листинг 3.20. Содержимое файла product-detail.ts

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

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

 

@Component({

  selector: 'auction-product-page',

  template: `

    <div>

      <img src="">

      <h4>{{productTitle}}</h4>

    </div>

  `

})

export default class ProductDetailComponent {

  productTitle: string;

  constructor(route: ActivatedRoute){

    this.productTitle = route.snapshot.params['prodTitle'];

  }

}

3.8.2. Создание HomeComponent и рефакторинг кода

В главе 2 вы создали главную страницу аукциона, на которой расположились несколько элементов. Нужно переписать код так, чтобы для новой версии главной страницы использовалась маршрутизация. Вы определите область с тегом <router-outlet>, где будете отображать либо компонент HomeComponent, либо компонент ProductDetailComponent. Первый инкапсулирует существующий компонент CarouselComponent и сетку, содержащую компонент ProductItemComponent. Это делается путем выполнения следующих шагов.

1. Создайте новый каталог app/components/home и добавьте в него файл home.ts с таким содержимым (листинг 3.21).

Листинг 3.21. Содержимое файла home.ts

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

 

@Component({

  selector: 'auction-home-page',

  styleUrls: ['/home.css'],

  template: `

    <div class="row carousel-holder">

      <div class="col-md-12">

        <auction-carousel></auction-carousel>

      </div>

    </div>

    <div class="row">

      <div *ngFor="let product of products"

        class="col-sm-4 col-lg-4 col-md-4">

        <auction-product-item [product]="product">

          </auction-product-item>

      </div>

    </div>

  `

})

export default class HomeComponent {

  products: Product[] = [];

  constructor(private productService: ProductService) {

    this.products = this.productService.getProducts();

  }

}

Angular внедряет в данный компонент ProductService, поставщик для этого сервиса объявлен в модуле AppModule. О поставщиках вы узнаете из следующей главы.

В главе 2 предыдущий код располагался в файле application.ts file. Но вам нужно инкапсулировать этот код внутри компонента HomeComponent, а также сконфигурировать маршрут для него в модуле AppModule. На следующем шаге вы удалите соответствующий код из файла application.ts.

Если видите стили, которые не определены в коде явно, то имейте в виду, что они берутся из CSS, поставляемом вместе с библиотекой Bootstrap. Можете изменять их по мере необходимости.

2. Создайте файл home.css, чтобы указать стили для компонента карусели из Bootstrap внутри HomeComponent (листинг 3.22).

Листинг 3.22. Содержимое файла home.css

.slide-image {

    width: 100%;

}

.carousel-holder {

    margin-bottom: 30px;

}

.carousel-control,.item {

    border-radius: 4px;

}

3.8.3. Упрощаем компонент ApplicationComponent

Теперь, когда вы инкапсулировали большой фрагмент кода внутри элемента HomeComponent, код компонента ApplicationComponent станет короче:

1. Замените содержимое файла application.ts на следующий код (листинг 3.23).

Листинг 3.23. Содержимое файла application.ts

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

 

@Component({

  selector: 'auction-application',

  templateUrl: 'app/components/application/application.html',

  styleUrls: ['app/components/application/application.css'],

  encapsulation:ViewEncapsulation.None

})

export default class ApplicationComponent {}

В свойствах templateUrl и styleUrls вы будете применять полный путь к файлам HTML и CSS. В главе 10 из вставки «Относительные пути в шаблонах при использовании SystemJS» вы узнаете, как задействовать относительные пути при указании файлов HTML и CSS. В рамках книги гораздо проще описать более короткие фрагменты кода, поэтому вы разместите разметку компонента ApplicationComponent в отдельном файле application.html.

2. Измените содержимое файла application.html, чтобы он выглядел следующим образом (листинг 3.24).

Листинг 3.24. Обновленный файл application.html

<auction-navbar></auction-navbar>

<div class="container">

  <div class="row">

    <div class="col-md-3">

      <auction-search></auction-search>

    </div>

    <div class="col-md-9">

      <router-outlet></router-outlet>

    </div>

  </div>

</div>

<auction-footer></auction-footer>

Основное изменение заключается в том, что элементы карусели и элементов продуктов заменены на тег <router-outlet>. Когда маршрутизатор отрисует компонент HomeComponent, будут отрисованы также карусель и элементы продуктов.

В верхней части окна аукционов располагается панель навигации, в нижней — нижний колонтитул, а область посередине разбита надвое: компонент поиска и область отображения маршрутизатора. В соответствии с системой таблицы Bootstrap, вся ширина окна делится на 12 равных колонок, три из них выделяются <auction-search>, а еще девять — <router-outlet>. Другими словами, 25 % ширины экрана выделяется для поиска, а 75 % — для маршрутов. Вы реализуете функциональность поиска в главе 7, в которой рассматривается работа с формами.

3.8.4. Добавление RouterLink в компонент ProductItemComponent

Компонент HomeComponent содержит несколько экземпляров компонента Pro­ductItemComponent. Каждый из них должен иметь routerLink, чтобы вы могли переходить к компоненту ProductDetailComponent, передавая в качестве параметра название продукта. Сделайте следующее.

1. Измените код файла product-item.ts для ссылки на файл CSS, как показано здесь (листинг 3.25).

Листинг 3.25. Обновленный файл product-item.ts

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

import {Product} from '../../services/product-service';

 

@Component({

  selector: 'auction-product-item',

  styleUrls: ['app/components/product-item/product-item.css'],

  templateUrl: 'app/components/product-item/product-item.html',

})

export default class ProductItemComponent {

  @Input() product: Product;

}

Для файла product-item.html требуется якорный тег без директивы routerLink, что позволит выполнять навигацию по маршруту, соотнесенному с путем products/:prodTitle. Вы сконфигурируете его в модуле AppModule несколько позже.

2. Измените содержимое файла product-item.html, чтобы он выглядел вот так (листинг 3.26).

Листинг 3.26. Обновленный файл product-item.html

<div class="thumbnail">

  <img src="">

  <div class="caption">

    <h4 class="pull-right">{{ product.price | currency }}</h4>

    <h4><a [routerLink]="['/

        products', product.title]">{{ product.title }}</a></h4>

    <p>{{ product.description }}</p>

  </div>

  <div class="ratings">

    <auction-stars [rating]="product.rating"></auction-stars>

  </div>

</div>

При форматировании цены продукта вы используете канал currency (канал указывается после символа вертикальной полосы). Если данный канал не ра­ботает в вашем браузере, то в разделе 5.3 вы сможете найти способ обойти эту проблему.

3. Создайте файл product-item.css, имеющий следующее содержимое (листинг 3.27).

Листинг 3.27. Содержимое файла product-item.css

.caption {

  height: 130px;

  overflow: hidden;

}

.caption h4 { white-space: nowrap;}

.thumbnail { padding: 0;}

.thumbnail img { width: 100%;}

.thumbnail .caption-full {

  padding: 9px;

  color: #333;

}

.ratings {

  color: #d17581;

  padding-left: 10px;

  padding-right: 10px;

}

3.8.5. Изменение корневого модуля с целью добавления маршрутизации

Наконец, нужно обновить файл app.module.ts, добавив модуль RouterModule и стратегию расположения, а также сконфигурировать маршруты (листинг 3.28).

Листинг 3.28. Обновленный файл app.module.ts

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

import { BrowserModule } from '@angular/platform-browser';

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

import {LocationStrategy, HashLocationStrategy} from '@angular/common';

import ApplicationComponent from './components/application/application';

import CarouselComponent from "./components/carousel/carousel";

import FooterComponent from "./components/footer/footer";

import NavbarComponent from "./components/navbar/navbar";

import ProductItemComponent from "./components/product-item/product-item";

import SearchComponent from "./components/search/search";

import StarsComponent from "./components/stars/stars";

import {ProductService} from "./services/product-service";

import HomeComponent from "./components/home/home";

import ProductDetailComponent from "./components/product-detail/

  product-detail";

 

@NgModule({

    imports:      [ BrowserModule,

                    RouterModule.forRoot([

                        {path: '',                     component:

                          HomeComponent},

                        {path: 'products/:prodTitle',

                          component: ProductDetailComponent}

    ]) ],

    declarations: [ ApplicationComponent,CarouselComponent,

                    FooterComponent, NavbarComponent,

                    HomeComponent, ProductDetailComponent,

                    ProductItemComponent,SearchComponent,StarsComponent],

    providers:    [ProductService,

                    {provide: LocationStrategy, useClass:

                      HashLocationStrategy}],

    bootstrap:    [ ApplicationComponent ]

})

export class AppModule { }

Здесь вы конфигурируете два маршрута: базовый URL (пустой путь) будет указывать на компонент HomeComponent, а путь products/:prodTitle нужен для отрисовки компонента ProductDetailComponent, получающего в качестве параметра название продукта. Значение prodTitle будет предоставлено компонентом ProductItemComponent, в котором был определен routerLink.

3.8.6. Запуск аукциона

Переключитесь в каталог auction в командной строке и запустите сервер, введя команду npm start (сценарий запуска сконфигурирован в файле package.json, как показано в главе 2). Браузер откроет главную страницу аукциона, которая будет выглядеть аналогично изображению в главе 2. Теперь нажмите название любого из продуктов, вы должны увидеть упрощенную страницу Product Details, показанную на рис. 3.13. В главе 4 вы внедрите дополнительную информацию о продуктах в это представление.

Применение оператора расширения

По мере роста приложения количество компонентов, объявленных вами в модуле AppModule, может сделать код менее читаемым. Использование оператора расширения ES6 (рассматривается в приложении A) может помочь в этой ситуации. Создайте отдельный файл, в котором вы перечислите все свои компоненты, например, так:

export const myComponents = [

    ApplicationComponent,

    CarouselComponent,

    FooterComponent,

    NavbarComponent,

    HomeComponent,

    ProductDetailComponent,

    ProductItemComponent,

    SearchComponent,

    StarsComponent];

Далее декоратор @NgModule сможет применить оператор расширения следующим образом:

@NgModule({

    // здесь будет другой код

    declarations: [ ...myComponents],

    // здесь будет другой код

})

3.9. Резюме

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

Конфигурируйте маршруты для вашего приложения с помощью RouterModule.

• Выбирая стратегию расположения, вы можете управлять тем, как выглядит URL для каждого представления.

• При навигации по приложению маршрутизатор отрисовывает лежащий в основе маршрута компонент в области содержимого, определенной тегами <router-outlet>. У вас может быть больше одной такой области.

• Чтобы выполнить навигацию по маршруту, добавьте в приложение якорные теги. Они должны использовать свойство routerLink вместо атрибута href. После этого вы сможете передавать параметры в маршрут.

• Для уменьшения размера приложения проверьте, можно ли некоторые модули загружать отдельно по требованию, реализовав ленивую загрузку.

Назад: 2. Приступаем к работе с Angular
Дальше: 4. Внедрение зависимостей

32
32
Alex
32
fagmefs
kamagra 100 mg on line
lasix other names
Amoxicillin 250 5ml
dragzolotoru
Ювелирные изделия, те что покупатели имеем или приобретаем именно на подарок родным несут внутри себе большое число увлекательных данных, которые реально просто изучить, когда Вы нажмет на источник новых публикаций касательно драгоценных изделий виды плетение цепочек. Интернет страничка золотых украшений ознакомит любителей из спектр полезными умений, с пособием их Вы могут хорошо ориентироваться у высококачества металлических плюс популярных изделий, к тому же дорогих породах камней плюс металлах. Онлайн сайт всякой проверенных данных о украшения ежедневно увеличивает сведения, те что смогут Вам вернее разбираться какие именно изделия совмещаются в стиле, как же требуется чистить про ювелирными предметами, и что сегодня актуально. Переходите на сайт, читайте также лайкайте подходящие вам новости или же делитесь ссылки на странички соц. сети, здесь на сайте мы будем систематически дополнять существующие библиотеку постов ювелирных украшений.
CharlesTut
импорт товаров
JacobBal
Каждый человек способен испытать испуг в разных жизненных ситуациях. Это – абсолютно обычное явление, помогающее нам спасти себе жизнь в момент угрозы жизни. Правда большинство страхов это просто напросто иррациональное переживание. Например страх летать на самолёте. С этими страхами возможно будет бороться успешно, существуют очень действенные технологии, про них узнаете в анализе ссылка на статью Также можно почувствовать страх опасности, именно это ощущение помогает спасти человеческую жизнь в угрожающих ситуациях. Соответственно страх это адекватное состояние, проблема может возникнуть только лишь если подобный страх часто сводит с ума. В этом случае надо предпринимать определенные меры, обращаться в психологический центр.
Caseybup
електро тепла підлога
Howardton
электро полы с подогревом