Angular 教程:為英雄之旅添加路由支持-里程碑 5:路由守衛(wèi)

2022-07-05 10:41 更新

里程碑 5:路由守衛(wèi)

現(xiàn)在,任何用戶都能在任何時(shí)候導(dǎo)航到任何地方。但有時(shí)候出于種種原因需要控制對(duì)該應(yīng)用的不同部分的訪問(wèn)??赡馨ㄈ缦聢?chǎng)景:

  • 該用戶可能無(wú)權(quán)導(dǎo)航到目標(biāo)組件
  • 可能用戶得先登錄(認(rèn)證)
  • 在顯示目標(biāo)組件前,你可能得先獲取某些數(shù)據(jù)
  • 在離開(kāi)組件前,你可能要先保存修改
  • 你可能要詢問(wèn)用戶:你是否要放棄本次更改,而不用保存它們?

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

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

守衛(wèi)返回的值

詳情

true

導(dǎo)航過(guò)程會(huì)繼續(xù)

false

導(dǎo)航過(guò)程就會(huì)終止,且用戶留在原地。

UrlTree

取消當(dāng)前導(dǎo)航,并開(kāi)始導(dǎo)航到所返回的 UrlTree

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

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

因此,路由的守衛(wèi)可以返回一個(gè) ?Observable<boolean>? 或 ?Promise<boolean>?,并且路由器會(huì)等待這個(gè)可觀察對(duì)象被解析為 ?true ?或 ?false?。

注意:
提供給 ?Router ?的可觀察對(duì)象會(huì)在接收到第一個(gè)值之后自動(dòng)完成(complete)。

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

守衛(wèi)接口

詳情

?CanActivate?

導(dǎo)航某路由時(shí)介入

?CanActivateChild?

導(dǎo)航某個(gè)子路由時(shí)介入

?CanDeactivate?

從當(dāng)前路由離開(kāi)時(shí)介入

?Resolve?

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

?CanLoad?

導(dǎo)航到某個(gè)異步加載的特性模塊時(shí)介入

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

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

CanActivate :需要身份驗(yàn)證

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

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

添加一個(gè)“管理”特性模塊


本節(jié)將指導(dǎo)你使用一些新的管理功能來(lái)擴(kuò)展危機(jī)中心。首先添加一個(gè)名為 ?AdminModule ?的新特性模塊。

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

ng generate module admin --routing

接下來(lái),生成一些支持性組件。

ng generate component admin/admin-dashboard
ng generate component admin/admin
ng generate component admin/manage-crises
ng generate component admin/manage-heroes

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

屏幕截圖 2022-07-05 095722

管理特性模塊包含 ?AdminComponent?,它用于在特性模塊內(nèi)的儀表盤(pán)路由以及兩個(gè)尚未完成的用于管理危機(jī)和英雄的組件之間進(jìn)行路由。

  • src/app/admin/admin/admin.component.html
  • <h2>Admin</h2>
    <nav>
      <a routerLink="./" routerLinkActive="active"
        [routerLinkActiveOptions]="{ exact: true }" ariaCurrentWhenActive="page">Dashboard</a>
      <a routerLink="./crises" routerLinkActive="active" ariaCurrentWhenActive="page">Manage Crises</a>
      <a routerLink="./heroes" routerLinkActive="active" ariaCurrentWhenActive="page">Manage Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  • src/app/admin/admin-dashboard/admin-dashboard.component.html
  • <h3>Dashboard</h3>
  • 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 {}
  • src/app/admin/manage-crises/manage-crises.component.html
  • <p>Manage your crises here</p>
  • src/app/admin/manage-heroes/manage-heroes.component.html
  • <p>Manage your heroes here</p>
雖然管理儀表盤(pán)中的 ?RouterLink ?只包含一個(gè)沒(méi)有其它 URL 段的斜杠 ?/?,但它能匹配管理特性區(qū)下的任何路由。但你只希望在訪問(wèn) ?Dashboard ?路由時(shí)才激活該鏈接。往 ?Dashboard? 這個(gè) routerLink 上添加另一個(gè)綁定 ?[routerLinkActiveOptions]="{ exact: true }"?,這樣就只有當(dāng)用戶導(dǎo)航到 ?/admin? 這個(gè) URL 時(shí)才會(huì)激活它,而不會(huì)在導(dǎo)航到它的某個(gè)子路由時(shí)。
無(wú)組件路由:分組路由,而不需要組件

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

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 ?下的子路由有一個(gè) ?path ?和一個(gè) ?children ?屬性,但是它沒(méi)有使用 ?component?。這就定義了一個(gè)無(wú)組件路由。

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

接下來(lái),把 ?AdminModule ?導(dǎo)入到 ?app.module.ts? 中,并把它加入 ?imports ?數(shù)組中來(lái)注冊(cè)這些管理類(lèi)路由。

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 ?中添加一個(gè)鏈接,讓用戶能點(diǎn)擊它,以訪問(wèn)該特性。

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

守護(hù)“管理特性”區(qū)

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

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

在 ?auth ?文件夾中生成一個(gè) ?AuthGuard?。

ng generate guard auth/auth

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

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

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

接下來(lái),打開(kāi) ?admin-routing.module.ts?,導(dǎo)入 ?AuthGuard ?類(lèi),修改管理路由并通過(guò) ?CanActivate()? 守衛(wèi)來(lái)引用 ?AuthGuard?:

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ù)了,不過(guò)該守衛(wèi)還需要做進(jìn)一步定制。

通過(guò) AuthGuard 驗(yàn)證

讓 ?AuthGuard ?模擬身份驗(yàn)證。

?AuthGuard ?可以調(diào)用應(yīng)用中的一項(xiàng)服務(wù),該服務(wù)能讓用戶登錄,并且保存當(dāng)前用戶的信息。在 ?admin ?目錄下生成一個(gè)新的 ?AuthService?:

ng generate service auth/auth

修改 ?AuthService ?以登入此用戶:

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 | null = null;

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

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

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

為了保持最小化,這個(gè)例子會(huì)將未經(jīng)身份驗(yàn)證的用戶重定向到 ?/admin?。

修改 ?AuthGuard ?以調(diào)用 ?AuthService?。

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(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    const 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 ?服務(wù)注入到了構(gòu)造函數(shù)中。你還沒(méi)有提供 ?AuthService?,這里要說(shuō)明的是:可以往路由守衛(wèi)中注入有用的服務(wù)。

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

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

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

添加 LoginComponent

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

ng generate component auth/login

在 ?auth/auth-routing.module.ts? 文件中注冊(cè)一個(gè) ?/login? 路由。在 ?app.module.ts? 中,導(dǎo)入 ?AuthModule ?并且添加到 ?AppModule ?的 ?imports ?中。

  • 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 {
    }
  • src/app/auth/login/login.component.html
  • <h2>Login</h2>
    <p>{{message}}</p>
    <p>
      <button type="button" (click)="login()"  *ngIf="!authService.isLoggedIn">Login</button>
      <button type="button" (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button>
    </p>
  • 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.message = this.getMessage();
      }
    
      getMessage() {
        return 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out');
      }
    
      login() {
        this.message = 'Trying to log in ...';
    
        this.authService.login().subscribe(() => {
          this.message = this.getMessage();
          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.message = this.getMessage();
      }
    }
  • 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)來(lái)保護(hù)子路由。?CanActivateChild ?守衛(wèi)和 ?CanActivate ?守衛(wèi)很像。它們的區(qū)別在于,?CanActivateChild ?會(huì)在任何子路由被激活之前運(yùn)行。

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

擴(kuò)展 ?AuthGuard ?以便在 ?admin ?路由之間導(dǎo)航時(shí)提供保護(hù)。打開(kāi) ?auth.guard.ts? 并從路由庫(kù)中導(dǎo)入 ?CanActivateChild ?接口。

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

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 {
    const url: string = state.url;

    return this.checkLogin(url);
  }

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

/* . . . */
}

同樣把這個(gè) ?AuthGuard ?添加到“無(wú)組件的”管理路由,來(lái)同時(shí)保護(hù)它的所有子路由,而不是為每個(gè)路由單獨(dú)添加這個(gè) ?AuthGuard?。

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)用會(huì)立即接受對(duì)英雄的每次更改,而不進(jìn)行驗(yàn)證。

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

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

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

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

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

取消與保存

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

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

cancel() {
  this.gotoCrises();
}

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

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

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

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

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

ng generate service dialog

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

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)用戶最終決定了如何去做時(shí),它就會(huì)被解析 —— 或者決定放棄更改直接導(dǎo)航離開(kāi)(?true?),或者保留未完成的修改,留在危機(jī)編輯器中(?false?)。

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

ng generate guard can-deactivate

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

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)不需要知道哪個(gè)組件有 ?deactivate ?方法,它可以檢測(cè) ?CrisisDetailComponent ?組件有沒(méi)有 ?canDeactivate()? 方法并調(diào)用它。守衛(wèi)在不知道任何組件 ?deactivate ?方法細(xì)節(jié)的情況下,就能讓這個(gè)守衛(wèi)重復(fù)使用。

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

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)實(shí)現(xiàn)了對(duì)未保存的更改進(jìn)行確認(rèn)的工作流。

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()? 方法可以同步返回;如果沒(méi)有危機(jī),或者沒(méi)有待處理的更改,它會(huì)立即返回 ?true?。但它也能返回一個(gè) ?Promise ?或一個(gè) ?Observable?,路由器也會(huì)等待它解析為真值(導(dǎo)航)或偽造(停留在當(dāng)前路由上)。

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

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)給了用戶一個(gè)能保護(hù)未保存更改的安全守衛(wèi)。

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

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

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

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

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

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

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

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

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

ng generate service crisis-center/crisis-detail-resolver
import { Injectable } from '@angular/core';

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

  constructor() { }

}

把 ?CrisisDetailComponent.ngOnInit()? 中與危機(jī)檢索有關(guān)的邏輯移到 ?CrisisDetailResolverService ?中。導(dǎo)入 ?Crisis ?模型、?CrisisService ?和 ?Router ?以便讓你可以在找不到指定的危機(jī)時(shí)導(dǎo)航到別處。

為了更明確一點(diǎn),可以實(shí)現(xiàn)一個(gè)帶有 ?Crisis ?類(lèi)型的 ?Resolve ?接口。

注入 ?CrisisService ?和 ?Router?,并實(shí)現(xiàn) ?resolve()? 方法。該方法可以返回一個(gè) ?Promise?、一個(gè) ?Observable ?來(lái)支持異步方式,或者直接返回一個(gè)值來(lái)支持同步方式。

?CrisisService.getCrisis()? 方法返回一個(gè)可觀察對(duì)象,以防止在數(shù)據(jù)獲取完之前加載本路由。

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

import { Injectable } from '@angular/core';
import {
  Router, Resolve,
  RouterStateSnapshot,
  ActivatedRouteSnapshot
} from '@angular/router';
import { Observable, of, EMPTY } from 'rxjs';
import { mergeMap } 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> {
    const id = route.paramMap.get('id')!;

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

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

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

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

回顧以下三個(gè)重要點(diǎn):

  1. 路由器的這個(gè) ?Resolve ?接口是可選的。?CrisisDetailResolverService ?沒(méi)有繼承自某個(gè)基類(lèi)。路由器只要找到了這個(gè)方法,就會(huì)調(diào)用它。
  2. 路由器會(huì)在用戶可以導(dǎo)航的任何情況下調(diào)用該解析器,這樣你就不用針對(duì)每個(gè)用例都編寫(xiě)代碼了。
  3. 在任何一個(gè)解析器中返回空的 ?Observable ?就會(huì)取消導(dǎo)航。

與里程碑相關(guān)的危機(jī)中心代碼如下。

  • app.component.html
  • <div class="wrapper">
      <h1 class="title">Angular Router</h1>
      <nav>
        <a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
        <a routerLink="/superheroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
        <a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">Admin</a>
        <a routerLink="/login" routerLinkActive="active" ariaCurrentWhenActive="page">Login</a>
        <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
      </nav>
      <div [@routeAnimation]="getRouteAnimationData()">
        <router-outlet></router-outlet>
      </div>
      <router-outlet name="popup"></router-outlet>
    </div>
  • crisis-center-home.component.html
  • <h3>Welcome to the Crisis Center</h3>
  • crisis-center.component.html
  • <h2>Crisis Center</h2>
    <router-outlet></router-outlet>
  • crisis-center-routing.module.ts
  • 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 { }
  • crisis-list.component.html
  • <ul class="crises">
      <li *ngFor="let crisis of crises$ | async" [class.selected]="crisis.id === selectedId">
        <a [routerLink]="[crisis.id]">
          <span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
        </a>
      </li>
    </ul>
    
    <router-outlet></router-outlet>
  • crisis-list.component.ts
  • import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    
    import { CrisisService } from '../crisis.service';
    import { Crisis } from '../crisis';
    import { Observable } from 'rxjs';
    import { switchMap } from 'rxjs/operators';
    
    @Component({
      selector: 'app-crisis-list',
      templateUrl: './crisis-list.component.html',
      styleUrls: ['./crisis-list.component.css']
    })
    export class CrisisListComponent implements OnInit {
      crises$!: Observable<Crisis[]>;
      selectedId = 0;
    
      constructor(
        private service: CrisisService,
        private route: ActivatedRoute
      ) {}
    
      ngOnInit() {
        this.crises$ = this.route.paramMap.pipe(
          switchMap(params => {
            this.selectedId = parseInt(params.get('id')!, 10);
            return this.service.getCrises();
          })
        );
      }
    }
  • crisis-detail.component.html
  • <div *ngIf="crisis">
      <h3>{{ editName }}</h3>
      <p>Id: {{ crisis.id }}</p>
      <label for="crisis-name">Crisis name: </label>
      <input type="text" id="crisis-name" [(ngModel)]="editName" placeholder="name"/>
      <div>
        <button type="button" (click)="save()">Save</button>
        <button type="button" (click)="cancel()">Cancel</button>
      </div>
    </div>
  • crisis-detail.component.ts
  • import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute, Router } from '@angular/router';
    import { Observable } from 'rxjs';
    
    import { Crisis } from '../crisis';
    import { DialogService } from '../../dialog.service';
    
    @Component({
      selector: 'app-crisis-detail',
      templateUrl: './crisis-detail.component.html',
      styleUrls: ['./crisis-detail.component.css']
    })
    export class CrisisDetailComponent implements OnInit {
      crisis!: Crisis;
      editName = '';
    
      constructor(
        private route: ActivatedRoute,
        private router: Router,
        public dialogService: DialogService
      ) {}
    
      ngOnInit() {
        this.route.data
          .subscribe(data => {
            const crisis: Crisis = data['crisis'];
            this.editName = crisis.name;
            this.crisis = crisis;
          });
      }
    
      cancel() {
        this.gotoCrises();
      }
    
      save() {
        this.crisis.name = this.editName;
        this.gotoCrises();
      }
    
      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?');
      }
    
      gotoCrises() {
        const crisisId = this.crisis ? this.crisis.id : null;
        // Pass along the crisis id if available
        // so that the CrisisListComponent can select that crisis.
        // Add a totally useless `foo` parameter for kicks.
        // Relative navigation back to the crises
        this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
      }
    }
  • 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 } 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> {
        const id = route.paramMap.get('id')!;
    
        return this.cs.getCrisis(id).pipe(
          mergeMap(crisis => {
            if (crisis) {
              return of(crisis);
            } else { // id not found
              this.router.navigate(['/crisis-center']);
              return EMPTY;
            }
          })
        );
      }
    }
  • crisis.service.ts
  • import { BehaviorSubject } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    import { Injectable } from '@angular/core';
    import { MessageService } from '../message.service';
    import { Crisis } from './crisis';
    import { CRISES } from './mock-crises';
    
    @Injectable({
      providedIn: 'root',
    })
    export class CrisisService {
      static nextCrisisId = 100;
      private crises$: BehaviorSubject<Crisis[]> = new BehaviorSubject<Crisis[]>(CRISES);
    
      constructor(private messageService: MessageService) { }
    
      getCrises() { return this.crises$; }
    
      getCrisis(id: number | string) {
        return this.getCrises().pipe(
          map(crises => crises.find(crisis => crisis.id === +id)!)
        );
      }
    
    }
  • 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);
      }
    }

路由守衛(wèi)

  • auth.guard.ts
  • 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 {
        const 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;
    
        // Redirect to the login page
        return this.router.parseUrl('/login');
      }
    }
  • 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;
      }
    }

查詢參數(shù)及片段

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

片段可以引用頁(yè)面中帶有特定 ?id ?屬性的元素。

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

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

為 ?router.navigate()? 方法添加一個(gè) ?NavigationExtras ?對(duì)象,用來(lái)導(dǎo)航到 ?/login? 路由。

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 {
    const 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
    const sessionId = 123456789;

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

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

還可以在導(dǎo)航之間保留查詢參數(shù)和片段,而無(wú)需再次在導(dǎo)航中提供。在 ?LoginComponent ?中的 ?router.navigate()? 方法中,添加一個(gè)對(duì)象作為第二個(gè)參數(shù),該對(duì)象提供了 ?queryParamsHandling ?和 ?preserveFragment?,用于傳遞當(dāng)前的查詢參數(shù)和片段到下一個(gè)路由。

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

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

?queryParamsHandling ?特性還提供了 ?merge ?選項(xiàng),它將會(huì)在導(dǎo)航時(shí)保留當(dāng)前的查詢參數(shù),并與其它查詢參數(shù)合并。

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

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ù)和片段可通過(guò) ?Router ?服務(wù)的 ?routerState ?屬性使用。和路由參數(shù)類(lèi)似,全局查詢參數(shù)和片段也是 ?Observable ?對(duì)象。在修改過(guò)的英雄管理組件中,你將借助 ?AsyncPipe ?直接把 ?Observable ?傳給模板。

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)