DevGang
Авторизоваться

Angular Router: раскрываем некоторые интересные факты и особенности 

Нельзя отрицать, что пакет angular/router полон полезных функций. На этот раз вместо того, чтобы сосредоточиться на одной конкретной теме, мы рассмотрим некоторые интересные факты и свойства этого пакета, о которых вы, возможно, не знали. Они могут варьироваться от различных видов сравнения (например, перенаправления relative против absolute) до неочевидных деталей (например RouterOutlet, иерархия; как URL-адрес устанавливается в браузере и т.д.).

В этой статье предполагается, что читатель имеет некоторые базовые знания об Angular Router (например, навигация по маршруту, outlets). К концу вы должны лучше понять, на что способен этот пакет.

Относительные и абсолютные перенаправления

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

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

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    component: DefaultComponent
  },
  {
    path: 'a/b',
    component: AComponent, // reachable from `DefaultComponent`
    children: [
      {
        // Reached when `redirectTo: 'err-page'` (relative) is used
        path: 'err-page',
        component: BComponent,
      },
      {
        path: '**',
        redirectTo: 'err-page'
      },
    ],
  },
  {
    // Reached when `redirectTo: '/err-page'` is used
    path: 'err-page',
    component: DComponent,
  }
]

Демо StackBlitz можно найти здесь.

С текущим параметром redirectTo: 'err-page' (относительный путь) будет использоваться BComponent. Если бы мы изменили его на /err-page, то нужно было бы использовать DComponent. В качестве обобщения можно сказать, что одно из различий между redirectTo: 'foo/bar' и redirectTo: '/foo/bar' заключается в том, что при использовании абсолютного пути поиск следующего объекта конфигурации будет начинаться с корня, то есть с первого, самого внешнего массива маршрутов.

const routes: Routes = [
  // **STARTS FROM HERE**
  {
    /* ... */
  },
  {
    /* ... */
    children: [
      /* ... */
      {
        path: '**',
        redirectTo: '/err-page'
      },
    ],
  },

  {
    path: 'err-page',
    /* ... */
  }
]

В то время как при использовании относительного пути поиск начнется с первого маршрута в массиве, с которого началась операция перенаправления:

const routes: Routes = [
  {
    /* ... */
  },
  {
    /* ... */
    children: [
      // **STARTS FROM HERE**
      /* ... */
      {
        path: '**',
        redirectTo: 'err-page'
      },
    ],
  },

  {
    path: 'err-page',
    /* ... */
  }
]

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

{
  path: 'a/b',
  component: AComponent,
  children: [
    {
      path: '',
      component: BComponent,
    },
    {
      path: 'c',
      outlet: 'c-outlet',
      component: CComponent,
    },
  ],
},
{
  path: 'd-route',
  redirectTo: '/a/b/(c-outlet:c)'
}

Демо StackBlitz.

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

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

Во-первых, с маршрутом без подстановочных знаков мы можем повторно использовать query params и positional params (параметры, которые следуют модели :nameOfParam) из текущего выданного URL:

const routes: Routes = [
  {
    path: 'a/b',
    component: AComponent,
    children: [
      {
        // Reached when `redirectTo: 'err-page'` (relative) is used
        path: 'err-page',
        component: BComponent,
      },
      {
        path: 'c/:id',
        // foo=:foo - get the value of the `foo` query param that 
        // exists in the URL that against this route
        // it works for relative paths as well: `err-page/:id?errored=true&foo=:foo`
        redirectTo: '/err-page/:id?errored=true&foo=:foo'
      },
    ],
  },
  {
    // Reached when `redirectTo: '/err-page'` is used
    path: 'err-page/:id',
    component: DComponent,
  }
]

Демо StackBlitz.

В приведенном выше фрагменте мы видим, что этому шаблону следует

  1. ?name=:foo - параметр запроса foo берется из фактического url
  2. path: 'a/:id'redirectTo: 'err-page/:id'id позиционный параметр берется из a/:id

А вот куда бы мы попали по такому маршруту:

<button routerLink="a/b/c/123" [queryParams]="{ foo: 'foovalue' }">...</button>

Кроме того, при использовании пути non-wildcard и перенаправления relative эти дополнительные сегменты URL будут добавлены к redirectTo сегментам

const routes: Routes = [
  {
    path: 'a/b',
    component: AComponent,
    children: [
      {
        path: 'err-page/test',
        component: BComponent,
      },
      {
        // `redirectTo: '/err-page'` - would lead to errors
        path: 'c',
        redirectTo: 'err-page'
      },
    ],
  },
  
  // this could never be reached from `path: 'c'`
  {
    path: 'err-page/test',
    component: DComponent,
  }
]

Примечание. Это работает только для relative переадресации.

Итак, мы можем добраться до маршрута BComponent следующим образом:

<button routerLink="a/b/c/test">...</button>

Демо StackBlitz.

Все может даже стать немного сложнее (и интереснее), если мы также рассмотрим matrix params (например ;k1=v1;k2=v2). В качестве дополнительного примечания, positional params это те, которые мы явно определяем в путях маршрута (например /:id), тогда как matrix params взяты вместе с их путями. Внутри Angular использует такие объекты, как UrlSegmentGroup, UrlSegment для достижения своих функций. Если мы посмотрим на реализацию UrlSegment, мы увидим упомянутые параметры матрицы. Имея это в виду, давайте посмотрим на пример:

const routes: Routes = [
  {
    path: 'd/a/:id/e',
    component: DComponent,
  },
  {
    // `redirectTo: '/d/a/:id/e'` would work as well
    path: 'a/:id', 
    redirectTo: 'd/a/:id/e'
  },
]

Если мы начнем навигацию с

<button [routerLink]="['/a', { p1: 1 }, '1', { p2: 2, p3: 3 }]">...</button>

Демо StackBlitz.

маршрут DComponent будет активирован, и будет в конечном итоге, этот URL: .../d/a;p1=1/1;p2=2;p3=3/e

Прежде всего, ['a/path', { p1, p2, p3 }] это способ передать в сегмент параметры матрицы. Параметры матрицы будут привязаны к предыдущему пути. Затем, как мы узнали из предыдущих абзацев, мы можем использовать позиционные параметры, которые присутствуют в текущем маршруте в пути redirectTo. Важно отметить, что параметры матрицы данного сегмента будут сохранены в новом пути навигации, если они используются в redirectTo.

Наконец, следует отметить, что маршруты с подстановочными знаками можно использовать только повторно query params. Позиционные параметры невозможны, потому что для повторного использования таких параметров они сначала должны найти их соответствие в свойстве path, и '**', поскольку они используются, они не могут использоваться в дальнейшем redirectTo.

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

Router.navigate против Router.navigateByUrl

Хотя у них обоих одна и та же цель - начать новую навигацию, у них также есть несколько отличий. Прежде чем раскрывать их, важно знать, что Angular Router работает с UrlTree. A UrlTree можно рассматривать как десериализованную версию URL-адреса (строку).

navigate() создаст UrlTree необходимые для навигации по текущему UrlTree. Это может быть немного сложнее в использовании, так как в некоторых случаях это необходимо, чтобы обеспечить маршрут relativeTo: navigate(commandsArray, { relativeTo: ActivatedRouteInstance }). Если опция relativeTo не указана, будет выбран корень ActivatedRoute.

Метод navigateByUrl() создаст новый UrlTree, независимо от текущего.

Если вы хотите поэкспериментировать с некоторыми примерами, вы можете найти их в этой демонстрации StackBlitz.

Как устанавливается URL-адрес в браузере?

Под капотом Angular Router просто использует собственный history API. Например, при переходе к новому маршруту /user/:id, вызывается метод history.pushState. Точно так же используется history.replaceState при переходе по тому же пути или когда для параметра replaceUrl установлено значение true.

Вот пример StackBlitz, демонстрирующий поведение, которого можно достичь с помощью этой опции replaceUrl.

Параметр skipLocationChange

Эти параметры гарантируют, что метод Router, который отвечает за установку URL-адреса браузера, таким образом добавляя элементы в стек истории, не будет вызываться. Тем не менее, внутреннее состояние Router будет обновляться соответствующим образом (например, маршрут PARAMS, параметры запроса, все, что может быть observed в ActivatedRoute).

Здесь вы можете найти демонстрацию StackBlitz.

Как видите, из-за того, что используется эта опция, /d даже не будет отображаться в адресной строке. Несмотря на это, компонент маршрута /d (DComponent) будет загружен.

Иерархия, созданная директивой RouterOutlet

Основная единица Angular Router - это директива RouterOutlet (идентифицируемая как router-outlet). Без него было бы невозможно показать что-либо в браузере. Но, как мы видели при создании приложений Angular, нередки случаи, когда у нас появляются вложенные теги router-outlet. Говоря об этом, предположим, что у нас есть такая конфигурация маршрута:

const routes = [
  {
    path: 'foo',
    component: FooComponent,
    children: [
      { path: 'bar/:id', component: BarComponent }   
    ]
  }
];

а также предположим, что мы вводим ActivatedRoute внутрь BarComponent. Вы когда - нибудь задавались вопросом, почему, при навигации foo/bar/123, экземпляр ActivatedRoute является правильным один (например, он выставляет params и queryParams которые связаны с bar/:id)? Это снова важная деталь, которая регулируется директивой RouterOutlet. В этом разделе мы собираемся изучить, как это достигается (подсказка: это включает создание собственного инжектора!).

Давайте рассмотрим более простой сценарий - у нас есть такая конфигурация маршрута:

const routes = [
  {
    path: 'foo',
    component: FooComponent,
  }
]

А теперь, чтобы увидеть визуализированное представление /foo, нам нужно вставить тег router-outlet вapp.component.html

<button routerLink="/foo">Go to /foo route</button>

<router-outlet></router-outlet>

Здесь начинается самое интересное. Посмотрим, каковы первые шаги инициализации:

constructor(
    private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
    private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
    private changeDetector: ChangeDetectorRef) {
  // in case we're using named outlet, we provide the `name` property
  // as we can see, it defaults to `PRIMARY_OUTLET`(`primary`)
  this.name = name || PRIMARY_OUTLET;
  parentContexts.onChildOutletCreated(this.name, this);
}

Мы уже видим кое-что, что встречается не так часто: ChildrenOutletContexts. Посмотрим, о чем идет речь:

export class ChildrenOutletContexts {
  // contexts for child outlets, by name.
  private contexts = new Map<string, OutletContext>();

  /** Called when a `RouterOutlet` directive is instantiated */
  onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
    const context = this.getOrCreateContext(childName);
    context.outlet = outlet;
    this.contexts.set(childName, context);
  }
  /* ... */

  getOrCreateContext(childName: string): OutletContext {
    let context = this.getContext(childName);

    if (!context) {
      context = new OutletContext();
      this.contexts.set(childName, context);
    }

    return context;
  }

  getContext(childName: string): OutletContext|null {
    return this.contexts.get(childName) || null;
  }
}

export class OutletContext {
  outlet: RouterOutlet|null = null;
  route: ActivatedRoute|null = null;
  resolver: ComponentFactoryResolver|null = null;
  children = new ChildrenOutletContexts();
  attachRef: ComponentRef<any>|null = null;
}

Итак, когда создается любой RouterDirective, он сразу же вызывает ChildrenOutletContexts.onChildOutletCreated(). Затем он либо повторно использует существующий контекст, либо создаст новый, что и является текущей ситуацией. Мы ввели понятие контекста, и его можно точно описать с помощью OutletContext. Здесь интересно отметить, что у контекста есть свойство children, которое указывает ChildrenOutletContexts. Это означает, что мы можем визуализировать этот процесс как дерево контекстов, точнее дерево экземпляров OutletContext.

Теперь вы можете задаться вопросом, почему класс ChildrenOutletContexts должен поддерживать данный интерфейс:

private contexts = new Map<string, OutletContext>();

Даже если этот вопрос не сразу пришел в голову, это вопрос, который стоит задать. Чтобы ответить на этот вопрос, напомним, что у нас также могут быть названные router-outlet. Итак, как бы выглядел экземпляр context Map, если бы мы их добавили?:

const routes = [
  {
    path: 'foo',
    component: FooComponent,
  },
  {
    path: 'bar',
    component: BarComponent,
    outlet: 'named-bar'
  }
]
<!-- app.component.html -->
<button routerLink="/foo">Go to /foo route</button>
<button [routerLink]="[{ outlets: { named-bar: bar } }]">Go to /bar route - named outlet</button>

<router-outlet></router-outlet>
<router-outlet name="named-bar"></router-outlet>

На этот раз основной класс ChildrenOutletContexts будет вызывать дважды onChildOutletCreated, каждый раз, когда будет создаваться новый OutletContext. Итак, context будет следующим:

{
  primary: OutletContext,
  'named-bar': OutletContext
}

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

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

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

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

В конце файла, в котором реализовано RouterOutlet, есть кое-что, на что стоит обратить внимание:

class OutletInjector implements Injector {
  constructor(
      private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,
      private parent: Injector) {}

  get(token: any, notFoundValue?: any): any {
    if (token === ActivatedRoute) {
      return this.route;
    }

    if (token === ChildrenOutletContexts) {
      return this.childContexts;
    }

    return this.parent.get(token, notFoundValue);
  }
}

Если мы посмотрим, как RouterOutlet что-то отображает на экране, мы увидим, как используется OutletInjector:

const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
// this.location - `ViewContainerRef`
this.activated = this.location.createComponent(factory, this.location.length, injector);

Это то, что позволяет нам всегда получать необходимое ActivatedRoute, когда это необходимо. Когда компонент внедряет ActivatedRoute, он будет искать в дереве инжекторов, пока не найдет первое вхождение токена. scope фактически создается, когда создается новый RouterOutlet. Как мы видим в реализации OutletInjector, когда требуется токен ActivatedRoute, он предоставляет активированный маршрут, который был получен при создании инжектора.

Нужно ли отказываться от подписки на свойства ActivatedRoute?

Короткий ответ - нет.

Вот как создается ActivatedRoute:

function createActivatedRoute(c: ActivatedRouteSnapshot) {
  return new ActivatedRoute(
      new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
      new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
}

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

{
  path: 'a/:id',
  component: AComponent,
  children: [
    {
      path: 'b',
      component: BComponent,
    },
    {
      path: 'c',
      component: CComponent,
    },
  ]
}

и выданный URL, например a/123/b

у вас будет дерево ActivatedRoute:

 APP
  |
  A
  |
  B

Каждый раз, когда вы планируете навигацию (например router.navigateToUrl()), маршрутизатор должен пройти несколько важных этапов:

  1. apply redirects: проверка редиректов, загрузка ленивых модулей, поиск ошибок NoMatch
  2. recognize: создание дерева ActivatedRouteSnapshot
  3. preactivation: сравнение полученного дерева с текущим; эта фаза также собирает canActivate и canDeactivateохраняет, основываясь на обнаруженных различиях
  4. running guards
  5. create router state: где создается дерево ActivatedRoute
  6. activating routes: это вишенка на торте и место, где дерево ActivatedRoute используется

Также важно упомянуть о той роли, которую играет router-outlet.

Как было описано в предыдущем разделе, Angular отслеживает router-outlet с помощью объекта Map.

Итак, для нашей конфигурации маршрута:

{
  path: 'a/:id',
  component: AComponent,
  children: [
    {
      path: 'b',
      component: BComponent,
    },
    {
      path: 'c',
      component: CComponent,
    },
  ]
}

в контексты RouterOutlet будет выглядеть следующим образом (примерно):

{
  primary: { // Where `AComponent` resides [1]
    children: {
      // Here `AComponent`'s children reside [2]
      primary: { children: { /* ... */ } }
    }
  }
}

Когда объект RouterOutlet активирован (он собирается что-то отобразить), будет вызван его метод activateWith. В предыдущем разделе мы видели, что это место, где создается объект OutletInjector, что обеспечивает scope для ActivatedRoute:

activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
  if (this.isActivated) {
    throw new Error('Cannot activate an already activated outlet');
  }

  this._activatedRoute = activatedRoute;
    
  /* ... */

  const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
  this.activated = this.location.createComponent(factory, this.location.length, injector);
}

Обратите внимание, что this.activated держит маршрутизируемый компонент (например AComponent) и this._activatedRoute держит ActivatedRoute для этого компонента.

Давайте теперь посмотрим, что происходит, когда мы переходим к другому маршруту и ​​текущее представление уничтожается:

deactivateRouteAndOutlet(
    route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
  const context = parentContexts.getContext(route.value.outlet);

  if (context) {
    const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
    
    // from this we can also deduce that a component requires an additional `router-outlet` in this template
    // if it is part of route config. object where there is also a `children`/`loadChildren` property
    // the `route`'s `children` can also refer the routes obtained after loading a lazy module
    const contexts = route.value.component ? context.children : parentContexts;

    // Deactivate children first
    forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts));

    if (context.outlet) {
      // Destroy the component
      context.outlet.deactivate();
      // Destroy the contexts for all the outlets that were in the component
      context.children.onOutletDeactivated();
    }
  }
}

где RouterOutlet.deactivate() выглядит так:

deactivate(): void {
  if (this.activated) {
    const c = this.component;
    this.activated.destroy(); // Destroying the current component
    this.activated = null;
    // Nulling out the activated route - so no `complete` notification
    this._activatedRoute = null;
    this.deactivateEvents.emit(c);
  }
}

Обратите внимание на this._activatedRoute = null;, это означает, что нет необходимости отказываться от подписки на наблюдаемые свойства ActivatedRoute. Это потому, что эти свойства являются BehaviorSubject и, как мы знаем, тип Subject поддерживает список подписчиков. Утечка памяти может произойти, если подписчик не удалил себя из списка (он может удалиться с помощью subscriber.unsubscribe()). Но когда сущность, которая содержит все (в данном случае список подписчиков), обнуляется, она может быть обработана сборщиком мусора, поскольку на нее больше нет ссылок, а это означает, что подписчик, который не отписался, больше не может быть вызван.

Параметр paramsInheritanceStrategy

Эта опция может быть указана как часть объекта ExtraOptions при вызове RouterModule.forRoot([], extraOptions) и принимает 2 значения: 'emptyOnly' (по умолчанию) или 'always'. При использовании 'emptyOnly', позволяет наследовать объекты params и data, которые будут унаследованы от родительского маршрута, если текущий маршрут (не обязательно активированный один) имеет path: '' или если родительский маршрут является маршрутом componentless.

Например, с такой конфигурацией маршрута:

const routes: Routes = [
  {
    path: "",
    pathMatch: "full",
    component: DefaultComponent
  },
  {
    path: "a/:id",
    data: { one: 1 },
    resolve: { two: "resolveTwo" },
    // component: AComponent,
    children: [
      { path: "", data: { three: 3 }, component: BComponent },
      {
        path: "",
        data: { four: 4 },
        resolve: { five: "resolveFive" },
        component: CComponent,
        outlet: "named-c"
      }
    ]
  }
];

если перейти к /a/123, во - первых, оба маршрута children будут активированы (потому что у обоих есть path: ''), во вторрых оба они будут наследовать data и params от своих родителей: params: { id: 123, }data: { one: 1, two: valueOfResolveTwo }. Если бы мы раскомментировали строку component: AComponent,, результаты были бы такими же, поскольку условием наследования является то, что объект маршрута либо имеет бескомпонентный родительский маршрут, либо сам маршрут должен иметь значение path''.

Вы можете увидеть приведенные выше результаты и продолжить эксперименты в этой демонстрации StackBlitz.

Давайте также рассмотрим еще несколько примеров:

[
  {
    path: 'a',
    data: { one: 1 },
    children: [ { path: 'b', data: { two: 2 }, component: ComponentB } ]
  }
]

После перехода к a/b, в ComponentB ActivatedRoute.data будет {one: 1, two: 2}, потому что родитель ActivatedRoute принадлежит к маршруту componentless.

[
  {
    path: 'a',
    component: ComponentA,
    data: { one: 1 },
    children: [ { path: 'b', data: { two: 2 }, component: ComponentB } ],
  },
]

После перехода к a/b, в ComponentB ActivatedRoute.data будет { two: 2 }, потому что в настоящее время ActivatedRoute не относится к пути path: '', и родитель ActivatedRoute принадлежит к маршруту componentless. Если бы поставили paramsInheritanceStrategy: 'always', то получили бы { one: 1, two: 2 }.

И наконец

[
  {
    path: 'foo/:id',
    children: [
      {
        path: 'a/:name',
        children: [
          { 
            path: 'b', 
            component: ComponentB, 
            children: [ { path: 'c', component: ComponentC } ]
          }
        ]
      }
    ]
  }
]

После перехода к foo/123/a/andrei/b/c объекту ComponentB ActivatedRoute будет присвоено params значение { id: 123, name: 'andrei' } (его родительский элемент принадлежит к бескомпонентному маршруту, а родительский элемент его родительского элемента делает то же самое), тогда как ComponentC ActivatedRoute будет иметь params значение {}, поскольку маршрут, которому он принадлежит, имеет path: 'c', а родительский ActivatedRoute не относится к маршруту componentless.

Параметр queryParamsHandling

  1. показать подсказку о том, как повторно использовать одно и то же представление (без перезагрузки страницы), но с разными queryParams

Этот параметр может быть указан как свойство в директиве RouterLink или директиве RouterLinkWithRef и может принимать два значения: merge или preserve.

Все примеры можно найти в этой демонстрации StackBlitz.

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

<!-- assuming the current route has `k1='v1'` -->
<!-- after clicking the button, the same component will be used(without being reloaded) -->
<!-- but the `queryParams` this time will be those written below -->
<button [queryParams]="{ k2: 'v2', k1: 'foo-value-refreshed' }" [routerLink]="[]">...</button>

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

Одна из интересных вещей @angular/router - это количество функций и настроек, которые он нам предоставляет. Одна из них - опция runGuardsAndResolvers, которую можно использовать в объекте конфигурации Route:

export type RunGuardsAndResolvers =
    'pathParamsChange'|'pathParamsOrQueryParamsChange'|'paramsChange'|'paramsOrQueryParamsChange'|
    'always'|((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);

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

// runGuardsAndResolvers: RunGuardsAndResolvers = 'paramsChange' (the default value)
[
  {
    path: 'a',
    runGuardsAndResolvers,
    component: /* ... */,
    canActivate: ['guard'],
    resolve: {data: 'resolver'}
  },
]

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

Вот несколько примеров и логика, определяющая тестовый пример:

router.navigateByUrl('/a');
const cmp = /* ... */; // the component associated with the `path: 'a'` route
const recordedData: any[] = [];
cmp.route.data.subscribe((data: any) => recordedData.push(data)); // the values will be of type: `{ data: counterValue }`

runGuardsAndResolvers = `paramsChange` // run guards & resolvers when either `positional params` or `matrix params` change

// since the first navigation already occurred, the resolver function was invoked once
expect(recordedData).toEqual([{data: 0}]);

// although it's the same URL, the matrix params are different, so the guards and resolvers will be invoked once again
router.navigateByUrl('/a;p=1');
expect(recordedData).toEqual([{data: 0}, {data: 1}]);

// same case the previous one
router.navigateByUrl('/a;p=2');
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);

router.navigateByUrl('/a;p=2?q=1');
// this time, nothing is changed, because only the `queryParams` have changed, but not the params
// this would've worked if `runGuardsAndResolvers` was set to `paramsOrQueryParamsChange`
// so, `paramsOrQueryParamsChange` = `paramsChange` | `queryParamsChange`
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);

Поначалу эта опция pathParamsChange может показаться немного запутанной, но, возможно, мы сможем прояснить ее на нескольких примерах:

// let's presume the counter has been reset 

// run guards & resolvers when only the positional params change
// under the hood its just comparing the URLs of 2 `ActivatedRouteSnapshot` nodes that have the same route config. object
runGuardsAndResolvers = 'pathParamsChange'

router.navigateByUrl('/a');

// `pathParamsChange` implies something like `a/1 !== a/2`
// changing any optional(matrix) params will not result in running guards or resolvers
router.navigateByUrl('/a;p=1');
expect(recordedData).toEqual([{data: 0}]);

router.navigateByUrl('/a;p=2');
expect(recordedData).toEqual([{data: 0}]);

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

// let's presume the counter has been reset 

runGuardsAndResolvers = 'pathParamsOrQueryParamsChange'

router.navigateByUrl('/a');

// changing matrix params will not result in running guards or resolvers
router.navigateByUrl('/a;p=1');
expect(recordedData).toEqual([{data: 0}]);

router.navigateByUrl('/a;p=2');
expect(recordedData).toEqual([{data: 0}]);

// adding query params will re-run guards/resolvers
router.navigateByUrl('/a;p=2?q=1');
expect(recordedData).toEqual([{data: 0}, {data: 1}]);

Вывод

В этой статье мы рассмотрели многие полезные функции @angular/router. Я надеюсь, что смог ответить на некоторые вопросы, которые у вас могли возникнуть об этом пакете, и пролить свет на то, почему он такой мощный.

Источник:

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу