Angular 教程:為英雄之旅添加路由支持-里程碑 4:危機中心

2022-07-05 10:41 更新

里程碑 4:危機中心

本節(jié)將向你展示如何在應用中添加子路由并使用相對路由。

要為應用當前的危機中心添加更多特性,請執(zhí)行類似于 heroes 特性的步驟:

  • 在 ?src/app? 目錄下創(chuàng)建一個 ?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' },
];

最終的危機中心可以作為引入子路由這個新概念的基礎。你可以把英雄管理保持在當前狀態(tài),以便和危機中心進行對比。

遵循關注點分離(Separation of Concerns)原則,對危機中心的修改不會影響 ?AppModule ?或其它特性模塊中的組件。

帶有子路由的危機中心

本節(jié)會展示如何組織危機中心,來滿足 Angular 應用所推薦的模式:

  • 把每個特性放在自己的目錄中
  • 每個特性都有自己的 Angular 特性模塊
  • 每個特性區(qū)都有自己的根組件
  • 每個特性區(qū)的根組件中都有自己的路由出口及其子路由
  • 特性區(qū)內的路由很少(也許永遠不會)與其它特性區(qū)的路由產(chǎn)生交叉

如果你的應用具有多個特性區(qū),那些特性的組件樹可能由多個組件構成,每個都包含一些其它相關組件的分支。

子路由組件

在 ?crisis-center? 目錄下生成一個 ?CrisisCenter ?組件:

ng generate component crisis-center/crisis-center

使用如下代碼更新組件模板:

<h2>Crisis Center</h2>
<router-outlet></router-outlet>

?CrisisCenterComponent ?和 ?AppComponent ?有下列共同點:

  • 它是危機中心特性區(qū)的根,正如 ?AppComponent ?是整個應用的根
  • 它是危機管理特性區(qū)的殼,正如 ?AppComponent ?是管理高層工作流的殼

就像大多數(shù)的殼一樣,?CrisisCenterComponent ?類是最小化的,因為它沒有業(yè)務邏輯,它的模板中沒有鏈接,只有一個標題和用于放置危機中心的子組件的 ?<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 ?數(shù)組。

這兩個路由分別導航到了危機中心的兩個子組件:?CrisisCenterHomeComponent ?和 ?CrisisDetailComponent?。

對這些子路由的處理中有一些重要的差異。

路由器會把這些路由對應的組件放在 ?CrisisCenterComponent ?的 ?RouterOutlet ?中,而不是 ?AppComponent ?殼組件中的。

?CrisisListComponent ?包含危機列表和一個 ?RouterOutlet?,用以顯示 ?Crisis Center Home? 和 ?Crisis Detail? 這兩個路由組件。

?Crisis Detail? 路由是 ?Crisis List? 的子路由。由于路由器默認會復用組件,因此當你選擇了另一個危機時,?CrisisDetailComponent ?會被復用。 作為對比,回頭看看 ?Hero Detail? 路由,每當你從列表中選擇了不同的英雄時,都會重新創(chuàng)建該組件。

在頂層,以 ?/? 開頭的路徑指向的總是應用的根。但這里是子路由。它們是在父路由路徑的基礎上做出的擴展。在路由樹中每深入一步,你就會在該路由的路徑上添加一個斜線 ?/?(除非該路由的路徑是空的)。

如果把該邏輯應用到危機中心中的導航,那么父路徑就是 ?/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 ?數(shù)組中,就在 ?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 {}
  • 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 { }

這些模塊的導入順序是至關重要的,因為這些模塊中定義的路由的順序會影響路由的匹配順序。如果先導入 ?AppModule?,它的通配符路由 (?path: '**'?)。

從 ?app.routing.ts? 中移除危機中心的初始路由。因為現(xiàn)在是 ?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 {}

相對導航

雖然構建出了危機中心特性區(qū),你卻仍在使用以斜杠開頭的絕對路徑來導航到危機詳情的路由。

路由器會從路由配置的頂層來匹配像這樣的絕對路徑。

你固然可以繼續(xù)像危機中心特性區(qū)一樣使用絕對路徑,但是那樣會把鏈接釘死在特定的父路由結構上。如果你修改了父路徑 ?/crisis-center?,那就不得不修改每一個鏈接參數(shù)數(shù)組。

通過改成定義相對于當前 URL 的路徑,你可以把鏈接從這種依賴中解放出來。當你修改了該特性區(qū)的父路由路徑時,該特性區(qū)內部的導航仍然完好無損。

路由器支持在鏈接參數(shù)數(shù)組中使用“目錄式”語法來為查詢路由名提供幫助:

目錄式語法

詳情

./
無前導斜線

形式是相對于當前級別的。

../

回到當前路由路徑的上一級。

你可以把相對導航語法和一個祖先路徑組合起來用。如果不得不導航到一個兄弟路由,你可以用 ?../<sibling>? 來回到上一級,然后進入兄弟路由路徑中。

用 ?Router.navigate? 方法導航到相對路徑時,你必須提供當前的 ?ActivatedRoute?,來讓路由器知道你現(xiàn)在位于路由樹中的什么位置。

鏈接參數(shù)數(shù)組后面,添加一個帶有 ?relativeTo ?屬性的對象,并把它設置為當前的 ?ActivatedRoute?。這樣路由器就會基于當前激活路由的位置來計算出目標 URL。

當調用路由器的 ?navigateByUrl()? 時,總是要指定完整的絕對路徑。

使用相對 URL 導航到危機列表

你已經(jīng)注入了組成相對導航路徑所需的 ?ActivatedRoute?。

如果用 ?RouterLink ?來代替 ?Router ?服務進行導航,就要使用相同的鏈接參數(shù)數(shù)組,不過不再需要提供 ?relativeTo ?屬性。?ActivatedRoute ?已經(jīng)隱含在了 ?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)顯示多重路由

你決定給用戶提供一種方式來聯(lián)系危機中心。當用戶點擊“Contact”按鈕時,你要在一個彈出框中顯示一條消息。

即使在應用中的不同頁面之間切換,這個彈出框也應該始終保持打開狀態(tài),直到用戶發(fā)送了消息或者手動取消。顯然,你不能把這個彈出框跟其它放到頁面放到同一個路由出口中。

迄今為止,你只定義過單路由出口,并且在其中嵌套了子路由以便對路由分組。在每個模板中,路由器只能支持一個無名主路由出口。

模板還可以有多個命名的路由出口。每個命名出口都自己有一組帶組件的路由。多重出口可以在同一時間根據(jù)不同的路由來顯示不同的內容。

在 ?AppComponent ?中添加一個名叫“popup”的出口,就在無名出口的下方。

<div [@routeAnimation]="getAnimationData()">
  <router-outlet></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>

一旦你學會了如何把一個彈出框組件路由到該出口,那里就是將會出現(xiàn)彈出框的地方。

第二路由

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

第二路由很像主路由,配置方式也一樣。它們只有一些關鍵的不同點。

  • 它們彼此互不依賴
  • 它們與其它路由組合使用
  • 它們顯示在命名出口中

生成一個新的組件來組合這個消息。

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>
  • 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 = '';
      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 }}]);
      }
    }
  • src/app/compose-message/compose-message.component.css
  • textarea {
      width: 100%;
      margin-top: 1rem;
      font-size: 1.2rem;
      box-sizing: border-box;
    }

它看起來幾乎和你以前見過其它組件一樣,但有兩個值得注意的區(qū)別。

注意:
?send()? 方法通過在“發(fā)送”消息之前等待一秒并關閉彈出窗口來模擬延遲。

?closePopup()? 方法用把 ?popup ?出口導航到 ?null ?的方式關閉了彈出框,它在稍后的部分有講解。

添加第二路由

打開 ?AppRoutingModule?,并把一個新的 ?compose ?路由添加到 ?appRoutes ?中。

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

除了 ?path ?和 ?component ?屬性之外還有一個新的屬性 ?outlet?,它被設置成了 ?'popup'?。這個路由現(xiàn)在指向了 ?popup ?出口,而 ?ComposeMessageComponent ?也將顯示在那里。

為了給用戶某種途徑來打開這個彈出框,還要往 ?AppComponent ?模板中添加一個“Contact”鏈接。

<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>

雖然 ?compose ?路由被配置到了 ?popup ?出口上,但這仍然不足以把該路由和 ?RouterLink ?指令聯(lián)系起來。你還要在鏈接參數(shù)數(shù)組中指定這個命名出口,并通過屬性綁定的形式把它綁定到 ?RouterLink ?上。

鏈接參數(shù)數(shù)組包含一個只有一個 ?outlets ?屬性的對象,它的值是另一個對象,這個對象以一個或多個路由的出口名作為屬性名。在這里,它只有一個出口名“popup”,它的值則是另一個鏈接參數(shù)數(shù)組,用于指定 ?compose ?路由。

換句話說,當用戶點擊此鏈接時,路由器會在路由出口 ?popup ?中顯示與 ?compose ?路由相關聯(lián)的組件。

當只需要考慮一個路由和一個無名出口時,外部對象中的這個 ?outlets ?對象是完全不必要的。
路由器假設這個路由指向了無名的主出口,并為你創(chuàng)建這些對象。
路由到一個命名出口會揭示一個路由特性:你可以在同一個 ?RouterLink ?指令中為多個路由出口指定多個路由。

第二路由導航:在導航期間合并路由

導航到危機中心并點擊“Contact”,你將會在瀏覽器的地址欄看到如下 URL。

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

這個 URL 中有意義的部分是 ?...? 后面的這些:

  • ?crisis-center? 是主導航。
  • 圓括號包裹的部分是第二路由。
  • 第二路由包括一個出口名稱(?popup?)、一個冒號分隔符和第二路由的路徑(?compose?)。

點擊 Heroes 鏈接,并再次查看 URL。

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

主導航的部分變化了,而第二路由沒有變。

路由器在導航樹中對兩個獨立的分支保持追蹤,并在 URL 中對這棵樹進行表達。

你還可以添加更多出口和更多路由(無論是在頂層還是在嵌套的子層)來創(chuàng)建一個帶有多個分支的導航樹。路由器將會生成相應的 URL。

通過像前面那樣填充 ?outlets ?對象,你可以告訴路由器立即導航到一棵完整的樹。然后把這個對象通過一個鏈接參數(shù)數(shù)組傳給 ?router.navigate? 方法。

清除第二路由

像常規(guī)出口一樣,二級出口會一直存在,直到你導航到新組件。

每個第二出口都有自己獨立的導航,跟主出口的導航彼此獨立。修改主出口中的當前路由并不會影響到 ?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()? 函數(shù)會使用 ?Router.navigate()? 方法強制導航,并傳入一個鏈接參數(shù)數(shù)組。

就像在 ?AppComponent ?中綁定到的 Contact ?RouterLink ?一樣,它也包含了一個帶 ?outlets ?屬性的對象。 ?outlets ?屬性的值是另一個對象,該對象用一些出口名稱作為屬性名。 唯一的命名出口是 ?'popup'?。

但這次,?'popup'? 的值是 ?null?。?null ?不是一個路由,但卻是一個合法的值。把 ?popup ?這個 ?RouterOutlet ?設置為 ?null ?會清除該出口,并且從當前 URL 中移除第二路由 ?popup?。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號