Route 子路由

2020-07-07 16:30 更新

本节将向你展示如何在应用中添加子路由并使用相对路由。

要为应用当前的危机中心添加更多特性,请执行类似于 heroes 特性的步骤:

  • 在 src/app 目录下创建一个 crisis-center 子目录。

  • 把 app/heroes 中的文件和目录复制到新的 crisis-center 文件夹中。

  • 在这些新建的文件中,把每个 "hero" 都改成 "crisis",每个 "heroes" 都改成 "crises"。

  • 把这些 NgModule 文件改名为 crisis-center.module.ts 和 crisis-center-routing.module.ts。

使用 mockcrises 来代替 mockheroes

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>

CrisisCenterComponentAppComponent 有下列共同点:

它是危机中心特性区的根,正如 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 数组。

这两个路由分别导航到了危机中心的两个子组件:CrisisCenterHomeComponentCrisisDetailComponent

对这些子路由的处理中有一些重要的差异。

路由器会把这些路由对应的组件放在 CrisisCenterComponentRouterOutlet 中,而不是 AppComponent 壳组件中的。

CrisisListComponent 包含危机列表和一个 RouterOutlet,用以显示 Crisis Center HomeCrisis 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 添加到 AppModuleimports 数组中,就在 AppRoutingModule 前面:

  1. 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 {}

  1. 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" 中移除危机中心的初始路由。 因为现在是 HeroesModuleCrisisCenter 模块提供了这些特性路由。

"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` 指令中。

修改 CrisisDetailComponentgotoCrises() 方法,来使用相对路径返回危机中心列表。

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>

一旦你学会了如何把一个弹出框组件路由到该出口,那里就是将会出现弹出框的地方。

  1. 第二路由。

命名出口是第二路由的目标。

第二路由很像主路由,配置方式也一样。它们只有一些关键的不同点:

  • 它们彼此互不依赖。

  • 它们与其它路由组合使用。

  • 它们显示在命名出口中。

生成一个新的组件来组合这个消息。

    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 的方式关闭了弹出框,它在稍后的部分有讲解。

  1. 添加第二路由。

打开 AppRoutingModule,并把一个新的 compose 路由添加到 appRoutes 中。

Path:"src/app/app-routing.module.ts (compose route)" 。

    {
      path: 'compose',
      component: ComposeMessageComponent,
      outlet: 'popup'
    },

除了 pathcomponent 属性之外还有一个新的属性 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 指令中为多个路由出口指定多个路由。

  1. 第二路由导航:在导航期间合并路由

导航到危机中心并点击“Contact”,你将会在浏览器的地址栏看到如下 URL:

    http://.../crisis-center(popup:compose)

这个 URL 中有意义的部分是 ... 后面的这些:

  • "crisis-center" 是主导航。

  • 圆括号包裹的部分是第二路由。

  • 第二路由包括一个出口名称(popup)、一个冒号分隔符和第二路由的路径(compose)。

点击 Heroes 链接,并再次查看 URL

    http://.../heroes(popup:compose)

主导航的部分变化了,而第二路由没有变。

路由器在导航树中对两个独立的分支保持追踪,并在 URL 中对这棵树进行表达。

你还可以添加更多出口和更多路由(无论是在顶层还是在嵌套的子层)来创建一个带有多个分支的导航树。 路由器将会生成相应的 URL

通过像前面那样填充 outlets 对象,你可以告诉路由器立即导航到一棵完整的树。 然后把这个对象通过一个链接参数数组传给 router.navigate 方法。

  1. 清除第二路由。

像常规出口一样,二级出口会一直存在,直到你导航到新组件。

每个第二出口都有自己独立的导航,跟主出口的导航彼此独立。 修改主出口中的当前路由并不会影响到 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' 的值是 nullnull 不是一个路由,但却是一个合法的值。 把 popup 这个 RouterOutlet 设置为 null 会清除该出口,并且从当前 URL 中移除第二路由 popup

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号