Angular 教程:为英雄之旅添加路由支持-里程碑 4:危机中心
里程碑 4:危机中心
本节将向你展示如何在应用中添加子路由并使用相对路由。
要为应用当前的危机中心添加更多特性,请执行类似于 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:
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' },
];
最终的危机中心可以作为引入子路由这个新概念的基础。你可以把英雄管理保持在当前状态,以便和危机中心进行对比。
遵循关注点分离(Separation of Concerns)原则,对危机中心的修改不会影响
AppModule
或其它特性模块中的组件。
带有子路由的危机中心
本节会展示如何组织危机中心,来满足 Angular 应用所推荐的模式:
- 把每个特性放在自己的目录中
- 每个特性都有自己的 Angular 特性模块
- 每个特性区都有自己的根组件
- 每个特性区的根组件中都有自己的路由出口及其子路由
- 特性区内的路由很少(也许永远不会)与其它特性区的路由产生交叉
如果你的应用具有多个特性区,那些特性的组件树可能由多个组件构成,每个都包含一些其它相关组件的分支。
子路由组件
在 crisis-center
目录下生成一个 CrisisCenter
组件:
ng generate component crisis-center/crisis-center
使用如下代码更新组件模板:
<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
中的模板。
<h3>Welcome to the Crisis Center</h3>
把 heroes-routing.module.ts
文件复制过来,改名为 crisis-center-routing.module.ts
,并修改它。这次你要把子路由定义在父路由 crisis-center
中。
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
及其导入语句。
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
前面:
- 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 {}
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 { }
这些模块的导入顺序是至关重要的,因为这些模块中定义的路由的顺序会影响路由的匹配顺序。如果先导入
AppModule
,它的通配符路由 (path: '**'
)。
从 app.routing.ts
中移除危机中心的初始路由。因为现在是 HeroesModule
和 CrisisCenter
模块提供了这些特性路由。
app-routing.module.ts
文件中只有应用的顶层路由,比如默认路由和通配符路由。
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()
方法,来使用相对路径返回危机中心列表。
// 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”的出口,就在无名出口的下方。
<div [@routeAnimation]="getAnimationData()">
<router-outlet></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
一旦你学会了如何把一个弹出框组件路由到该出口,那里就是将会出现弹出框的地方。
第二路由
命名出口是第二路由的目标。
第二路由很像主路由,配置方式也一样。它们只有一些关键的不同点。
- 它们彼此互不依赖
- 它们与其它路由组合使用
- 它们显示在命名出口中
生成一个新的组件来组合这个消息。
ng generate component compose-message
它显示一个简单的表单,包括一个头、一个消息输入框和两个按钮:“Send”和“Cancel”。
下面是该组件及其模板和样式:
- src/app/compose-message/compose-message.component.html
<h3>Contact Crisis Center</h3>
<div *ngIf="details">
{{ details }}
</div>
<div>
<div>
<label for="message">Enter your message: </label>
</div>
<div>
<textarea id="message" [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea>
</div>
</div>
<p *ngIf="!sending">
<button type="button" (click)="send()">Send</button>
<button type="button" (click)="cancel()">Cancel</button>
</p>
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 = '';
message = '';
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 }}]);
}
}
textarea {
width: 100%;
margin-top: 1rem;
font-size: 1.2rem;
box-sizing: border-box;
}
它看起来几乎和你以前见过其它组件一样,但有两个值得注意的区别。
注意:
send()
方法通过在“发送”消息之前等待一秒并关闭弹出窗口来模拟延迟。
closePopup()
方法用把 popup
出口导航到 null
的方式关闭了弹出框,它在稍后的部分有讲解。
添加第二路由
打开 AppRoutingModule
,并把一个新的 compose
路由添加到 appRoutes
中。
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'popup'
},
除了 path
和 component
属性之外还有一个新的属性 outlet
,它被设置成了 'popup'
。这个路由现在指向了 popup
出口,而 ComposeMessageComponent
也将显示在那里。
为了给用户某种途径来打开这个弹出框,还要往 AppComponent
模板中添加一个“Contact”链接。
<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()
方法:
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
。