現(xiàn)在,任何用戶都能在任何時(shí)候導(dǎo)航到任何地方。但有時(shí)候出于種種原因需要控制對(duì)該應(yīng)用的不同部分的訪問(wèn)??赡馨ㄈ缦聢?chǎng)景:
你可以往路由配置中添加守衛(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)航到所返回的 |
注意:
守衛(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é)中有一些例子。
應(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ī)則的工具。
本節(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ū)的文件是這樣的:
管理特性模塊包含 ?AdminComponent
?,它用于在特性模塊內(nèi)的儀表盤(pán)路由以及兩個(gè)尚未完成的用于管理危機(jī)和英雄的組件之間進(jìn)行路由。
<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>
<h3>Dashboard</h3>
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 {}
<p>Manage your crises here</p>
<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í)。
最初的管理路由配置如下:
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>
現(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)一步定制。
讓 ?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á)意圖。
你需要一個(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
?中。
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 {
}
<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>
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();
}
}
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
?守衛(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 {}
回到 “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)。
在 ?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è)路由組件。
目前,?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):
Resolve
?接口是可選的。?CrisisDetailResolverService
?沒(méi)有繼承自某個(gè)基類(lèi)。路由器只要找到了這個(gè)方法,就會(huì)調(diào)用它。Observable
?就會(huì)取消導(dǎo)航。與里程碑相關(guān)的危機(jī)中心代碼如下。
<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>
<h3>Welcome to the Crisis Center</h3>
<h2>Crisis Center</h2>
<router-outlet></router-outlet>
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 { }
<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>
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();
})
);
}
}
<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>
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 });
}
}
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;
}
})
);
}
}
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)!)
);
}
}
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)
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');
}
}
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ù)部分,你只需要處理該路由的專(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 保存。
更多建議: