[Angular學習紀錄] 路由機制

SPA(Single Page Application)的開發方式就是指透過一個 html 檔案來達成整個網站系統的任何操作,從頭到尾都不會離開此頁面,例如 Web Gmail 就是一個典型的 SPA 設計,回過頭來看一下 Angular 的基礎示範教學中也是使用 SPA 的方式,並透過屬性的判斷來決定使否載入某個 Template。

小型應用程式只有少數幾個頁面,使用此方式到還好,但假想一下有一個中大型應用程式有數十甚至數百個頁面需要切換,如果再像小型應用程式一樣透過屬性判斷的方式,後續整個程式碼可能會出現難以維護的情況,因此這種時候就需要運用 Angular 所提供的路由機制來處理此類的問題了。

什麼是路由? 簡單來說就是 Angular 會根據瀏覽器中的 URL,自動對應到要載入的視圖,這跟其它開發語言的路由,如 ASP.NET MVC 的概念是差不多的,差別在 Angular 透過路由機制來完成單一個網頁檔案切換視圖(SPA)的效果。

Angular 路由器是一個可選擇的外部 Angular NgModule,名叫RouterModule。 路由器包含了多種服務(RouterModule)、多種指令(RouterOutlet、RouterLink、RouterLinkActive)、 和一套配置(Routes),本文將說明如何建立 Angular 路由設定。


base href


路由器設定的必要條件,必須確保 base href 的存在,因此請打開 index.html,確保head標籤內是否有,設定<base href="/">。
<head>
 <base href="/">
</head>


RouterModule


路由模塊,由於路由器預設是不存在的,因此必須在 app.module.ts 內 import RouterModule 建立預設路由,你可以直接寫在 module 內,但一般來說會建立路由就是因為程式數量較多,因此原則上都至少會拆出一個路由,並獨立出一個檔案,在這裡我們命名為 app-routing.module.ts。

app.module.ts 匯入路由語法參考如下,注意一下註解的部分是原本寫在 module 內的樣子,本範例會將它獨立一個新的檔案 app-routing.module.ts。
@NgModule({
  imports: [
    BrowserModule,
    AppRoutingModule //底下的寫法與此方法相同,差別在將路由設定獨立出來而已
    // RouterModule.forRoot([
    //   { path: '', redirectTo: '/layout1', pathMatch: 'full' },
    //   { path: 'layout1', component: Layout1Component }
    // ])
  ]
  bootstrap: [AppComponent]
})
export class AppModule { }

路由器定義是一個樹狀的結構,路由內部還可以包含子路由,這裡先介紹一下幾個常見的路由屬性。
  • path : 定義名稱,路由機制會把URL內對應的此名稱轉換到對應的組件,注意path不能使用反斜線 ( / ) 開頭。
    • 若為空為預設路由,若URL網址匹配為空,則會轉向到此處的設定。
    • 之後若有接反斜線和分號 ( /: ),代表此路由必須帶入參數,否則會發生錯誤。
    • 若為 ** ,萬用路由,所有匹配不符合的項目都會轉向此設定,須放在最後面
  • redirectTo : 轉向到另外一個定義的名稱,使用該屬性時一定要加上pathMatch。
  • component : 路由名稱對應的組件名稱。

※ 請注意路由的轉址至多只會轉換一次,舉例來說 A 設定轉到 B,B 設定轉到 C,若從 A 進入只會轉到 B,並不會兩次轉址。

※ Angular 路由器的搜尋原則使用先匹配者優先的策略,若以匹配到符合則不會繼續進行底下的搜尋,這也解釋了為什麼官網中,預設路由總是放在最上方,而萬用路由一律放在所有路由的最底下。

※ 老舊的瀏覽器在當前位址的URL變化時總會往伺服器發送頁面請求,唯一的例外規則是:當這些變化位於“#”(被稱為“hash”)後面時不會發送。通過把應用內的路由URL拼接在#之後,路由器可以獲得這條“例外規則”帶來的優點,底下是兩種路由策略和說明。

  • PathLocationStrategy - 預設的策略,支持"HTML 5 pushState"風格。
    • http://localhost:4200/layout1
  • HashLocationStrategy - 支持"hash URL"風格。
    • http://localhost:4200/#/layout1

app-routing.module.ts 語法參考如下
const routes: Routes = [
  { path: '', redirectTo: '/layout1', pathMatch: 'full' },
  { path: 'layout1', component: Layout1Component },
  { path: 'layout2', component: Layout2Component },
  { path: 'layout3/:id', component: Layout3Component },
  { path: '**', redirectTo: '/layout1', pathMatch: 'full' }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      useHash: false,
    })],
  exports: [RouterModule]
})
export class AppRoutingModule { }



RouterOutlet


router-outlet,路由插座,這樣的翻譯好像不太好理解,白話文就是要讓 Template 顯示的地方,以<router-outlet></router-outlet>呈現。

app-component.html 語法參考如下
<fieldset>
  <legend>內容顯示區</legend>
  <router-outlet></router-outlet>
</fieldset>
路由插座除了可以定義一個以外,一樣可以定義兩個路由器,但另外一個路由器則必須要指定名稱,詳細說明可以參閱官網,下面說明一下第二路由的使用方式。

1. 在 Template 內定義第二路由的位置及定義名稱(name="pop"),app-component.html 語法參考如下。
<fieldset>
  <legend>內容顯示區</legend>
  <router-outlet></router-outlet>
</fieldset>
<fieldset>
  <legend>第二路由內容顯示區</legend>
  <router-outlet name="popup"></router-outlet>
</fieldset>

2. 在 routing 內指定第二路由的 outlet 為該定義的名稱,app-routing.module.ts語法參考如下。
{ path: 'layout2', component: Layout2Component, outlet: 'popup' } //增加oulet屬性,名稱為popup

3. 開啟第二路由的方式,app-component.html 語法參考如下。
<a [routerLink]="[{ outlets: { popup: ['layout2'] } }]">Layout2(顯示第二路由)</a>

4. 官網範例的第二路由的運用場景為開一個Pop視窗,因此若需隱藏該路由,layout2.component.ts 語法參考如下。
//關閉第二路由
onCloseRouter() {
 this.router.navigate([{ outlets: { popup: null }}]);
}



RouterLink


路由器的超連結路徑的寫法如下三種類型,請注意此處的超連結使用方式非傳統 href 的方式。

  • routerLink,在 Template 端寫死字串,適用於固定路由
  • [routerLink],在 Template 端透過屬性的傳遞,適用於動態路由
  • router.navigate,在 Component 程式端轉換,適用於動態路由某種事件觸發

app-component.html 語法參考如下
<a routerLink="/layout1">Layout1</a> 
<a routerLink="/layout2">Layout2</a> 
<a routerLink="/layout3/1983">Layout3(Template 死寫參數)</a> 
<a [routerLink]="['layout3' , '2010']">Layout3(Template 屬性帶參數)</a> 
<button (click)="goLayout3()">Layout3(Component 帶參數)</button>

app-component.ts 語法參考如下
還有多種取得路由傳遞的方式,這裡不一一說明,請自行參考範例檔內的語法。
export class AppComponent { 

  constructor(private route: ActivatedRoute, private router: Router) { }

  goLayout3() {
    this.router.navigate(['/layout3', '2013']);
  }
}

layout3.component.ts 語法參考如下(後端取得帶參數的路由的參數值)
還有多種取得路由參數的方式,這裡不一一說明,請自行參考範例檔內的語法。
constructor(private route: ActivatedRoute, private router: Router) { }

ngOnInit() {
 //取得外部傳入的參數
 this.route.params.subscribe(params => {
   this.id = params["id"];
 });
}




RouterLinkActive


Angular路由器提供了routerLinkActive指令,可以用它來為匹配中的路由超連結添加一個CSS類別,因此我們需要定義該CSS的樣式,本文範例中選中的路由連結會變成紅底字。

上文中有提到,路由為一個樹狀結構,因此這裡的routerLinkActive一樣會套用選中節點其所有父節點都會變成紅色,為了要解決該問題,必須在父節點內加上[routerLinkActiveOptions]="{exact : true}"。

app-component.html 語法參考如下
<a routerLink="/layout1" routerLinkActive="active" [routerLinkActiveOptions]="{exact : true}">Layout1</a> 
<a routerLink="/layout1/layout1-a" routerLinkActive="active">Layout1-A</a><br>
<a routerLink="/layout2" routerLinkActive="active">Layout2</a><br>

以上說明執行畫面如下參考




Child Routing Component



  • 把每個特性放在自己的目錄中。
  • 每個特性都有自己的Angular特性模塊。
  • 每個特性區都有自己的根組件。
  • 每個特性區的根組件中都有自己的路由出口及其子路由。
  • 特性區的路由很少(或完全不)與其它特性區的路由交叉。

如果我們有更多特性區,它們的組件樹是這樣的:



子路由模組,以上這些說明是擷取自官網的說明和圖片,若看不懂的話,簡單的說明就是,一群處理相同事情的功能可以獨立出來成為一個模組,而且該模組可以擁有自己的路由設定和路由插座,下面簡單的介紹一下子路由的配置,若需詳細的說明可自行參閱官網

1. 建立新的module和routing,這裡的範例為layout4,開啟console視窗,在專案目錄底下輸入 ng g m layout4 --routing,系統會協助我們建立 module 和 routing 兩個檔案(如下圖參考)。


2. 一樣在console視窗,建立兩個layout4A、layout4B,這兩個為稍後要當子路由模組的歸屬組件如下圖紅框處語法參考,此時 CLI 除了會幫我們產生檔案外,還會自動把組件一併加入layout4-module.ts當中。


3. 接下來就是定義子路由的樹狀結構了,可參考 layout4-routing.module.ts 內的語法定義,原則上路由的結構和上文提到結構都是類似的,只是這邊要特別留意一下children這個屬性定義該模組的子路由。
const routes: Routes = [
  {
    path: 'layout4',
    component : Layout4Component,
    children: [ //底下為子路由 
      { path: 'layout4-a', component: Layout4AComponent }, //實際訪問路徑為 /layout4/layout4-a
      { path: 'layout4-b', component: Layout4BComponent }  //實際訪問路徑為 /layout4/layout4-b
    ]
  }
];
 
@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class Layout4RoutingModule { }


4. 再來設定一下子路由要顯示的布局,可參考 app-component.html 內的語法。
<fieldset>
  <legend>子路由示範區</legend>
  <a routerLink="/layout4" routerLinkActive="active">Layout4</a> 
  <a routerLink="/layout4/layout4-a" routerLinkActive="active">Layout4-A</a> 
  <a routerLink="/layout4/layout4-b" routerLinkActive="active">Layout4-B</a> 
</fieldset>


5. 最重要的記得將新加入的模塊 Layout4Module 加入 app.module.ts 當中。
@NgModule({ 
  imports: [
    BrowserModule,
    Layout4Module, 
    AppRoutingModule
  ], 
  bootstrap: [AppComponent]
})
export class AppModule { }

6. 子路由範例示意如下圖



Child Routing Lazy Loading


應用程式發展越來越大的時候,而有些模組並不是被大部分的人所使用的時候,可能就需要使用到惰性加載的功能,當使用者實際使用到該模組的時候 Angular 才將該模組的檔案加載到瀏覽器中使用,繼續上面的子路由的例子,只需要調整幾個步驟就可以達成惰性加載的功能,底下簡單的惰性加載的配置。

1. 把 Layout4RoutingModule 的路徑 layout4 清除改成空字串,參考layout4-routing.module.ts。
const routes: Routes = [
  {
    path: '', //清除layout4字串
    component : Layout4Component,
    children: [ //底下為子路由
      { path: 'layout4-a', component: Layout4AComponent }, //實際訪問路徑為 /layout4/layout4-a
      { path: 'layout4-b', component: Layout4BComponent } //實際訪問路徑為 /layout4/layout4-b
    ]
  }
];


2. 在 AppRoutingModule 中增加一個 layout4的路由,並設定 loadChildren 屬性來加載 Layout4Module,參考app-routing.module.ts。
const routes: Routes = [
  { path: '', redirectTo: '/layout1', pathMatch: 'full' },
  { path: 'layout1', component: Layout1Component },
  { path: 'layout4', loadChildren : './layout4/layout4.module#Layout4Module' } //惰性加載 
];


3. 務必清除 app.module.ts 中所有跟 Layout4Module 和其模組所有的引用關係,否則會造成延遲加載失效。
//import { Layout4Module } from './layout4/layout4.module'; //惰性加載需求,清除相依關係

@NgModule({ 
  imports: [
    BrowserModule,
    //Layout4Module, //惰性加載需求,清除相依關係
    AppRoutingModule  
  ], 
  bootstrap: [AppComponent]
})
export class AppModule { }


4. 下圖紅框處為點選 Layout4 路由後才加載進來的檔案。




Route guards


路由守衛,Angular 為路由器提供了一個管理機制,讓使用者要進入離開或者加載路由時,都可以被管理,目前 Angular 提供了以下幾個事件。

  1. 用CanActivate來處理導航到某路由的情況。
  2. 用CanActivateChild處理導航到子路由的情況。
  3. 用CanDeactivate來處理從當前路由離開的情況。
  4. 用Resolve在路由啟動之前獲取路由資料。
  5. 用CanLoad來處理非同步導航到某特性模組的情況。

# CanActivate
要求取得進入路由的憑證,應用程式通常會根據訪問者來決定是否授予某個特性區的訪問權。 我們可以只對已認證過的用戶或具有特定角色的用戶授予訪問權,還可以阻止或限制用戶訪問權,直到用戶帳戶啟動為止。

新增 auth-guard.service.ts 路由服務,必且實作 CanActivate 介面,此範例為判斷是否登入,若無登入則轉換到登入頁面,另外記住一下,該服務請一併加入 app.module.ts 內的 providers內。
import { Injectable } from '@angular/core';
import { CanActivate, CanActivateChild, Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { User } from './user'; //模擬User登入類別

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private router: Router) { }

  //使否允許存取路由
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (!User.IsLogin) { //未通過認證轉換到登入頁面
      this.router.navigate(['/login']);  
      return false;
    }

    return true;
  }
}

設定路由參數,指定需要被管理的路由,加上 canActivate 屬性並傳入參數為一陣列,這裡就是剛剛建立的服務類別名稱 AuthGuard,請參閱 app-routing.module.ts ,部分語法如下參考。
{ path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }, //保護路由訪問權限

接下來就是撰寫登入邏輯,實際運作後應該會發現,瀏覽到 /admin 路由時會轉跳到 /login 頁面,若成功登入後會再轉回 /admin,登入登出的寫法就不在這邊介紹了,有興趣的人可自行參閱 login.component.ts 和 admin.component.ts 兩個檔案。

# CanActivateChild
要求取得進入子路由的憑證,功能和實作方式幾乎都跟 CanActivateChild 一樣,它的差別只是在保護子路由的存取權限,但若 Parent 路由已經有實作 CanActivate 就不需要再針對子路由進行設定了,就不多說明了,自行參考底下語法。

auth-guard.service.ts 參考如下
import { Injectable } from '@angular/core';
import { CanActivate, CanActivateChild, Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { User } from './user'; //模擬User登入類別

@Injectable()
export class AuthGuard implements CanActivateChild {

  constructor(private router: Router) { }

  //允許訪問子路由
  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(childRoute, state);
  }
}

app-routing.module.ts 參考如下
  {
    path: 'admin',
    component: AdminComponent, 
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard], //保護子路由訪問權限
        children: [
          { path: 'admin-a', component: AdminAComponent } //實際訪問路徑/admin/admin-a
        ]
      }
    ]
  },


# CanDeactivate
判斷是否允許離開路由,主要用來處理未保存的變更,本範例就簡單的示範登出前的詢問視窗就好了。

新增 can-deactivate-guard.service.ts 檔案,並實作 CanDeactivate 服務介面,並且在此指定要受到管控的類別為 AdminComponent,會在離開前跳出 confirm 視窗,照慣例,新增服務請記得將該類別一併加入 app.module.ts 內的 providers內。
import { Injectable } from '@angular/core';
import { CanDeactivate, Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

import { AdminComponent } from './admin/admin.component';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<AdminComponent>
{
  canDeactivate(component: AdminComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot) {
    return confirm("確定要登出嗎?");
  }
}

設定路由參數,指定需要被管理的路由,加上 canDeactivate 屬性並傳入參數為一陣列,這裡就是剛剛建立的服務類別名稱 CanDeactivateGuard,請參閱 app-routing.module.ts ,部分語法如下參考。
{ path: 'admin', component: AdminComponent, canDeactivate: [CanDeactivateGuard] }, //離開前的詢問判斷


# CanLoad
使否允許載入模塊,說明一下這個使用情境,會到上面惰性加載的部分,假設管理者模塊原本就是惰性加載,但在成功登入時,我們並不希望點選到該模塊就直接加載檔案的話,這時候就可以實作 Canload 介面來判斷是否允許加載,本範例就直接使用 layout4 來調整,必須在確認登入後,才會加載該模塊。

修改auth-guard.service.ts 路由服務,必且實作 CanLoad 介面,此範例為判斷是否登入,若有登入才加載該模塊,參考底下語法。
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate,
  CanActivateChild, CanLoad, Router, Route, RouterStateSnapshot
} from '@angular/router';
import { User } from './user'; //模擬User登入類別

@Injectable()
export class AuthGuard implements CanLoad {

  constructor(private router: Router) { }

  //是否載入模塊
  canLoad(route: Route): boolean {
    return User.IsLogin;
  }
}

設定路由參數,指定需要被管理的路由,加上 canDeactivate 屬性並傳入參數為一陣列,這裡就是剛剛建立的服務類別名稱 CanDeactivateGuard,請參閱 app-routing.module.ts ,部分語法如下參考。
{ path: 'layout4', loadChildren: './layout4/layout4.module#Layout4Module', canLoad: [AuthGuard] }, //是否允許載入該模塊





以上就是路由比較常用的功能,當然路由還有一些也有可能會使用到的特殊功能,就列出來讓大家參考,有需要可自行查詢,就不在逐一說明了。





本文範例請至Github下載。

參考網站
Angular官網(路由教學)
Angular官網(進階路由)

留言