Route 路由守衛(wèi)

2020-07-07 17:49 更新

現(xiàn)在,任何用戶都能在任何時候導航到任何地方。但有時候出于種種原因需要控制對該應用的不同部分的訪問??赡馨ㄈ缦聢鼍埃?/p>

  • 該用戶可能無權導航到目標組件。

  • 可能用戶得先登錄(認證)。

  • 在顯示目標組件前,你可能得先獲取某些數(shù)據(jù)。

  • 在離開組件前,你可能要先保存修改。

  • 你可能要詢問用戶:你是否要放棄本次更改,而不用保存它們?

你可以往路由配置中添加守衛(wèi),來處理這些場景。

守衛(wèi)返回一個值,以控制路由器的行為:

  • 如果它返回 true,導航過程會繼續(xù)

  • 如果它返回 false,導航過程就會終止,且用戶留在原地。

  • 如果它返回 UrlTree,則取消當前的導航,并且開始導航到返回的這個 UrlTree.

注:

  • 守衛(wèi)還可以告訴路由器導航到別處,這樣也會取消當前的導航。要想在守衛(wèi)中這么做,就要返回 false; 。

守衛(wèi)可以用同步的方式返回一個布爾值。但在很多情況下,守衛(wèi)無法用同步的方式給出答案。 守衛(wèi)可能會向用戶問一個問題、把更改保存到服務器,或者獲取新數(shù)據(jù),而這些都是異步操作。

因此,路由的守衛(wèi)可以返回一個 Observable<boolean>Promise<boolean>,并且路由器會等待這個可觀察對象被解析為 truefalse

注:

  • 提供給 Router 的可觀察對象還必須能結束(complete)。否則,導航就不會繼續(xù)。

路由器可以支持多種守衛(wèi)接口:

  • CanActivate來處理導航到某路由的情況。

  • CanActivateChild來處理導航到某子路由的情況。

  • CanDeactivate來處理從當前路由離開的情況.

  • Resolve在路由激活之前獲取路由數(shù)據(jù)。

  • CanLoad來處理異步導航到某特性模塊的情況。

在分層路由的每個級別上,你都可以設置多個守衛(wèi)。 路由器會先按照從最深的子路由由下往上檢查的順序來檢查 CanDeactivate()CanActivateChild() 守衛(wèi)。 然后它會按照從上到下的順序檢查 CanActivate() 守衛(wèi)。 如果特性模塊是異步加載的,在加載它之前還會檢查 CanLoad()守衛(wèi)。 如果任何一個守衛(wèi)返回 false,其它尚未完成的守衛(wèi)會被取消,這樣整個導航就被取消了。

接下來的小節(jié)中有一些例子。

CanActivate :需要身份驗證

應用程序通常會根據(jù)訪問者來決定是否授予某個特性區(qū)的訪問權。 你可以只對已認證過的用戶或具有特定角色的用戶授予訪問權,還可以阻止或限制用戶訪問權,直到用戶賬戶激活為止。

CanActivate 守衛(wèi)是一個管理這些導航類業(yè)務規(guī)則的工具。

  1. 添加一個“管理”特性模塊:

使用一些新的管理功能來擴展危機中心。首先添加一個名為 AdminModule 的新特性模塊。

生成一個帶有特性模塊文件和路由配置文件的 admin 目錄。

    ng generate module admin --routing

接下來,生成一些支持性組件。

    ng generate component admin/admin-dashboard

    ng generate component admin/admin

    ng generate component admin/manage-crises

    ng generate component admin/manage-heroes

管理特性區(qū)的文件是這樣的:

管理特性模塊包含 AdminComponent,它用于在特性模塊內的儀表盤路由以及兩個尚未完成的用于管理危機和英雄的組件之間進行路由。

Path:"src/app/admin/admin/admin.component.html" 。

    <h3>ADMIN</h3>
    <nav>
      <a routerLink="./" routerLinkActive="active"
        [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
      <a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
      <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
    </nav>
    <router-outlet></router-outlet>

Path:"src/app/admin/admin-dashboard/admin-dashboard.component.html src/app/admin/admin.module.ts src/app/admin/manage-crises/manage-crises.component.html src/app/admin/manage-heroes/manage-heroes.component.html " 。

    <p>Dashboard</p>

Path:"src/app/admin/admin.module.ts" 。

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


    import { AdminComponent }           from './admin/admin.component';
    import { AdminDashboardComponent }  from './admin-dashboard/admin-dashboard.component';
    import { ManageCrisesComponent }    from './manage-crises/manage-crises.component';
    import { ManageHeroesComponent }    from './manage-heroes/manage-heroes.component';


    import { AdminRoutingModule }       from './admin-routing.module';


    @NgModule({
      imports: [
        CommonModule,
        AdminRoutingModule
      ],
      declarations: [
        AdminComponent,
        AdminDashboardComponent,
        ManageCrisesComponent,
        ManageHeroesComponent
      ]
    })
    export class AdminModule {}

Path:"src/app/admin/manage-crises/manage-crises.component.html" 。

    <p>Manage your crises here</p>

Path:"src/app/admin/manage-heroes/manage-heroes.component.html" 。

    <p>Manage your heroes here</p>

雖然管理儀表盤中的 RouterLink 只包含一個沒有其它 URL 段的斜杠 /,但它能匹配管理特性區(qū)下的任何路由。 但你只希望在訪問 Dashboard 路由時才激活該鏈接。 往 Dashboard 這個 routerLink 上添加另一個綁定 [routerLinkActiveOptions]="{ exact: true }", 這樣就只有當用戶導航到 /admin 這個 URL 時才會激活它,而不會在導航到它的某個子路由時。

無組件路由:分組路由,而不需要組件。

最初的管理路由配置如下:

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

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


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

AdminComponent 下的子路由有一個 path 和一個 children 屬性,但是它沒有使用 component。這就定義了一個無組件路由。

要把 Crisis Center 管理下的路由分組到 admin 路徑下,組件是不必要的。此外,無組件路由可以更容易地保護子路由。

接下來,把 AdminModule 導入到 "app.module.ts" 中,并把它加入 imports 數(shù)組中來注冊這些管理類路由。

Path:"src/app/app.module.ts (admin module)" 。

    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';


    import { AdminModule }             from './admin/admin.module';


    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        HeroesModule,
        CrisisCenterModule,
        AdminModule,
        AppRoutingModule
      ],
      declarations: [
        AppComponent,
        ComposeMessageComponent,
        PageNotFoundComponent
      ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }

然后往殼組件 AppComponent 中添加一個鏈接,讓用戶能點擊它,以訪問該特性。

Path:"src/app/app.component.html (template)" 。

    <h1 class="title">Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
      <a routerLink="/admin" routerLinkActive="active">Admin</a>
      <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
    </nav>
    <div [@routeAnimation]="getAnimationData(routerOutlet)">
      <router-outlet #routerOutlet="outlet"></router-outlet>
    </div>
    <router-outlet name="popup"></router-outlet>


2. 守護“管理特性”區(qū)。


    現(xiàn)在危機中心的每個路由都是對所有人開放的。這些新的管理特性應該只能被已登錄用戶訪問。


    編寫一個 `CanActivate()` 守衛(wèi),將正在嘗試訪問管理組件匿名用戶重定向到登錄頁。


    在 "auth" 文件夾中生成一個 `AuthGuard`。

ng generate guard auth/auth
```

為了演示這些基礎知識,這個例子只把日志寫到控制臺中,立即 return true,并允許繼續(xù)導航:

Path:"src/app/auth/auth.guard.ts (excerpt)" 。

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


    @Injectable({
      providedIn: 'root',
    })
    export class AuthGuard implements CanActivate {
      canActivate(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot): boolean {
        console.log('AuthGuard#canActivate called');
        return true;
      }
    }

接下來,打開 "admin-routing.module.ts",導入 AuthGuard 類,修改管理路由并通過 CanActivate() 守衛(wèi)來引用 AuthGuard

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

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


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

管理特性區(qū)現(xiàn)在受此守衛(wèi)保護了,不過該守衛(wèi)還需要做進一步定制。

  1. 通過 AuthGuard 驗證。

AuthGuard 模擬身份驗證。

AuthGuard 可以調用應用中的一項服務,該服務能讓用戶登錄,并且保存當前用戶的信息。在 "admin" 目錄下生成一個新的 AuthService

    ng generate service auth/auth

修改 AuthService 以登入此用戶:

Path:"src/app/auth/auth.service.ts (excerpt)" 。

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


    import { Observable, of } from 'rxjs';
    import { tap, delay } from 'rxjs/operators';


    @Injectable({
      providedIn: 'root',
    })
    export class AuthService {
      isLoggedIn = false;


      // store the URL so we can redirect after logging in
      redirectUrl: string;


      login(): Observable<boolean> {
        return of(true).pipe(
          delay(1000),
          tap(val => this.isLoggedIn = true)
        );
      }


      logout(): void {
        this.isLoggedIn = false;
      }
    }

雖然不會真的進行登錄,但它有一個 isLoggedIn 標志,用來標識是否用戶已經登錄過了。 它的 login() 方法會仿真一個對外部服務的 API 調用,返回一個可觀察對象(observable)。在短暫的停頓之后,這個可觀察對象就會解析成功。 redirectUrl 屬性將會保存在用戶要訪問的 URL 中,以便認證完之后導航到它。

為了保持最小化,這個例子會將未經身份驗證的用戶重定向到 "/admin"。

修改 AuthGuard 以調用 AuthService。

Path:"src/app/auth/auth.guard.ts (v2)" 。

    import { Injectable } from '@angular/core';
    import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';


    import { AuthService }      from './auth.service';


    @Injectable({
      providedIn: 'root',
    })
    export class AuthGuard implements CanActivate {
      constructor(private authService: AuthService, private router: Router) {}


      canActivate(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot): true|UrlTree {
        let url: string = state.url;


        return this.checkLogin(url);
      }


      checkLogin(url: string): true|UrlTree {
        if (this.authService.isLoggedIn) { return true; }


        // Store the attempted URL for redirecting
        this.authService.redirectUrl = url;


        // Redirect to the login page
        return this.router.parseUrl('/login');
      }
    }

注意,你把 AuthServiceRouter 服務注入到了構造函數(shù)中。 你還沒有提供 AuthService,這里要說明的是:可以往路由守衛(wèi)中注入有用的服務。

該守衛(wèi)返回一個同步的布爾值。如果用戶已經登錄,它就返回 true,導航會繼續(xù)。

這個 ActivatedRouteSnapshot 包含了即將被激活的路由,而 RouterStateSnapshot 包含了該應用即將到達的狀態(tài)。 你應該通過守衛(wèi)進行檢查。

如果用戶還沒有登錄,你就會用 RouterStateSnapshot.url 保存用戶來自的 URL 并讓路由器跳轉到登錄頁(你尚未創(chuàng)建該頁)。 這間接導致路由器自動中止了這次導航,checkLogin() 返回 false 并不是必須的,但這樣可以更清楚的表達意圖。

  1. 添加 LoginComponent。

你需要一個 LoginComponent 來讓用戶登錄進這個應用。在登錄之后,你就會跳轉到前面保存的 URL,如果沒有,就跳轉到默認 URL。 該組件沒有什么新內容,你在路由配置中使用它的方式也沒什么新意。

    ng generate component auth/login

在 "auth/auth-routing.module.ts" 文件中注冊一個 /login 路由。在 "app.module.ts" 中,導入 AuthModule 并且添加到 AppModuleimports 中。

Path:"src/app/app.module.ts" 。

    import { NgModule }       from '@angular/core';
    import { BrowserModule }  from '@angular/platform-browser';
    import { FormsModule }    from '@angular/forms';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';


    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 { AuthModule }              from './auth/auth.module';


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

Path:"src/app/auth/login/login.component.html" 。

    <h2>LOGIN</h2>
    <p>{{message}}</p>
    <p>
      <button (click)="login()"  *ngIf="!authService.isLoggedIn">Login</button>
      <button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button>
    </p>

Path:"src/app/auth/login/login.component.ts" 。

    import { Component } from '@angular/core';
    import { Router } from '@angular/router';
    import { AuthService } from '../auth.service';


    @Component({
      selector: 'app-login',
      templateUrl: './login.component.html',
      styleUrls: ['./login.component.css']
    })
    export class LoginComponent {
      message: string;


      constructor(public authService: AuthService, public router: Router) {
        this.setMessage();
      }


      setMessage() {
        this.message = 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out');
      }


      login() {
        this.message = 'Trying to log in ...';


        this.authService.login().subscribe(() => {
          this.setMessage();
          if (this.authService.isLoggedIn) {
            // Usually you would use the redirect URL from the auth service.
            // However to keep the example simple, we will always redirect to `/admin`.
            const redirectUrl = '/admin';


            // Redirect the user
            this.router.navigate([redirectUrl]);
          }
        });
      }


      logout() {
        this.authService.logout();
        this.setMessage();
      }
    }

Path:"src/app/auth/auth.module.ts" 。

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


    import { LoginComponent }    from './login/login.component';
    import { AuthRoutingModule } from './auth-routing.module';


    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        AuthRoutingModule
      ],
      declarations: [
        LoginComponent
      ]
    })
    export class AuthModule {}

CanActivateChild:保護子路由

你還可以使用 CanActivateChild 守衛(wèi)來保護子路由。 CanActivateChild 守衛(wèi)和 CanActivate 守衛(wèi)很像。 它們的區(qū)別在于,CanActivateChild 會在任何子路由被激活之前運行。

你要保護管理特性模塊,防止它被非授權訪問,還要保護這個特性模塊內部的那些子路由。

擴展 AuthGuard 以便在 admin 路由之間導航時提供保護。 打開 "auth.guard.ts" 并從路由庫中導入 CanActivateChild 接口。

接下來,實現(xiàn) CanActivateChild 方法,它所接收的參數(shù)與 CanActivate 方法一樣:一個 ActivatedRouteSnapshot 和一個 RouterStateSnapshot。 CanActivateChild 方法可以返回 Observable<boolean|UrlTree>Promise<boolean|UrlTree> 來支持異步檢查,或 booleanUrlTree 來支持同步檢查。 這里返回的或者是 true 以便允許用戶訪問管理特性模塊,或者是 UrlTree 以便把用戶重定向到登錄頁:

Path:"src/app/auth/auth.guard.ts (excerpt)" 。

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  UrlTree
}                           from '@angular/router';
import { AuthService }      from './auth.service';


@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}


  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    let url: string = state.url;


    return this.checkLogin(url);
  }


  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    return this.canActivate(route, state);
  }


/* . . . */
}

同樣把這個 AuthGuard 添加到“無組件的”管理路由,來同時保護它的所有子路由,而不是為每個路由單獨添加這個 AuthGuard

Path:"src/app/admin/admin-routing.module.ts (excerpt)" 。

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


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

CanDeactivate:處理未保存的更改

回到 “Heroes” 工作流,該應用會立即接受對英雄的每次更改,而不進行驗證。

在現(xiàn)實世界,你可能不得不積累來自用戶的更改,跨字段驗證,在服務器上驗證,或者把變更保持在待定狀態(tài),直到用戶確認這一組字段或取消并還原所有變更為止。

當用戶要導航離開時,你可以讓用戶自己決定該怎么處理這些未保存的更改。 如果用戶選擇了取消,你就留下來,并允許更多改動。 如果用戶選擇了確認,那就進行保存。

在保存成功之前,你還可以繼續(xù)推遲導航。如果你讓用戶立即移到下一個界面,而保存卻失敗了(可能因為數(shù)據(jù)不符合有效性規(guī)則),你就會丟失該錯誤的上下文環(huán)境。

你需要用異步的方式等待,在服務器返回答復之前先停止導航。

CanDeactivate 守衛(wèi)能幫助你決定如何處理未保存的更改,以及如何處理。

取消與保存

用戶在 CrisisDetailComponent 中更新危機信息。 與 HeroDetailComponent 不同,用戶的改動不會立即更新危機的實體對象。當用戶按下了 Save 按鈕時,應用就更新這個實體對象;如果按了 Cancel 按鈕,那就放棄這些更改。

這兩個按鈕都會在保存或取消之后導航回危機列表。

Path:"src/app/crisis-center/crisis-detail/crisis-detail.component.ts (cancel and save methods)" 。

cancel() {
  this.gotoCrises();
}


save() {
  this.crisis.name = this.editName;
  this.gotoCrises();
}

在這種情況下,用戶可以點擊 heroes 鏈接,取消,按下瀏覽器后退按鈕,或者不保存就離開。

這個示例應用會彈出一個確認對話框,它會異步等待用戶的響應,等用戶給出一個明確的答復。

你也可以用同步的方式等用戶的答復,阻塞代碼。但如果能用異步的方式等待用戶的答復,應用就會響應性更好,還能同時做別的事。

生成一個 Dialog 服務,以處理用戶的確認操作。

ng generate service dialog

DialogService 添加一個 confirm() 方法,以提醒用戶確認。window.confirm 是一個阻塞型操作,它會顯示一個模態(tài)對話框,并等待用戶的交互。

Path:"src/app/dialog.service.ts" 。

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';


/**
 * Async modal dialog service
 * DialogService makes this app easier to test by faking this service.
 * TODO: better modal implementation that doesn't use window.confirm
 */
@Injectable({
  providedIn: 'root',
})
export class DialogService {
  /**
   * Ask user to confirm an action. `message` explains the action and choices.
   * Returns observable resolving to `true`=confirm or `false`=cancel
   */
  confirm(message?: string): Observable<boolean> {
    const confirmation = window.confirm(message || 'Is it OK?');


    return of(confirmation);
  };
}

它返回observable,當用戶最終決定了如何去做時,它就會被解析 —— 或者決定放棄更改直接導航離開(true),或者保留未完成的修改,留在危機編輯器中(false)。

生成一個守衛(wèi)(guard),以檢查組件(任意組件均可)中是否存在 canDeactivate() 方法。

ng generate guard can-deactivate

把下面的代碼粘貼到守衛(wèi)中。

Path:"src/app/can-deactivate.guard.ts" 。

import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs';


export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}


@Injectable({
  providedIn: 'root',
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

守衛(wèi)不需要知道哪個組件有 deactivate 方法,它可以檢測 CrisisDetailComponent 組件有沒有 canDeactivate() 方法并調用它。守衛(wèi)在不知道任何組件 deactivate 方法細節(jié)的情況下,就能讓這個守衛(wèi)重復使用。

另外,你也可以為 CrisisDetailComponent 創(chuàng)建一個特定的 CanDeactivate 守衛(wèi)。 在需要訪問外部信息時,canDeactivate() 方法為你提供了組件、ActivatedRouteRouterStateSnapshot 的當前實例。 如果只想為這個組件使用該守衛(wèi),并且需要獲取該組件屬性或確認路由器是否允許從該組件導航出去時,這會非常有用。

Path:"src/app/can-deactivate.guard.ts (component-specific)" 。

import { Injectable }           from '@angular/core';
import { Observable }           from 'rxjs';
import { CanDeactivate,
         ActivatedRouteSnapshot,
         RouterStateSnapshot }  from '@angular/router';


import { CrisisDetailComponent } from './crisis-center/crisis-detail/crisis-detail.component';


@Injectable({
  providedIn: 'root',
})
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {


  canDeactivate(
    component: CrisisDetailComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    // Get the Crisis Center ID
    console.log(route.paramMap.get('id'));


    // Get the current URL
    console.log(state.url);


    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // observable which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}

看看 CrisisDetailComponent 組件,它已經實現(xiàn)了對未保存的更改進行確認的工作流。

Path:"src/app/crisis-center/crisis-detail/crisis-detail.component.ts (excerpt)" 。

canDeactivate(): Observable<boolean> | boolean {
  // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
  if (!this.crisis || this.crisis.name === this.editName) {
    return true;
  }
  // Otherwise ask the user with the dialog service and return its
  // observable which resolves to true or false when the user decides
  return this.dialogService.confirm('Discard changes?');
}

注意,canDeactivate() 方法可以同步返回;如果沒有危機,或者沒有待處理的更改,它會立即返回 true。但它也能返回一個 Promise 或一個 Observable,路由器也會等待它解析為真值(導航)或偽造(停留在當前路由上)。

往 "crisis-center.routing.module.ts" 的危機詳情路由中用 canDeactivate 數(shù)組添加一個 Guard(守衛(wèi))。

Path:"src/app/crisis-center/crisis-center-routing.module.ts (can deactivate guard)" 。

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';


import { CanDeactivateGuard }    from '../can-deactivate.guard';


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


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

現(xiàn)在,你已經給了用戶一個能保護未保存更改的安全守衛(wèi)。

Resolve: 預先獲取組件數(shù)據(jù)

Hero DetailCrisis Detail 中,它們等待路由讀取完對應的英雄和危機。

如果你在使用真實 api,很有可能數(shù)據(jù)返回有延遲,導致無法即時顯示。 在這種情況下,直到數(shù)據(jù)到達前,顯示一個空的組件不是最好的用戶體驗。

最好使用解析器預先從服務器上獲取完數(shù)據(jù),這樣在路由激活的那一刻數(shù)據(jù)就準備好了。 還要在路由到此組件之前處理好錯誤。 但當某個 id 無法對應到一個危機詳情時,就沒辦法處理它。 這時最好把用戶帶回到“危機列表”中,那里顯示了所有有效的“危機”。

總之,你希望的是只有當所有必要數(shù)據(jù)都已經拿到之后,才渲染這個路由組件。

導航前預先加載路由信息

目前,CrisisDetailComponent 會接收選中的危機。 如果該危機沒有找到,路由器就會導航回危機列表視圖。

如果能在該路由將要激活時提前處理了這個問題,那么用戶體驗會更好。 CrisisDetailResolver 服務可以接收一個 Crisis,而如果這個 Crisis 不存在,就會在激活該路由并創(chuàng)建 CrisisDetailComponent 之前先行離開。

Crisis Center 特性區(qū)生成一個 CrisisDetailResolver 服務文件。

ng generate service crisis-center/crisis-detail-resolver

Path:"src/app/crisis-center/crisis-detail-resolver.service.ts (generated)" 。

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


@Injectable({
  providedIn: 'root',
})
export class CrisisDetailResolverService {


  constructor() { }


}

CrisisDetailComponent.ngOnInit() 中與危機檢索有關的邏輯移到 CrisisDetailResolverService 中。 導入 Crisis 模型、CrisisServiceRouter 以便讓你可以在找不到指定的危機時導航到別處。

為了更明確一點,可以實現(xiàn)一個帶有 Crisis 類型的 Resolve 接口。

注入 CrisisServiceRouter,并實現(xiàn) resolve() 方法。 該方法可以返回一個 Promise、一個 Observable 來支持異步方式,或者直接返回一個值來支持同步方式。

CrisisService.getCrisis() 方法返回一個可觀察對象,以防止在數(shù)據(jù)獲取完之前加載本路由。 Router 守衛(wèi)要求這個可觀察對象必須可結束(complete),也就是說它已經發(fā)出了所有值。 你可以為 take 操作符傳入一個參數(shù) 1,以確保這個可觀察對象會在從 getCrisis 方法所返回的可觀察對象中取到第一個值之后就會結束。

如果它沒有返回有效的 Crisis,就會返回一個 Observable,以取消以前到 CrisisDetailComponent 的在途導航,并把用戶導航回 CrisisListComponent。修改后的 resolver 服務是這樣的:

Path:"src/app/crisis-center/crisis-detail-resolver.service.ts" 。

import { Injectable }             from '@angular/core';
import {
  Router, Resolve,
  RouterStateSnapshot,
  ActivatedRouteSnapshot
}                                 from '@angular/router';
import { Observable, of, EMPTY }  from 'rxjs';
import { mergeMap, take }         from 'rxjs/operators';


import { CrisisService }  from './crisis.service';
import { Crisis } from './crisis';


@Injectable({
  providedIn: 'root',
})
export class CrisisDetailResolverService implements Resolve<Crisis> {
  constructor(private cs: CrisisService, private router: Router) {}


  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> {
    let id = route.paramMap.get('id');


    return this.cs.getCrisis(id).pipe(
      take(1),
      mergeMap(crisis => {
        if (crisis) {
          return of(crisis);
        } else { // id not found
          this.router.navigate(['/crisis-center']);
          return EMPTY;
        }
      })
    );
  }
}

把這個解析器(resolver)導入到 "crisis-center-routing.module.ts" 中,并往 CrisisDetailComponent 的路由配置中添加一個 resolve 對象。

Path:"src/app/crisis-center/crisis-center-routing.module.ts (resolver)" 。

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';


import { CanDeactivateGuard }             from '../can-deactivate.guard';
import { CrisisDetailResolverService }    from './crisis-detail-resolver.service';


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


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

CrisisDetailComponent 不應該再去獲取這個危機的詳情。 你只要重新配置路由,就可以修改從哪里獲取危機的詳情。 把 CrisisDetailComponent 改成從 ActivatedRoute.data.crisis 屬性中獲取危機詳情,這正是你重新配置路由的恰當時機。

Path:"src/app/crisis-center/crisis-detail/crisis-detail.component.ts (ngOnInit v2)" 。

ngOnInit() {
  this.route.data
    .subscribe((data: { crisis: Crisis }) => {
      this.editName = data.crisis.name;
      this.crisis = data.crisis;
    });
}

注意以下三個要點:

  1. 路由器的這個 Resolve 接口是可選的。CrisisDetailResolverService 沒有繼承自某個基類。路由器只要找到了這個方法,就會調用它。

  1. 路由器會在用戶可以導航的任何情況下調用該解析器,這樣你就不用針對每個用例都編寫代碼了。

  1. 在任何一個解析器中返回空的 Observable 就會取消導航。

查詢參數(shù)及片段

在路由參數(shù)部分,你只需要處理該路由的專屬參數(shù)。但是,你也可以用查詢參數(shù)來獲取對所有路由都可用的可選參數(shù)。

片段可以引用頁面中帶有特定 id 屬性的元素.

修改 AuthGuard 以提供 session_id 查詢參數(shù),在導航到其它路由后,它還會存在。

再添加一個錨點(A)元素,來讓你能跳轉到頁面中的正確位置。

router.navigate() 方法添加一個 NavigationExtras 對象,用來導航到 /login 路由。

Path:"src/app/auth/auth.guard.ts (v3)" 。

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  NavigationExtras,
  UrlTree
}                           from '@angular/router';
import { AuthService }      from './auth.service';


@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}


  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
    let url: string = state.url;


    return this.checkLogin(url);
  }


  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
    return this.canActivate(route, state);
  }


  checkLogin(url: string): true|UrlTree {
    if (this.authService.isLoggedIn) { return true; }


    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;


    // Create a dummy session id
    let sessionId = 123456789;


    // Set our navigation extras object
    // that contains our global query params and fragment
    let navigationExtras: NavigationExtras = {
      queryParams: { 'session_id': sessionId },
      fragment: 'anchor'
    };


    // Redirect to the login page with extras
    return this.router.createUrlTree(['/login'], navigationExtras);
  }
}

還可以在導航之間保留查詢參數(shù)和片段,而無需再次在導航中提供。在 LoginComponent 中的 router.navigateUrl() 方法中,添加一個對象作為第二個參數(shù),該對象提供了 queryParamsHandlingpreserveFragment,用于傳遞當前的查詢參數(shù)和片段到下一個路由。

Path:"src/app/auth/login/login.component.ts (preserve)" 。

// Set our navigation extras object
// that passes on our global query params and fragment
let navigationExtras: NavigationExtras = {
  queryParamsHandling: 'preserve',
  preserveFragment: true
};


// Redirect the user
this.router.navigate([redirectUrl], navigationExtras);

queryParamsHandling 特性還提供了 merge 選項,它將會在導航時保留當前的查詢參數(shù),并與其它查詢參數(shù)合并。

要在登錄后導航到 Admin Dashboard 路由,請更新 "admin-dashboard.component.ts" 以處理這些查詢參數(shù)和片段。

Path:"src/app/admin/admin-dashboard/admin-dashboard.component.ts (v2)" 。

import { Component, OnInit }  from '@angular/core';
import { ActivatedRoute }     from '@angular/router';
import { Observable }         from 'rxjs';
import { map }                from 'rxjs/operators';


@Component({
  selector: 'app-admin-dashboard',
  templateUrl: './admin-dashboard.component.html',
  styleUrls: ['./admin-dashboard.component.css']
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;


  constructor(private route: ActivatedRoute) {}


  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .pipe(map(params => params.get('session_id') || 'None'));


    // Capture the fragment if available
    this.token = this.route
      .fragment
      .pipe(map(fragment => fragment || 'None'));
  }
}

查詢參數(shù)和片段可通過 Router 服務的 routerState 屬性使用。和路由參數(shù)類似,全局查詢參數(shù)和片段也是 Observable 對象。 在修改過的英雄管理組件中,你將借助 AsyncPipe 直接把 Observable 傳給模板。

按照下列步驟試驗下:點擊 Admin 按鈕,它會帶著你提供的 queryParamMapfragment 跳轉到登錄頁。 點擊 Login 按鈕,你就會被重定向到 Admin Dashboard 頁。 注意,它仍然帶著上一步提供的 queryParamMapfragment

你可以用這些持久化信息來攜帶需要為每個頁面都提供的信息,如認證令牌或會話的 ID 等。

“查詢參數(shù)”和“片段”也可以分別用 RouterLink 中的 queryParamsHandlingpreserveFragment 保存。

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號