Маршрутизация Angular Подробное руководство

Apr 23, 2018

Порядок настройки роутинга

// app.module.ts
// I
import { RouterModule } from '@angular/router'

@NgModule({
  imports: [
      // II
    RouterModule.forRoot([{
            path: 'welcome',
            component: WelcomeComponent
        }, {
            path: '',
            redirectTo: 'welcome',
            pathMatch: 'full'
        }, {
            path: '**',
            component: PageNotFoundComponent
        }
    ]),

/// III (index.html)

<router-outlet></router-outlet>

Свойства Routes

  • path - путь для маршрута
  • component - компонент для URL
  • pathMatch - задает соответствие URL свойству PATH ('full', 'prefix'); свойство обязательно при наличии redirectTo
  • redirectTo - редирект на другой URL
  • children - для задания дочерних маршрутов, которые отображают дополнительные компоненты во вложенных элементах router-outlet, содержащихся в шаблоне компонента активации
  • outlet - для поддержки множественных компонентов outlet
  • resolve - определяет действия, которые должны быть совершены перед активацией маршрута
  • canActive - управляем активацией маршрута
  • canActiveChild - управляем активацией дочернего маршрута
  • canDeactivate - управляем тем, когда маршрут может деактивироваться для активации нового маршрута
  • loadCildren - для настройки модуля, который загружается только в случае необходимости
  • canLoad - загрузка модулей по требованию

Директивы RouterOutlet

Именно корневой компонент обеспечивает навигацию между разными компонентами. RouterOutlet - директива (<router-outlet>) станет заполнителем, где роутер отобразит view (при этом все предыдущие компоненты будут удалены).

Именованные элементы router-outlet

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

Чтобы отличать элементы router-outlet используется атрибут name. router-outlet без атрибута name является первичным, что равносильно outlet: "primary".

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

<div>
    <router-outlet name="left"></router-outlet>
</div>
<div>
    <router-outlet name="right"></router-outlet>
</div>
let routing = RouterModule.forChild([
    {
        path: "",
        component: TestComponent,
        children: [
            {
                path: "",
                children: [
                    // свойство outlet используется для назначения router-outlet
                    { outlet: "primary", path: "", component: FirstComponent, },
                    { outlet: "left", path: "", component: SecondComponent, },
                    { outlet: "right", path: "", component: SecondComponent, },
                ]
            },

Элемент base

Элемент base задает URl, с которым будут сравниваться пути маршрутизации.

<base href="/">

Директива routerLink

Директива routerLink приказывает Angular выбрать в качестве целевого маршрута результат значения(выражения) директивы routerLink.

<a routerLink=""
   routerLinkActive="active"
   [routerLinkActiveOptions]="{exact:true}">
    Главная
</a>
<a routerLink="/about"
   routerLinkActive="active">
    О сайте
</a>

Выражения для routerLink задаются в виде массива, значения которого соответствуют каждому конкретному сегменту.

<a [routerLink]="['item', 'edit', '7']">item 7</a>

Директива routerLinkActive

Для стилизации активных ссылок применяется специальная директива routerLinkActive. Активная ссылка с директивой [routerLink] получает класс router-link-active.

routerLinkActive по умолчанию выполняет частичный поиск совпадения для активных URl. Однако это можно регулировать: если exact равно true, то происходит полное сопоставление URl.

<a routerLink="/about"
    routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"  >
    О сайте
</a>

Служба ActivatedRoute и параметры маршрута

ActivatedRoute - содержит информацию о маршруте связанную с компонентом, который загружен в outlet.

Свойство ActivatedRoute

snapshot - возвращает объект ActivatedRouteSnapshot, который описывает текущий маршрут.

Свойства ActivatedRouteSnapshot

  • url - возвращает массив объектов URLSegment, каждый из которых описывает один сегмент URl-адреса для текущего маршрута
  • params - возвращает объект Params с описанием параметров URl
  • queryParams - возвращает объект Params с описанием параметров запроса URl
  • fragment - возвращает объект string, содержащий фрагмент URl

Свойства URLSegment

  • Path - строка со значением сегмента
  • parameters - индексированная коллекция параметров
// определение маршрутов
const appRoutes: Routes =[
    { path: 'item/:id', component: MyComponent},
    {
        path: 'products/:id/edit',
        component: ProductEditComponent
    }

Маршрут вида 'item/:id' будет соответствовать любому URl-адресу из двух сегментов, первый из которых будет содержать item. Второй сегмент будет содержать значение, которое присвоится параметру с именем id. Mы сможем обратиться к компоненту с запросом типа /item/7, и число 7 будет представлять параметр id.

import { Component, OnInit} from '@angular/core';
import { ActivatedRoute } from '@angular/router';

export class ProductDetailComponent implements OnInit {
    constructor(private productService: ProductService,
                private route: ActivatedRoute) {
        // плохая практика вызывать в конструкторе "тяжелые" операции (в том числе и связанные с route)
        // поэтому лучше его использовать в ngOnInit()
    }

    ngOnInit(): void {
        // первый вариант вытащить параметры от роутинга
        // snapshot - параметры route на момент инициализации
        let id = +this.route.snapshot.params['id'];
    
        // Анализ сегмента URl:
        console.log(this.route.snapshot.url[1].path);
    
    }
}

Реагируем на текущие изменения навигации при помощи ActivatedRoute

Свойства класса ActivatedRoute относящиеся к Observable:

  • url - возвращает Observable<UrlSegment[]>: набор сегментов URl при изменении маршрута
  • params - возвращает Observable<Params>: набор параметров при изменении маршрута
  • queryParams - возвращает Observable<Params>: набор параметров запроса при изменении маршрута
  • fragment - возвращает Observable<string>: фрагмент URl при изменении маршрута
export class ProductDetailComponent implements OnInit {
    constructor(private route: ActivatedRoute) {

        // подписываемся на параметры
        this.route.params.subscribe(
            params => {
                let id = +params['id'];
                this.getProduct(id);
            }
        );

        // подписываемся на GET-параметры
        this.route.queryParams.subscribe((params: Params) => {
            this.color = params['color'];
            this.year = params['year'];
        })

    }
}

Множественные параметры и routerLink

const routes: Routes = [
    { path: "item/:mode/:id", component: ItemComponent },
    { path: "item/:mode", component: ItemComponent },
    { path: "", component: MainComponent }
]

Mы сможем обратиться к компоненту с запросом типа /item/edit/7:

<a [routerLink]="['item', 'edit', '7']">item 7</a>
Выражения для routerLink задается в виде массива, значения которого соответствуют каждому конкретному сегменту.

Дополнительные(необязательные) параметры маршрута

<a
    [routerLink] = "['/products', product.id, 'edit', { name: 'John', city: 'Moscow' }]">
    Edit
</a>

В браузере: http://localhost:3000/products/1/edit;name=John;city=Moscow

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

Класс Router

Класс Router предоставляет доступ к системе навигации из компонентов.

Свойства и методы Класса Router

  • navigated - возвращает true, если было хотя бы одно событие навигации, иначе false.
  • url - возвращает активный URl-адрес
  • isActive(url, exact) возвращает true, если заданный URl совпадает с URl, определенным активным маршрутом.
  • events - возвращает объект типа Observable<Event>, который может использоваться для отслеживания навигационных изменений.
  • navigateByUrl(url, extras) - делаем навигацию к заданному URl-адрему.
  • navigate(commands, extras) - делаем навигацию по массиву сегментов.
gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero.id]);

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

gotoBack(): void {
    this.router.navigateByUrl('/');
}

Система маршрутизации уничтожает компонент после того, как он перестает отображаться.

Навигация с queryParams и хэшем

// cars?color=pink&year=1975#pic

openPage() {
    this.router.navigate(['./cars', 8, 'opel'], {
        // ?color=pink&year=1975
        queryParams: {
        color: 'pink',
        year: 1975
        },
        // хэш: #pic
        fragment: 'pic'
    });
}

События навигации

Некоторые компоненты должны знать о выполнении навигации, неважно задействованы они в навигации или нет. Свойство events объекта router возвращает объект типа Observable<Event>, который может использоваться для отслеживания навигационных изменений.

Типы Event для Router.events

Event на angular.io

  • NavigationStart - триггер на начало навигации
  • RoutesRecognized - триггер на 'узнавание' системной марщрута
  • NavigationEnd - триггер на конец навигации
  • NavigationCancel - триггер на отмену навигации
  • NavigationError - триггер на ошибку навигации
export class TestComponent {

    public testProp = 1;

    constructor(router: Router) {
        router.events
            .filter(e => e instanceof NavigationEnd || e instanceof NavigationCancel)
            .subscribe(e => { this.testProp = null; });
    }
}

Универсальные маршруты

** - так обозначается путь, который может соответствовать любому URl.

@NgModule({
  imports: [
      //II
    RouterModule.forRoot([{
            path: '**',
            component: PageNotFoundComponent
        }
    ]),

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

Перенаправление маршрутов определяется при помощи свойства redirectTo, которое задает URl на который произойдет перенаправление. При перенаправлении должно быть задано свойства pathMatch (со значениями: full (URl полностью совпадает со значением совйства path), prefix (URl начинается с заданного пути))
// определение маршрутов
const routes: Routes = [
    // pathMatch:'full' указывает, что запрошенный адрес должен полностью соответствовать маршруту
    { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
    { path: 'dashboard',  component: DashboardComponent },
    { path: 'detail/:id', component: HeroDetailComponent },
    { path: 'heroes',     component: HeroesComponent }
];

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

Дочерние маршруты позволяют компонентом реагировать на изменении URl-адреса, путем вставки в шаблон компонентов элемента router-outlet.

Дочерние маршруты определяются в свойстве children. Компоненты выбранные как дочерние маршруты отображаются в router-outlet, который определен в родительском компоненте.

@NgModule({
    imports: [
        // Определение маршрутов для feature модуля

        RouterModule.forChild([
            // "" так как подгружаем по lazy loading и в основном роутинге уже прописан путь, который зайдет сюда
            { path: "", component: PhraseListComponent },
            { path: "phrase/:id", component: PhraseDetailsComponent },
            { path: "test-animation", component: TestAnimationComponent },
            {
                path: 'cars',
                component: CarsPageComponent,
                children: [{
                        path: ':id/:name',
                        component: CarPageComponent, canActivate: [ CarsGuardTestGuard ]
                    }
                ]
            }
        ])
    ],
    exports: [
        RouterModule // делаем re-export модуля для использования директив при маршрутизации,
                     // благодаря этому данный RouterModule будет доступен в модуле,
                     // который импортирует данный модуль
    ]
})
export class PhrasesRoutingModule { }

Свойства ActivatedRoute для обращения к частям маршрута

  • PathFromRoot - массив объектов ActivatedRoute, содержащий маршруты, использованные для сопоставления с текущим URl
  • parent - объект ActivatedRoute для родителя маршрута
  • firstChild - объект ActivatedRoute (первый дочерний), использованный для сопоставления с текущим URl
  • children - массив объектов ActivatedRoute (все дочерние маршруты), использованные для сопоставления с текущим URl

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

Guards

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

Свойства для использования guards:

  • resolve - назначает guadrs, которые откладывают активацию маршрута до завершения какого-либо действия, например, нам нужно загрузить данные с бэк-да.
  • canActivate - назначает guadrs, которые определяют возможность активации маршрута.
  • canActivateChild - назначает guadrs, которые определяют возможность активации дочерних маршрутов текущего маршрута
  • canDeactivate - назначает guadrs, которые определяют возможность уйти с текущего маршрута.
  • canLoad - определяет может ли модуль загрузиться с использованием lazy loading.

resolve

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

resolve - это классы, определяющие метод resolve с двумя аргументами:

  • route: ActivatedRouteSnapshot - текущее состояние активного route
  • state: RouterStateSnapshot - текущее состояние всего route

Допустимые значения, которые возвращает метод resolve, при которых произойдет активация нагого маршрута:

  • Observable<any> - когда срабатывает observer
  • Promise<any> - при обработке Promise
  • any - любой результат

Определяем Resolvers как отдельный сервис:

// app/products/product-resolver.service.ts

@Injectable()
export class ProductResolver implements Resolve<IProduct> {
    constructor(private productService: ProductService){ }
    // route - текущее состояние активного route
    // state - текущее состояние всего route
    resolve(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<IProduct> {

        let id = +route.params['id'];
        let product = this.productService.getProduct(id);
        return product;
    }
}

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

//app/products/product.module.ts
@NgModule({
    imports: [
    RouterModule.forChild([{
        {
            path: 'products/:id',
            component: ProductDetailComponent,
            // key - имя того что ресолвить (product), ProductResolver - resolver
            resolve: {product: ProductResolver}
        },
    ],
    providers: [
        ProductResolver,
        ProductService
    ]

Модифицируем компонент для работы с resolver:

//products/product-detail.component.ts

export class ProductDetailComponent implements OnInit {
    pageTitle: string = 'Product Detail';
    product: IProduct;
    errorMessage: string;

    constructor(private route: ActivatedRoute) {}

    ngOnInit(): void {
        // продукт загруженные resolver
        this.product = this.route.snapshot.data['product'];
    }

}

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

canActivate

Интерфейс CanActivate расширяет класс для решения задачи по активации маршрута, тем самым мы определяем guard по активации маршрутов. Метод canActivate - возвращает логическое значение, указывающее на необходимость активации компонента. Удобно для проверки паролем и т.д.

Допустимые значения, которые возвращает метод CanActivate:

  • boolean - обычно при синхронной операции; true - добро, false - отказ.
  • Observable<boolean> - асинхронная операция; Angular ожидает, когда Observable выдаст значение.
  • Promise<boolean> - асинхронная операция; Angular ожидает, как разрешится Promise.
// auth-guard-admin.service.ts
@Injectable()
export class AuthGuardAdmin implements CanActivate {
    // Observable<boolean>|Promise<boolean>|boolean - возможные результаты работы метода
    // Если return true, то маршрут будет активирован, иначе - нет
  constructor(public auth: AuthService, private router: Router) {}

  canActivate() {
    return this.auth.isAdmin;
  }

}
// routing.module.ts
const routes: Routes = [
    //...
    { path: 'delete-article', component: DeleteArticleComponent, canActivate: [AuthGuardAdmin] },
    //...

И не забудьте добавить AuthGuardLogin в providers:

// app.module.ts
//...
providers: [
    AuthGuardLogin
//...

canActivateChild

Метод canActivateChild - определяет возможность активации дочерних маршрутов текущего маршрута. guard применяется к родительскому маршруту, при этом Метод canActivateChild вызывается всякий раз при изменении дочерних маршрутов. CanActivateChild на angular.io

@NgModule({
    imports: [
        RouterModule.forRoot([
            {
                path: 'root',
                canActivateChild: [CanActivateTeam],
                children: [
                    {
                        path: 'team/:id',
                        component: Team
                    }
                ]
            }
        ])
    ],
    providers: [CanActivateTeam, UserToken, Permissions]
})

canDeactivate

Деактивация маршрута происходит при уходе пользователя с маршрута. Полезно, например, если у пользователя остались несохраненные данные. CanDeactivate на angular.io

Класс canDeactivate получает три аргумента:

  • component: T - деактивизируемый компонент
  • currentRoute: ActivatedRouteSnapshot
  • currentState: RouterStateSnapshot
@Injectable()
class CanDeactivateTeam implements CanDeactivate<TeamComponent> {
    constructor(private permissions: Permissions, private currentUser: UserToken) {}

    canDeactivate(
        component: TeamComponent,
        currentRoute: ActivatedRouteSnapshot,
        currentState: RouterStateSnapshot,
        nextState: RouterStateSnapshot
    ): Observable<boolean>|Promise<boolean>|boolean {
        return this.permissions.canDeactivate(this.currentUser, route.params.id);
    }
}
@NgModule({
    imports: [
        RouterModule.forRoot([
            {
                path: 'team/:id',
                component: TeamCmp,
                canDeactivate: [CanDeactivateTeam]
            }
        ])
    ],
    providers: [CanDeactivateTeam, UserToken, Permissions]
})

LAZY LOADING

Декомпозиция модулей (разбиение на модули) нужна в том числе для Lazy Loading (ленивая загрузка) - данную особенность предоставляют модули. Обычно приложение загружает весь js со всеми модулями, но зачем нам загружать все страницы, к примеру, если тот же юзер на второстепенные страницы может не перейти? Незачем. Поэтому мы можем вынести второстепенные страницы в отдельные модули и загружать их по необходимости.

Вот и рассмотрим на примере роутинга, то есть подключение модуелей с Lazy Loading по мере необходимости. Как видите, мы подключаем модуль указав путь, а не через import, так как import загрузит модуль сразу, а нам это не надо, также удалите import { PhrasesModule } в app.module.ts. Мы не будем использовать import, чтобы не webpack сразу не подключил наш файл в приложение.

loadChildren - значением данного свойства является строка, чтобы webpack ничего заранее не загрузил.
const routes: Routes = [
    { path: '', component: AboutComponent },

    // LAZY LOADING - Очень актуально на больших приложениях (актуально, например, подгружать модуль лишь после авторизации
    // пользователя этим мы ускоряем первоначальную загрузку приложения в разы)
    // Без "lazi loading" модуль PhrasesModule подгружался в app.module.ts в imports
    // и мы убираем из его app.module.ts
    // и настроим его роутинг на 'подгрузка по требованию'
    // Значение loadChildren является строкой (путь до нашего модуля), чтобы webpack его заранее не загрузил
    // #PhrasesModule - этим указываем какой класс подгрузить
    // для того чтобы не было роута phrases/phrases нужно заменить в phrases.module.ts { path: "phrases", component: PhraseListComponent },
    // на { path: "", component: PhraseListComponent }
    // canLoad: [AuthGuardAdmin] - guard для ленивой загрузки
    { path: 'phrases', loadChildren: './test/phrases.module#PhrasesModule', canLoad: [CanLoadChildModuleGuard] }
];

@NgModule({
    imports: [
        RouterModule.forRoot(
            routes,
            {
                initialNavigation: 'enabled',
                preloadingStrategy: PreloadAllModules
            }
        )
    ],
    exports: [ RouterModule ]
})
import { RouterModule } from "@angular/router";

// роутинг для подгружаемого модуля:
@NgModule({
    imports: [
        // Метод forRoot должен использоваться только в AppModule
        RouterModule.forChild([
            // "" так как подгружаем по lazy loading и в основном роутинге уже прописан путь, который зайдет сюда
            { path: "", component: PhraseListComponent },
            {
                path: 'cars',
                component: CarsPageComponent,
                children: [{
                        path: ':id/:name',
                        component: CarPageComponent, canActivate: [
                            CarsGuardTestGuard
                        ]
                    }
                ]
            }
        ])
    ],
    exports: [
        RouterModule
        // делаем re-export модуля для использования директив при маршрутизации,
        // благодаря этому данный RouterModule будет доступен в модуле, который импортирует данный модуль
    ]
})
export class PhrasesRoutingModule { }

Теперь при переходе на страницу phrases в консоли вы сможете увидеть подгружаемый chank.

Отметьте, что для Guard вместо canActivate: [CanLoadChildModuleGuard] нужно писать canLoad: [CanLoadChildModuleGuard]

Добавить комментарий