阅读(4727) 赞(1)

Route 路由守卫

2020-07-07 16:31:27 更新

现在,任何用户都能在任何时候导航到任何地方。但有时候出于种种原因需要控制对该应用的不同部分的访问。可能包括如下场景:

  • 该用户可能无权导航到目标组件。

  • 可能用户得先登录(认证)。

  • 在显示目标组件前,你可能得先获取某些数据。

  • 在离开组件前,你可能要先保存修改。

  • 你可能要询问用户:你是否要放弃本次更改,而不用保存它们?

你可以往路由配置中添加守卫,来处理这些场景。

守卫返回一个值,以控制路由器的行为:

  • 如果它返回 true,导航过程会继续

  • 如果它返回 false,导航过程就会终止,且用户留在原地。

  • 如果它返回 UrlTree,则取消当前的导航,并且开始导航到返回的这个 UrlTree.

注:

  • 守卫还可以告诉路由器导航到别处,这样也会取消当前的导航。要想在守卫中这么做,就要返回 false; 。

守卫可以用同步的方式返回一个布尔值。但在很多情况下,守卫无法用同步的方式给出答案。 守卫可能会向用户问一个问题、把更改保存到服务器,或者获取新数据,而这些都是异步操作。

因此,路由的守卫可以返回一个 Observable<boolean> 或 Promise<boolean>,并且路由器会等待这个可观察对象被解析为 true 或 false。

注:

  • 提供给 Router 的可观察对象还必须能结束(complete)。否则,导航就不会继续。

路由器可以支持多种守卫接口:

  • 用CanActivate来处理导航到某路由的情况。

  • 用CanActivateChild来处理导航到某子路由的情况。

  • 用CanDeactivate来处理从当前路由离开的情况.

  • 用Resolve在路由激活之前获取路由数据。

  • 用CanLoad来处理异步导航到某特性模块的情况。

在分层路由的每个级别上,你都可以设置多个守卫。 路由器会先按照从最深的子路由由下往上检查的顺序来检查 CanDeactivate() 和 CanActivateChild() 守卫。 然后它会按照从上到下的顺序检查 CanActivate() 守卫。 如果特性模块是异步加载的,在加载它之前还会检查 CanLoad()守卫。 如果任何一个守卫返回 false,其它尚未完成的守卫会被取消,这样整个导航就被取消了。

接下来的小节中有一些例子。

CanActivate :需要身份验证

应用程序通常会根据访问者来决定是否授予某个特性区的访问权。 你可以只对已认证过的用户或具有特定角色的用户授予访问权,还可以阻止或限制用户访问权,直到用户账户激活为止。

CanActivate 守卫是一个管理这些导航类业务规则的工具。

  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

管理特性区的文件是这样的:

管理特性模块包含 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 段的斜杠 /,但它能匹配管理特性区下的任何路由。 但你只希望在访问 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 数组中来注册这些管理类路由。

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. 守护“管理特性”区。


    现在危机中心的每个路由都是对所有人开放的。这些新的管理特性应该只能被已登录用户访问。


    编写一个 `CanActivate()` 守卫,将正在尝试访问管理组件匿名用户重定向到登录页。


    在 "auth" 文件夹中生成一个 `AuthGuard`。

ng generate guard auth/auth
```

为了演示这些基础知识,这个例子只把日志写到控制台中,立即 return true,并允许继续导航:

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() 守卫来引用 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 {}

管理特性区现在受此守卫保护了,不过该守卫还需要做进一步定制。

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

注意,你把 AuthService 和 Router 服务注入到了构造函数中。 你还没有提供 AuthService,这里要说明的是:可以往路由守卫中注入有用的服务。

该守卫返回一个同步的布尔值。如果用户已经登录,它就返回 true,导航会继续。

这个 ActivatedRouteSnapshot 包含了即将被激活的路由,而 RouterStateSnapshot 包含了该应用即将到达的状态。 你应该通过守卫进行检查。

如果用户还没有登录,你就会用 RouterStateSnapshot.url 保存用户来自的 URL 并让路由器跳转到登录页(你尚未创建该页)。 这间接导致路由器自动中止了这次导航,checkLogin() 返回 false 并不是必须的,但这样可以更清楚的表达意图。

  1. 添加 LoginComponent。

你需要一个 LoginComponent 来让用户登录进这个应用。在登录之后,你就会跳转到前面保存的 URL,如果没有,就跳转到默认 URL。 该组件没有什么新内容,你在路由配置中使用它的方式也没什么新意。

    ng generate component auth/login

在 "auth/auth-routing.module.ts" 文件中注册一个 /login 路由。在 "app.module.ts" 中,导入 AuthModule 并且添加到 AppModule 的 imports 中。

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 守卫来保护子路由。 CanActivateChild 守卫和 CanActivate 守卫很像。 它们的区别在于,CanActivateChild 会在任何子路由被激活之前运行。

你要保护管理特性模块,防止它被非授权访问,还要保护这个特性模块内部的那些子路由。

扩展 AuthGuard 以便在 admin 路由之间导航时提供保护。 打开 "auth.guard.ts" 并从路由库中导入 CanActivateChild 接口。

接下来,实现 CanActivateChild 方法,它所接收的参数与 CanActivate 方法一样:一个 ActivatedRouteSnapshot 和一个 RouterStateSnapshot。 CanActivateChild 方法可以返回 Observable<boolean|UrlTree> 或 Promise<boolean|UrlTree> 来支持异步检查,或 boolean 或 UrlTree 来支持同步检查。 这里返回的或者是 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” 工作流,该应用会立即接受对英雄的每次更改,而不进行验证。

在现实世界,你可能不得不积累来自用户的更改,跨字段验证,在服务器上验证,或者把变更保持在待定状态,直到用户确认这一组字段或取消并还原所有变更为止。

当用户要导航离开时,你可以让用户自己决定该怎么处理这些未保存的更改。 如果用户选择了取消,你就留下来,并允许更多改动。 如果用户选择了确认,那就进行保存。

在保存成功之前,你还可以继续推迟导航。如果你让用户立即移到下一个界面,而保存却失败了(可能因为数据不符合有效性规则),你就会丢失该错误的上下文环境。

你需要用异步的方式等待,在服务器返回答复之前先停止导航。

CanDeactivate 守卫能帮助你决定如何处理未保存的更改,以及如何处理。

取消与保存

用户在 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 是一个阻塞型操作,它会显示一个模态对话框,并等待用户的交互。

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)。

生成一个守卫(guard),以检查组件(任意组件均可)中是否存在 canDeactivate() 方法。

ng generate guard can-deactivate

把下面的代码粘贴到守卫中。

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

守卫不需要知道哪个组件有 deactivate 方法,它可以检测 CrisisDetailComponent 组件有没有 canDeactivate() 方法并调用它。守卫在不知道任何组件 deactivate 方法细节的情况下,就能让这个守卫重复使用。

另外,你也可以为 CrisisDetailComponent 创建一个特定的 CanDeactivate 守卫。 在需要访问外部信息时,canDeactivate() 方法为你提供了组件、ActivatedRoute 和 RouterStateSnapshot 的当前实例。 如果只想为这个组件使用该守卫,并且需要获取该组件属性或确认路由器是否允许从该组件导航出去时,这会非常有用。

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 组件,它已经实现了对未保存的更改进行确认的工作流。

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 数组添加一个 Guard(守卫)。

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

现在,你已经给了用户一个能保护未保存更改的安全守卫。

Resolve: 预先获取组件数据

在 Hero Detail 和 Crisis Detail 中,它们等待路由读取完对应的英雄和危机。

如果你在使用真实 api,很有可能数据返回有延迟,导致无法即时显示。 在这种情况下,直到数据到达前,显示一个空的组件不是最好的用户体验。

最好使用解析器预先从服务器上获取完数据,这样在路由激活的那一刻数据就准备好了。 还要在路由到此组件之前处理好错误。 但当某个 id 无法对应到一个危机详情时,就没办法处理它。 这时最好把用户带回到“危机列表”中,那里显示了所有有效的“危机”。

总之,你希望的是只有当所有必要数据都已经拿到之后,才渲染这个路由组件。

导航前预先加载路由信息

目前,CrisisDetailComponent 会接收选中的危机。 如果该危机没有找到,路由器就会导航回危机列表视图。

如果能在该路由将要激活时提前处理了这个问题,那么用户体验会更好。 CrisisDetailResolver 服务可以接收一个 Crisis,而如果这个 Crisis 不存在,就会在激活该路由并创建 CrisisDetailComponent 之前先行离开。

在 Crisis Center 特性区生成一个 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 模型、CrisisService 和 Router 以便让你可以在找不到指定的危机时导航到别处。

为了更明确一点,可以实现一个带有 Crisis 类型的 Resolve 接口。

注入 CrisisService 和 Router,并实现 resolve() 方法。 该方法可以返回一个 Promise、一个 Observable 来支持异步方式,或者直接返回一个值来支持同步方式。

CrisisService.getCrisis() 方法返回一个可观察对象,以防止在数据获取完之前加载本路由。 Router 守卫要求这个可观察对象必须可结束(complete),也就是说它已经发出了所有值。 你可以为 take 操作符传入一个参数 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 就会取消导航。

查询参数及片段

在路由参数部分,你只需要处理该路由的专属参数。但是,你也可以用查询参数来获取对所有路由都可用的可选参数。

片段可以引用页面中带有特定 id 属性的元素.

修改 AuthGuard 以提供 session_id 查询参数,在导航到其它路由后,它还会存在。

再添加一个锚点(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);
  }
}

还可以在导航之间保留查询参数和片段,而无需再次在导航中提供。在 LoginComponent 中的 router.navigateUrl() 方法中,添加一个对象作为第二个参数,该对象提供了 queryParamsHandling 和 preserveFragment,用于传递当前的查询参数和片段到下一个路由。

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 选项,它将会在导航时保留当前的查询参数,并与其它查询参数合并。

要在登录后导航到 Admin Dashboard 路由,请更新 "admin-dashboard.component.ts" 以处理这些查询参数和片段。

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

查询参数和片段可通过 Router 服务的 routerState 属性使用。和路由参数类似,全局查询参数和片段也是 Observable 对象。 在修改过的英雄管理组件中,你将借助 AsyncPipe 直接把 Observable 传给模板。

按照下列步骤试验下:点击 Admin 按钮,它会带着你提供的 queryParamMap 和 fragment 跳转到登录页。 点击 Login 按钮,你就会被重定向到 Admin Dashboard 页。 注意,它仍然带着上一步提供的 queryParamMap 和 fragment。

你可以用这些持久化信息来携带需要为每个页面都提供的信息,如认证令牌或会话的 ID 等。

“查询参数”和“片段”也可以分别用 RouterLink 中的 queryParamsHandling 和 preserveFragment 保存。