为什么 RouterLink 将输入添加到括号中当前 URL 的末尾

Why RouterLink adds the input to the end of current URL in bracket

假设我的 url 是:http://localhost:4200/user_id/home。这是我的按钮代码:

  <ion-button [routerLink]="['some_user_id', 'payments']" routerLinkActive="selected">
    <ion-label class="label">Payments</ion-label>
  </ion-button>

因为我得到 Error: Cannot match any routes. 我开始调查这个问题,然后我发现 routerLink 正在生成这样的 DOM 元素:

<a href="user_id/home/(some_user_id/payments)" class="button-native" part="native">

当(在同一组件中)我使用路由器导航时,例如:

this.router.navigate('some_user_id', 'payments'])

一切正常。

生成的 href 不只是 <a href="some_user_id/payments" class="button-native" part="native"> as allways 的问题是什么?

这是因为 routerLink 是一个指令,它在幕后做了一些其他的事情。

当您单击具有 RouterLink 指令的元素时,让我们看看 what happens

@Directive({selector: ':not(a):not(area)[routerLink]'})
export class RouterLink {
  /* ... */

  @HostListener('click')
  onClick(): boolean {
    const extras = {
      skipLocationChange: attrBoolValue(this.skipLocationChange),
      replaceUrl: attrBoolValue(this.replaceUrl),
      state: this.state,
    };
    this.router.navigateByUrl(this.urlTree, extras);
    return true;
  }

  get urlTree(): UrlTree {
    return this.router.createUrlTree(this.commands, {
      relativeTo: this.route, // !
      queryParams: this.queryParams,
      fragment: this.fragment,
      preserveQueryParams: attrBoolValue(this.preserve),
      queryParamsHandling: this.queryParamsHandling,
      preserveFragment: attrBoolValue(this.preserveFragment),
    });
  }

  /* ... */
}

关注 relativeTo: this.route,其中 this.route 指向当前 ActivatedRoute(例如与 /home 关联的那个)。

Router.createUrlTree所做的是将一组commands应用于当前的URL树,这将产生一个新的URL树。在您的情况下,commands['some_user_id', 'payments']

createUrlTree(commands: any[], navigationExtras: NavigationExtras = {}): UrlTree {
  const {
    relativeTo,
    queryParams,
    fragment,
    preserveQueryParams,
    queryParamsHandling,
    preserveFragment
  } = navigationExtras;
  
  /* .... */

  const a = relativeTo || this.routerState.root;
  const f = preserveFragment ? this.currentUrlTree.fragment : fragment;
  let q: Params|null = null;
  
  /* ... resolving query params based on the `queryParamsHandling` strategy */

  return createUrlTree(a, this.currentUrlTree, commands, q!, f!);
}

createUrlTree 是魔法发生的地方:

export function createUrlTree(
    route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params,
    fragment: string): UrlTree {
  // `route` - that one which corresponds to `/home`
  // `commends` - `['some_user_id', 'payments']`
  // `urlTree` - a tree of UrlSegmentGroups, we'll have a closer look a bit later

  if (commands.length === 0) { /* Not our case */ }

  /* 
  a command might also be one of these objects: 
    * { outlets: { outletName: path } }
    * { k1: v1, k2: v2 } - segment parameters
    * { segmentPath: path }

    but in this case, it will simply be a Navigation object {
      isAbsolute: false,
      numberOfDoubleDots: 0,
      commands: ['some_user_id', 'payments']
    }
  */
  const nav = computeNavigation(commands);

  if (nav.toRoot()) { 
    /* Not our case;  */ 
    /* It would've been if: this.isAbsolute && this.commands.length === 1 && this.commands[0] == '/' */
  }

  /* 
  We'd get a new `Position` object: `return new Position(g, false, ci - dd);`
  where `dd` - number of double dots = 0 and `ci` - current index = 1
  why is it 1? - https://github.com/angular/angular/blob/master/packages/router/src/create_url_tree.ts#L160
  */
  const startingPosition = findStartingPosition(nav, urlTree, route);

  const segmentGroup = startingPosition.processChildren ?
      updateSegmentGroupChildren(
          startingPosition.segmentGroup, startingPosition.index, nav.commands) :
      updateSegmentGroup(startingPosition.segmentGroup, startingPosition.index, nav.commands);
  return tree(startingPosition.segmentGroup, segmentGroup, urlTree, queryParams, fragment);
}

segmentGroup 将是 updateSegmentGroup 的结果。它最终会达到 createNewSegmentGroup:

function createNewSegmentGroup(
    segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
  // Everything before the `startIndex`
  const paths = segmentGroup.segments.slice(0, startIndex); 

  let i = 0;
  while (i < commands.length) {
    if (typeof commands[i] === 'object' && commands[i].outlets !== undefined) {
      /* Not our case */
    }

    // if we start with an object literal, we need to reuse the path part from the segment
    // That's why the `modifier` is 1 if there are no parameters: https://github.com/angular/angular/blob/master/packages/router/src/create_url_tree.ts#L160
    if (i === 0 && isMatrixParams(commands[0])) {
      const p = segmentGroup.segments[startIndex];
      paths.push(new UrlSegment(p.path, commands[0]));
      i++;
      continue;
    }

    const curr = getPath(commands[i]);
    const next = (i < commands.length - 1) ? commands[i + 1] : null;
    if (curr && next && isMatrixParams(next)) {
      paths.push(new UrlSegment(curr, stringify(next)));
      i += 2;
    } else {

      // Adding the commands(`['some_user_id', 'payments']`) the the previous segments
      // Which explains why you're getting the current behavior
      paths.push(new UrlSegment(curr, {}));
      i++;
    }
  }
  return new UrlSegmentGroup(paths, {});
}

注意:这个walk-through是基于这个ng-run demo的。


一个URL可以有这样的结构:segment?queryParams#fragment.

一个 UrlSegmentGroup 可以有一个 UrlSegments 的数组和一个 child 的 object UrlSegmentGroups:

export class UrlSegmentGroup {
  /* ... */

  parent: UrlSegmentGroup|null = null;

  constructor(
      public segments: UrlSegment[],
      public children: {[key: string]: UrlSegmentGroup}) {
    forEach(children, (v: any, k: any) => v.parent = this);
  }

  /* ... */
}

例如,我们可能有更复杂的URL,例如foo/123/(a//named:b)。结果 UrlSegmentGroup 将是这样的:

{
  segments: [], // The root UrlSegmentGroup never has any segments
  children: {
    primary: {
      segments: [{ path: 'foo', parameters: {} }, { path: '123', parameters: {} }],
      children: {
        primary: { segments: [{ path: 'a', parameters: {} }], children: {} },
        named: { segments: [{ path: 'b', parameters: {} }], children: {} },
      },
    },
  },
}

这将匹配这样的路由配置:

{
  {
    path: 'foo/:id',
    loadChildren: () => import('./foo/foo.module').then(m => m.FooModule)
  },

  // foo.module.ts
  {
    path: 'a',
    component: AComponent,
  },
  {
    path: 'b',
    component: BComponent,
    outlet: 'named',
  },
}

您可以在此 StackBlitz.

中试验此示例

如你所见,UrlSegmentGroup的children由()分隔。这些children的名字是router outlet.

/(a//named:b)中,因为它在(之前使用了一个/a将是主要出口的一部分. // 是路由器出口的分隔符。最后,named:b 遵循以下结构:outletName:segmentPath.

另一件应该提到的事情是 UrlSegmentparameters 属性。除了 位置参数 (例如 foo/:a/:b),段可以有这样声明的参数:segment/path;k1=v1;k2=v2;

因此,UrlTree 有 3 个重要属性:root UrlSegmentGroupqueryParams object 和 fragment发出 URL.


this.router.navigate('some_user_id', 'payments']) 有效,因为 Router.navigate 最终会调用 Router.createUrlTree:

navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
    Promise<boolean> {
  validateCommands(commands);
  return this.navigateByUrl(this.createUrlTree(commands, extras), extras);
}

然后,const a = relativeTo || this.routerState.root; 将在 Router.createUrlTree 内到达,并且由于没有 relativeTo(与 RouterLink 相对),它将相对于根 ActivatedRoute.

通过在第一个命令的开头添加 /,您可以获得与 routerLink 相同的行为:[routerLink]="['/some_user_id', 'payments']"