快訊!我的新書今天開始可以在天瓏網路書店預購啦!歡迎大家前往訂購!

 >>>> AI 職場超神助手:ChatGPT 與生成式 AI 一鍵搞定工作難題 <<<<

[功能介紹-15] Router進階介紹

設定Router Module

若我們的Router設定較為複雜時,可將Router配置為一個Router Module。

首先,設定一個路由模組app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes',        component: HeroListComponent },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  &#93;,
  exports: &#91;
    RouterModule
  &#93;
})
export class AppRoutingModule {}&#91;/code&#93;
接著,更新<code>app.module.ts</code>文件
[code lang="js"]import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

多層的Router Module

我們在做網站時,會希望每個模組專注在自己的功能上,Router也不例外。因此像英雄列表的元件,如果希望點選列表內容,可以進入該英雄詳細資料的頁面如下:

首先,創建一個pre-routing檔案heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

新增一個heroes-routing.module.ts,內容如下:

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

將Router Module的檔案放在與其伴隨的模組在相同的文件夾中。這兩個heroes-routing.module.tsheroes.module.ts在同一個src/app/heroes文件夾中。

考慮給每個功能模組自己的路由配置文件。當功能路線很簡單時,看起來可能會過早。但是,路線趨於變得更加複雜,模式的一致性會隨著時間的推移而得到回報。

接著在heroes.module.ts裡導入HeroRoutingModule

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

import { HeroRoutingModule } from './heroes-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

path: 'heroes'目前定義在兩個地方:HeroesRoutingModule和AppRoutingModule。
由功能模塊提供的route由Router合併到其導入的模塊的route中。這使您可以繼續定義功能模塊路由,而無需修改主路由配置。

如果不想設定兩次相同的資料,可以把舊的Router設定移除

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
// import { HeroListComponent }  from './hero-list.component';  // <-- delete this line
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = &#91;
  { path: 'crisis-center', component: CrisisListComponent },
  // { path: 'heroes',     component: HeroListComponent }, // <-- delete this line
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
&#93;;

@NgModule({
  imports: &#91;
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  &#93;,
  exports: &#91;
    RouterModule
  &#93;
})
export class AppRoutingModule {}&#91;/code&#93;

在應用程式裡面使用AppRoutingModule,打開<code>src/app/app.module.ts</code>:
[code lang="js"]import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule }     from './heroes/heroes.module';

import { CrisisListComponent }   from './crisis-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

這邊要注意的是模塊導入順序很重要

當所有的路由都在AppRoutingModule時,我們將通用path: '**'符號的設定放在路由/heroes的後面,這樣路由器才有機會匹配一個URL到/heroes路由,而不會先被AppRoutingModule的這個設定匹配走。

{ path: '**', component: PageNotFoundComponent }

如果Routing在兩個不同的模組內,也是要先載入HeroesModule再載入AppRoutingModule,這樣才會使用HeroesModule裡的路由設定而不會因為在AppRoutingModule找到匹配的通用符號而顯示PageNotFoundComponent。
如果將import的順序倒過來,則會顯示PageNotFoundComponent的頁面。

在程式裡去操控路由器

使用this.router.navigate來在程式內操作路由工作

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

若需要傳一個參數,則可以用

gotoHeroes() {
  this.router.navigate(['/hero', hero.id]);
}

要傳送兩個參數則如下

gotoHeroes(hero: Hero) {
  this.router.navigate(['/hero', { id: hero.id, foo: 'foo' }]);
}

點下後出現的連結如下
localhost:3000/heroes;id=15;foo=foo
這個叫做Matrix URL,在URL查詢字符串中,用分號隔開“;” 不同的參數,而不是用&

取得Router的參數

export class HeroListComponent implements OnInit {
  heroes$: Observable<hero&#91;&#93;>;

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.heroes$ = this.route.paramMap
      .switchMap((params: ParamMap) => {
        // (+) before `params.get()` turns the string into a number
        this.selectedId = +params.get('id');
        return this.service.getHeroes();
      });
  }
}

添加換頁效果

首先要載入BrowserAnimationsModule這個動態效果模組。

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  imports: [
    BrowserAnimationsModule

animations.ts在根src/app/文件夾中創建一個文件。內容如下所示:

import { animate, AnimationEntryMetadata, state, style, transition, trigger } from '@angular/core';

// Component transition animations
export const slideInDownAnimation: AnimationEntryMetadata =
  trigger('routeAnimation', [
    state('*',
      style({
        opacity: 1,
        transform: 'translateX(0)'
      })
    ),
    transition(':enter', [
      style({
        opacity: 0,
        transform: 'translateX(-100%)'
      }),
      animate('0.2s ease-in')
    ]),
    transition(':leave', [
      animate('0.5s ease-out', style({
        opacity: 0,
        transform: 'translateY(100%)'
      }))
    ])
  ]);

然後在src/app/heroes/hero-detail.component.ts增加使用這個動態效果的綁定

@HostBinding('@routeAnimation') routeAnimation = true;
@HostBinding('style.display')   display = 'block';
@HostBinding('style.position')  position = 'absolute';

Child routing component

子路由元件與直接使用參數去定義路由,Router預設的狀況下,若瀏覽器重新導航到相同的元件時,會重新使用該元件既有的實體,而不會重新創建。因此在物件被重用的狀況下,該元件的ngOnInit只會被呼叫一次,即使是要顯示不同的內容資料。
但是被創建的元件實體會在離開頁面時被銷毀並取消註冊,因此在前面範例的heroes-routing.module.ts檔案中裡,所使用的導航設定方式,由於在瀏覽HeroDetailComponent之後,一定要先回到HeroListComponent,才能進入下一個Detail頁面。

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

這會造成因為在回到HeroListComponent時已把HeroDetailComponent刪除掉了,再選擇另一個英雄要查看細節時,又會再創立一個新的HeroDetailComponent。因此每次選擇不同的英雄時,組件都會重新創建

如果想要保留頁面的狀態,就可以改使用子路由的方式來定義

下面是使用子路由的範例:

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

因為CrisisDetailComponentCrisisCenterHomeComponent都是CrisisListComponent的子組件,因此在不同的子組件內的狀態得以被保存,直到頁面切換離開CrisisCenterComponent時才會將元件實體刪除,這可以讓我們在瀏覽不同CrisisDetail時,得以使用到Router預設的重用設定。

使用相對路徑

./ 是在目前的位置。
../ 在上一層的位置。
下面為使用範例

// Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

網址認證功能

很多時候某些頁面或許需要登入才可以檢視,有些要登入後具備某些身份才可以開啟該網址,這時候就可以用到Router的CanActivate功能。

下面是src/app/auth-guard.service.ts的內容:

import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

下面是src/app/admin/admin-routing.module.ts的內容

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

本篇所有範例可見:live demo / download example.

參考資料


17年資歷女工程師,專精於動畫、影像辨識以及即時串流程式開發。經常組織活動,邀請優秀的女性分享她們的技術專長,並在眾多場合分享自己的技術知識,也活躍於非營利組織,辦理活動來支持特殊兒及其家庭。期待用技術改變世界。

如果你認同我或想支持我的努力,歡迎請我喝一杯咖啡!讓我更有動力分享知識!