Route 路由守衛(wèi)

2020-07-07 17:49 更新

現(xiàn)在,任何用戶都能在任何時候?qū)Ш降饺魏蔚胤?。但有時候出于種種原因需要控制對該應(yīng)用的不同部分的訪問??赡馨ㄈ缦聢鼍埃?/p>

  • 該用戶可能無權(quán)導(dǎo)航到目標(biāo)組件。

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

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

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

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

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

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

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

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

  • 如果它返回 UrlTree,則取消當(dāng)前的導(dǎo)航,并且開始導(dǎo)航到返回的這個 UrlTree.

注:

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

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

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

注:

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

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

  • CanActivate來處理導(dǎo)航到某路由的情況。

  • CanActivateChild來處理導(dǎo)航到某子路由的情況。

  • CanDeactivate來處理從當(dāng)前路由離開的情況.

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

  • CanLoad來處理異步導(dǎo)航到某特性模塊的情況。

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

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

CanActivate :需要身份驗證

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

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

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

使用一些新的管理功能來擴(kuò)展危機(jī)中心。首先添加一個名為 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,它用于在特性模塊內(nèi)的儀表盤路由以及兩個尚未完成的用于管理危機(jī)和英雄的組件之間進(jìn)行路由。

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 }", 這樣就只有當(dāng)用戶導(dǎo)航到 /admin 這個 URL 時才會激活它,而不會在導(dǎo)航到它的某個子路由時。

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

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

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 路徑下,組件是不必要的。此外,無組件路由可以更容易地保護(hù)子路由。

接下來,把 AdminModule 導(dǎo)入到 "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. 守護(hù)“管理特性”區(qū)。


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


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


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

ng generate guard auth/auth
```

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

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",導(dǎo)入 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)保護(hù)了,不過該守衛(wèi)還需要做進(jìn)一步定制。

  1. 通過 AuthGuard 驗證。

AuthGuard 模擬身份驗證。

AuthGuard 可以調(diào)用應(yīng)用中的一項服務(wù),該服務(wù)能讓用戶登錄,并且保存當(dāng)前用戶的信息。在 "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;
      }
    }

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

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

修改 AuthGuard 以調(diào)用 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 服務(wù)注入到了構(gòu)造函數(shù)中。 你還沒有提供 AuthService,這里要說明的是:可以往路由守衛(wèi)中注入有用的服務(wù)。

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

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

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

  1. 添加 LoginComponent。

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

    ng generate component auth/login

在 "auth/auth-routing.module.ts" 文件中注冊一個 /login 路由。在 "app.module.ts" 中,導(dǎo)入 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:保護(hù)子路由

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

你要保護(hù)管理特性模塊,防止它被非授權(quán)訪問,還要保護(hù)這個特性模塊內(nèi)部的那些子路由。

擴(kuò)展 AuthGuard 以便在 admin 路由之間導(dǎo)航時提供保護(hù)。 打開 "auth.guard.ts" 并從路由庫中導(dǎo)入 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 添加到“無組件的”管理路由,來同時保護(hù)它的所有子路由,而不是為每個路由單獨添加這個 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” 工作流,該應(yīng)用會立即接受對英雄的每次更改,而不進(jìn)行驗證。

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

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

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

你需要用異步的方式等待,在服務(wù)器返回答復(fù)之前先停止導(dǎo)航。

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

取消與保存

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

這兩個按鈕都會在保存或取消之后導(dǎo)航回危機(jī)列表。

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 鏈接,取消,按下瀏覽器后退按鈕,或者不保存就離開。

這個示例應(yīng)用會彈出一個確認(rèn)對話框,它會異步等待用戶的響應(yīng),等用戶給出一個明確的答復(fù)。

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

生成一個 Dialog 服務(wù),以處理用戶的確認(rèn)操作。

ng generate service dialog

DialogService 添加一個 confirm() 方法,以提醒用戶確認(rèn)。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,當(dāng)用戶最終決定了如何去做時,它就會被解析 —— 或者決定放棄更改直接導(dǎo)航離開(true),或者保留未完成的修改,留在危機(jī)編輯器中(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() 方法并調(diào)用它。守衛(wèi)在不知道任何組件 deactivate 方法細(xì)節(jié)的情況下,就能讓這個守衛(wèi)重復(fù)使用。

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

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 組件,它已經(jīng)實現(xiàn)了對未保存的更改進(jìn)行確認(rè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() 方法可以同步返回;如果沒有危機(jī),或者沒有待處理的更改,它會立即返回 true。但它也能返回一個 Promise 或一個 Observable,路由器也會等待它解析為真值(導(dǎo)航)或偽造(停留在當(dāng)前路由上)。

往 "crisis-center.routing.module.ts" 的危機(jī)詳情路由中用 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)在,你已經(jīng)給了用戶一個能保護(hù)未保存更改的安全守衛(wèi)。

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

Hero DetailCrisis Detail 中,它們等待路由讀取完對應(yīng)的英雄和危機(jī)。

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

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

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

導(dǎo)航前預(yù)先加載路由信息

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

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

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

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() 中與危機(jī)檢索有關(guān)的邏輯移到 CrisisDetailResolverService 中。 導(dǎo)入 Crisis 模型、CrisisServiceRouter 以便讓你可以在找不到指定的危機(jī)時導(dǎo)航到別處。

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

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

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

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

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)導(dǎo)入到 "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 不應(yīng)該再去獲取這個危機(jī)的詳情。 你只要重新配置路由,就可以修改從哪里獲取危機(jī)的詳情。 把 CrisisDetailComponent 改成從 ActivatedRoute.data.crisis 屬性中獲取危機(jī)詳情,這正是你重新配置路由的恰當(dāng)時機(jī)。

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 沒有繼承自某個基類。路由器只要找到了這個方法,就會調(diào)用它。

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

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

查詢參數(shù)及片段

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

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

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

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

router.navigate() 方法添加一個 NavigationExtras 對象,用來導(dǎo)航到 /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);
  }
}

還可以在導(dǎo)航之間保留查詢參數(shù)和片段,而無需再次在導(dǎo)航中提供。在 LoginComponent 中的 router.navigateUrl() 方法中,添加一個對象作為第二個參數(shù),該對象提供了 queryParamsHandlingpreserveFragment,用于傳遞當(dāng)前的查詢參數(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 選項,它將會在導(dǎo)航時保留當(dāng)前的查詢參數(shù),并與其它查詢參數(shù)合并。

要在登錄后導(dǎo)航到 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 服務(wù)的 routerState 屬性使用。和路由參數(shù)類似,全局查詢參數(shù)和片段也是 Observable 對象。 在修改過的英雄管理組件中,你將借助 AsyncPipe 直接把 Observable 傳給模板。

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

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

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

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號