为什么 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 UrlSegmentGroup
s:
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
.
另一件应该提到的事情是 UrlSegment
的 parameters
属性。除了 位置参数 (例如 foo/:a/:b
),段可以有这样声明的参数:segment/path;k1=v1;k2=v2
;
因此,UrlTree
有 3 个重要属性:root
UrlSegmentGroup
、queryParams
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']"
假设我的 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 UrlSegmentGroup
s:
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
.
另一件应该提到的事情是 UrlSegment
的 parameters
属性。除了 位置参数 (例如 foo/:a/:b
),段可以有这样声明的参数:segment/path;k1=v1;k2=v2
;
因此,UrlTree
有 3 个重要属性:root
UrlSegmentGroup
、queryParams
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']"