Route 子路由
本节将向你展示如何在应用中添加子路由并使用相对路由。
要为应用当前的危机中心添加更多特性,请执行类似于 heroes 特性的步骤:
- 在 src/app 目录下创建一个 crisis-center 子目录。
- 把 app/heroes 中的文件和目录复制到新的 crisis-center 文件夹中。
- 在这些新建的文件中,把每个 "hero" 都改成 "crisis",每个 "heroes" 都改成 "crises"。
- 把这些 NgModule 文件改名为 crisis-center.module.ts 和 crisis-center-routing.module.ts。
使用 mock
的 crises
来代替 mock
的 heroes
:
Path:"src/app/crisis-center/mock-crises.ts" 。
import { Crisis } from './crisis';
export const CRISES: Crisis[] = [
{ id: 1, name: 'Dragon Burning Cities' },
{ id: 2, name: 'Sky Rains Great White Sharks' },
{ id: 3, name: 'Giant Asteroid Heading For Earth' },
{ id: 4, name: 'Procrastinators Meeting Delayed Again' },
]
最终的危机中心可以作为引入子路由这个新概念的基础。 你可以把英雄管理保持在当前状态,以便和危机中心进行对比。
遵循关注点分离原则, 对危机中心的修改不会影响
AppModule
或其它特性模块中的组件。
带有子路由的危机中心
如何组织危机中心,来满足 Angular 应用所推荐的模式:
- 把每个特性放在自己的目录中。
- 每个特性都有自己的 Angular 特性模块。
- 每个特性区都有自己的根组件。
- 每个特性区的根组件中都有自己的路由出口及其子路由。
- 特性区的路由很少(或完全不)与其它特性区的路由交叉。
如果你还有更多特性区,它们的组件树是这样的:
子路由组件
在 crisis-center
目录下生成一个 CrisisCenter
组件:
ng generate component crisis-center/crisis-center
使用如下代码更新组件模板:
Path:"src/app/crisis-center/crisis-center/crisis-center.component.html" 。
<h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
CrisisCenterComponent
和 AppComponent
有下列共同点:
它是危机中心特性区的根,正如 AppComponent
是整个应用的根。
它是危机管理特性区的壳,正如 AppComponent
是管理高层工作流的壳。
就像大多数的壳一样,CrisisCenterComponent
类是最小化的,因为它没有业务逻辑,它的模板中没有链接,只有一个标题和用于放置危机中心的子组件的 <router-outlet>
。
子路由配置
在 crisis-center
目录下生成一个 CrisisCenterHome
组件,作为 "危机中心" 特性的宿主页面。
ng generate component crisis-center/crisis-center-home
用一条欢迎信息修改 Crisis Center
中的模板。
Path:"src/app/crisis-center/crisis-center-home/crisis-center-home.component.html" 。
<p>Welcome to the Crisis Center</p>
把 "heroes-routing.module.ts" 文件复制过来,改名为 "crisis-center-routing.module.ts",并修改它。 这次你要把子路由定义在父路由 crisis-center
中。
Path:"src/app/crisis-center/crisis-center-routing.module.ts (Routes)" 。
const crisisCenterRoutes: Routes = [
{
path: 'crisis-center',
component: CrisisCenterComponent,
children: [
{
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
]
}
];
注意,父路由 crisis-center
有一个 children
属性,它有一个包含 CrisisListComponent
的路由。 CrisisListModule
路由还有一个带两个路由的 children
数组。
这两个路由分别导航到了危机中心的两个子组件:CrisisCenterHomeComponent
和 CrisisDetailComponent
。
对这些子路由的处理中有一些重要的差异。
路由器会把这些路由对应的组件放在 CrisisCenterComponent
的 RouterOutlet
中,而不是 AppComponent
壳组件中的。
CrisisListComponent
包含危机列表和一个 RouterOutlet
,用以显示 Crisis Center Home
和 Crisis Detail
这两个路由组件。
Crisis Detail
路由是 Crisis List
的子路由。由于路由器默认会复用组件,因此当你选择了另一个危机时,CrisisDetailComponent
会被复用。 作为对比,回头看看 Hero Detail
路由,每当你从列表中选择了不同的英雄时,都会重新创建该组件。
在顶层,以 /
开头的路径指向的总是应用的根。 但这里是子路由。 它们是在父路由路径的基础上做出的扩展。 在路由树中每深入一步,你就会在该路由的路径上添加一个斜线 /
(除非该路由的路径是空的)。
如果把该逻辑应用到危机中心中的导航,那么父路径就是 "/crisis-center"。
要导航到 CrisisCenterHomeComponent
,完整的 URL
是 /crisis-center (/crisis-center + '' + '')。
要导航到 CrisisDetailComponent
以展示 id=2
的危机,完整的 URL
是 /crisis-center/2 (/crisis-center + '' + '/2')。
本例子中包含站点部分的绝对 URL
,就是:
localhost:4200/crisis-center/2
这里是完整的 "crisis-center.routing.ts" 及其导入语句。
Path:"src/app/crisis-center/crisis-center-routing.module.ts (excerpt)" 。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
const crisisCenterRoutes: Routes = [
{
path: 'crisis-center',
component: CrisisCenterComponent,
children: [
{
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
]
}
];
@NgModule({
imports: [
RouterModule.forChild(crisisCenterRoutes)
],
exports: [
RouterModule
]
})
export class CrisisCenterRoutingModule { }
把危机中心模块导入到 AppModule 的路由中
就像 HeroesModule
模块中一样,你必须把 CrisisCenterModule
添加到 AppModule
的 imports
数组中,就在 AppRoutingModule
前面:
- Path:"src/app/crisis-center/crisis-center.module.ts" 。
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
import { CrisisCenterRoutingModule } from './crisis-center-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
CrisisCenterRoutingModule
],
declarations: [
CrisisCenterComponent,
CrisisListComponent,
CrisisCenterHomeComponent,
CrisisDetailComponent
]
})
export class CrisisCenterModule {}
- Path:"src/app/app.module.ts (import CrisisCenterModule)" 。
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ComposeMessageComponent } from './compose-message/compose-message.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
HeroesModule,
CrisisCenterModule,
AppRoutingModule
],
declarations: [
AppComponent,
PageNotFoundComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
从 "app.routing.ts" 中移除危机中心的初始路由。 因为现在是 HeroesModule
和 CrisisCenter
模块提供了这些特性路由。
"app-routing.module.ts" 文件中只有应用的顶层路由,比如默认路由和通配符路由。
Path:"src/app/app-routing.module.ts (v3)" 。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
const appRoutes: Routes = [
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true } // <-- debugging purposes only
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}
相对导航
虽然构建出了危机中心特性区,你却仍在使用以斜杠开头的绝对路径来导航到危机详情的路由。
路由器会从路由配置的顶层来匹配像这样的绝对路径。
你固然可以继续像危机中心特性区一样使用绝对路径,但是那样会把链接钉死在特定的父路由结构上。 如果你修改了父路径 "/crisis-center",那就不得不修改每一个链接参数数组。
通过改成定义相对于当前 URL
的路径,你可以把链接从这种依赖中解放出来。 当你修改了该特性区的父路由路径时,该特性区内部的导航仍然完好无损。
路由器支持在链接参数数组中使用“目录式”语法来为查询路由名提供帮助:
&
./
或 无前导斜线 形式是相对于当前级别的。
&
../
会回到当前路由路径的上一级。
&你可以把相对导航语法和一个祖先路径组合起来用。 如果不得不导航到一个兄弟路由,你可以用
../<sibling&
来回到上一级,然后进入兄弟路由路径中。
用 Router.navigate
方法导航到相对路径时,你必须提供当前的 ActivatedRoute
,来让路由器知道你现在位于路由树中的什么位置。
在链接参数数组后面,添加一个带有 relativeTo
属性的对象,并把它设置为当前的 ActivatedRoute
。 这样路由器就会基于当前激活路由的位置来计算出目标 URL
。
当调用路由器的
navigateByUrl()
时,总是要指定完整的绝对路径。
使用相对 URL 导航到危机列表
你已经注入了组成相对导航路径所需的 ActivatedRoute
。
如果用 RouterLink
来代替 Router
服务进行导航,就要使用相同的链接参数数组,不过不再需要提供 relativeTo
属性。 ActivatedRoute已经隐含在了
RouterLink` 指令中。
修改 CrisisDetailComponent
的 gotoCrises()
方法,来使用相对路径返回危机中心列表。
Path:"src/app/crisis-center/crisis-detail/crisis-detail.component.ts (relative navigation)" 。
// Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
注意这个路径使用了 ../
语法返回上一级。 如果当前危机的 id
是 3,那么最终返回到的路径就是 "/crisis-center/;id=3;foo=foo"。
用命名出口(outlet)显示多重路由
你决定给用户提供一种方式来联系危机中心。 当用户点击“Contact”按钮时,你要在一个弹出框中显示一条消息。
即使在应用中的不同页面之间切换,这个弹出框也应该始终保持打开状态,直到用户发送了消息或者手动取消。 显然,你不能把这个弹出框跟其它放到页面放到同一个路由出口中。
迄今为止,你只定义过单路由出口,并且在其中嵌套了子路由以便对路由分组。 在每个模板中,路由器只能支持一个无名主路由出口。
模板还可以有多个命名的路由出口。 每个命名出口都自己有一组带组件的路由。 多重出口可以在同一时间根据不同的路由来显示不同的内容。
在 AppComponent
中添加一个名叫 “popup” 的出口,就在无名出口的下方。
Path:"src/app/app.component.html (outlets)" 。
<div [@routeAnimation]="getAnimationData(routerOutlet)">
<router-outlet #routerOutlet="outlet"></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
一旦你学会了如何把一个弹出框组件路由到该出口,那里就是将会出现弹出框的地方。
- 第二路由。
命名出口是第二路由的目标。
第二路由很像主路由,配置方式也一样。它们只有一些关键的不同点:
- 它们彼此互不依赖。
- 它们与其它路由组合使用。
- 它们显示在命名出口中。
生成一个新的组件来组合这个消息。
ng generate component compose-message
它显示一个简单的表单,包括一个头、一个消息输入框和两个按钮:“Send”和“Cancel”。
下面是该组件及其模板和样式:
- Path:"src/app/compose-message/compose-message.component.css" 。
:host {
position: relative; bottom: 10%;
}
- Path:"src/app/compose-message/compose-message.component.html" 。
<h3>Contact Crisis Center</h3>
<div *ngIf="details">
{{ details }}
</div>
<div>
<div>
<label>Message: </label>
</div>
<div>
<textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea>
</div>
</div>
<p *ngIf="!sending">
<button (click)="send()">Send</button>
<button (click)="cancel()">Cancel</button>
</p>
- Path:"src/app/compose-message/compose-message.component.ts" 。
import { Component, HostBinding } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-compose-message',
templateUrl: './compose-message.component.html',
styleUrls: ['./compose-message.component.css']
})
export class ComposeMessageComponent {
details: string;
message: string;
sending = false;
constructor(private router: Router) {}
send() {
this.sending = true;
this.details = 'Sending Message...';
setTimeout(() => {
this.sending = false;
this.closePopup();
}, 1000);
}
cancel() {
this.closePopup();
}
closePopup() {
// Providing a `null` value to the named outlet
// clears the contents of the named outlet
this.router.navigate([{ outlets: { popup: null }}]);
}
}
它看起来几乎和你以前见过其它组件一样,但有两个值得注意的区别。
注意,send()
方法在发送消息和关闭弹出框之前通过等待模拟了一秒钟的延迟。
closePopup()
方法用把 popup
出口导航到 null
的方式关闭了弹出框,它在稍后的部分有讲解。
- 添加第二路由。
打开 AppRoutingModule
,并把一个新的 compose
路由添加到 appRoutes
中。
Path:"src/app/app-routing.module.ts (compose route)" 。
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'popup'
},
除了 path
和 component
属性之外还有一个新的属性 outlet
,它被设置成了 'popup'
。 这个路由现在指向了 popup 出口,而 ComposeMessageComponent
也将显示在那里。
为了给用户某种途径来打开这个弹出框,还要往 AppComponent
模板中添加一个“Contact”链接。
Path:"src/app/app.component.html (contact-link)" 。
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
虽然 compose
路由被配置到了 popup
出口上,但这仍然不足以把该路由和 RouterLink
指令联系起来。 你还要在链接参数数组中指定这个命名出口,并通过属性绑定的形式把它绑定到 `RouterLink 上。
链接参数数组包含一个只有一个 outlets
属性的对象,它的值是另一个对象,这个对象以一个或多个路由的出口名作为属性名。 在这里,它只有一个出口名“popup”,它的值则是另一个链接参数数组,用于指定 compose
路由。
换句话说,当用户点击此链接时,路由器会在路由出口 popup
中显示与 compose
路由相关联的组件。
当只需要考虑一个路由和一个无名出口时,外部对象中的这个
outlets
对象是完全不必要的。
路由器假设这个路由指向了无名的主出口,并为你创建这些对象。
路由到一个命名出口会揭示一个路由特性: 你可以在同一个
RouterLink
指令中为多个路由出口指定多个路由。
- 第二路由导航:在导航期间合并路由
导航到危机中心并点击“Contact”,你将会在浏览器的地址栏看到如下 URL:
http://.../crisis-center(popup:compose)
这个 URL 中有意义的部分是 ... 后面的这些:
- "crisis-center" 是主导航。
- 圆括号包裹的部分是第二路由。
- 第二路由包括一个出口名称(popup)、一个冒号分隔符和第二路由的路径(compose)。
点击 Heroes
链接,并再次查看 URL
:
http://.../heroes(popup:compose)
主导航的部分变化了,而第二路由没有变。
路由器在导航树中对两个独立的分支保持追踪,并在 URL
中对这棵树进行表达。
你还可以添加更多出口和更多路由(无论是在顶层还是在嵌套的子层)来创建一个带有多个分支的导航树。 路由器将会生成相应的 URL
。
通过像前面那样填充 outlets
对象,你可以告诉路由器立即导航到一棵完整的树。 然后把这个对象通过一个链接参数数组传给 router.navigate
方法。
- 清除第二路由。
像常规出口一样,二级出口会一直存在,直到你导航到新组件。
每个第二出口都有自己独立的导航,跟主出口的导航彼此独立。 修改主出口中的当前路由并不会影响到 popup
出口中的。 这就是为什么在危机中心和英雄管理之间导航时,弹出框始终都是可见的。
再看 closePopup()
方法:
Path:"src/app/compose-message/compose-message.component.ts (closePopup)" 。
closePopup() {
// Providing a `null` value to the named outlet
// clears the contents of the named outlet
this.router.navigate([{ outlets: { popup: null }}]);
}
单击 “send” 或 “cancel” 按钮可以清除弹出视图。closePopup()
函数会使用 Router.navigate()
方法强制导航,并传入一个链接参数数组。
就像在 AppComponent
中绑定到的 Contact RouterLink
一样,它也包含了一个带 outlets
属性的对象。 outlets
属性的值是另一个对象,该对象用一些出口名称作为属性名。 唯一的命名出口是 'popup'
。
但这次,'popup'
的值是 null
。null
不是一个路由,但却是一个合法的值。 把 popup
这个 RouterOutlet
设置为 null
会清除该出口,并且从当前 URL
中移除第二路由 popup
。
更多建议: