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。

免费 AI IDE


更多建议: