首页 > 解决方案 > 为什么RouterLink将输入添加到括号中当前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

标签: angularangular-routingangular-routerlink

解决方案


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

让我们看看当您单击具有指令的元素时会发生什么:RouterLink

@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, {});
}

注意:本演练基于此ng-run 演示


URL 可以具有以下结构:segment?queryParams#fragment.

AnUrlSegmentGroup可以有一个数组UrlSegments和一个子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的孩子由 分隔()。这些孩子的名字就是路由器插座

/(a//named:b),因为它使用了一个/before (a将segment of the primary outlet//是路由器插座的分隔符。最后,named:b遵循这个结构:outletName:segmentPath.

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

因此,anUrlTree有 3 个重要属性:发出的 URL 的root UrlSegmentGroupqueryParams对象和fragment


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),它将相对于 root ActivatedRoute

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


推荐阅读