也许这是我最后一次写关于Angular的文章,但也不一定,如果有机会我可能会继续吧,但今天的主题很明确就是路由。
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
<a routerLink="/cities" routerLinkActive="active">Cities</a>
</nav>
<router-outlet></router-outlet>
`,
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'Tour of Heroes';
}
这是主页面模版,通过routerLink实现定位,routerLinkActive会给当前显示的打上标记。
nav a.active {
color: #039be5;
}
<router-outlet></router-outlet>实际上则是对当前路由页面的填充。
那么问题来了,我们怎么知道每一个routerLink所指向的页面,看这个:
<!-- app-routing.module.ts -->
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
说以下三点:
- 注意命名的规范,路由模块都以routing这种形式,.module结尾。
- 路由里面的依赖组件必须都引进来。
- 结尾处一定要 将这个路由注册[ RouterModule.forRoot(routes) ]。
redirectTo是进来之后重定向到dashboard这个组件显示的页面。重定向路由需要一个pathMatch属性,来告诉路由器如何用URL去匹配路由的路径,否则路由器就会报错。 在本应用中,路由器应该只有在完整的URL等于''时才选择HeroListComponent组件,因此我们要把pathMatch设置为'full'。
我们觉得有一点很奇怪{ path: 'detail/:id', component: HeroDetailComponent },这里出处在哪里,模版中并没有呀。。
此时我们定位到了heroes的组件:
<!-- heroes.component.js -->
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'my-heroes',
templateUrl: './heroes.component.html',
styleUrls: [ './heroes.component.css' ]
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
constructor(
private router: Router,
private heroService: HeroService) { }
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedHero.id]);
}
}
我们看这个
this.router.navigate(['/detail', this.selectedHero.id]);
这实际上是在走这个路由{ path: 'detail/:this.selectedHero.id', component: HeroDetailComponent }
我们来看模版的代码:
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="selectedHero">
<h2>
{{selectedHero.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
我们这里有一个选定的hero,点击之后就会走那个detail路由。问题来了,这样的话如何把需要的数据带到那个页面去呢?
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private location: Location
) {}
ngOnInit(): void {
this.route.paramMap
.switchMap((params: ParamMap) => this.heroService.getHero(+params.get('id')))
.subscribe(hero => this.hero = hero);
}
// 返回上一页
goBack(): void {
this.location.back();
}
}
我们在ngOninit的时候,通过paramMap获取到来这个路由携带的参数,进而在服务中去做操作,把hero取出来。params.get('id')就是从hero页面传过来的selectedHero的id。
paramMap
An Observable
that contains a map of the required and optional parameters specific to the route. The map supports retrieving single and multiple values from the same parameter.
注意ActivatedRoute很关键,它可以帮我们获取路由的信息。
好了就剩最后一步就完成了路由的所有配置,那到底是什么呢?
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule
]
以上截取部分app.module的代码。
我们有个页面是英雄列表,理出每条英雄明细,现在需要每个跳转到详细页面。
<a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4">
我们在routerlink里面也可以传递参数,这样也会走activeroute的路线。
我们再来看一个复杂一点的:
<!--app.component.ts -->
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
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="/login" routerLinkActive="active">Login</a>
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
</nav>
<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>
`
})
export class AppComponent {
}
我们这里有2个点不太明白,我也是一样,后面我们再解释。
<!-- app-routing.module.ts -->
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ComposeMessageComponent } from './compose-message.component';
import { PageNotFoundComponent } from './not-found.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { AuthGuard } from './auth-guard.service';
import { SelectivePreloadingStrategy } from './selective-preloading-strategy';
const appRoutes: Routes = [
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'popup'
},
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
canLoad: [AuthGuard]
},
{
path: 'crisis-center',
loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
data: { preload: true }
},
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{
enableTracing: true, // <-- debugging purposes only
preloadingStrategy: SelectivePreloadingStrategy,
}
)
],
exports: [
RouterModule
],
providers: [
CanDeactivateGuard,
SelectivePreloadingStrategy
]
})
export class AppRoutingModule { }
讲解一下几点:
- { path: '**', component: PageNotFoundComponent } 找不到匹配,notfound组件
- loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',这个路由还有子路由,admin也是。
是不是有点傻傻分不清了,没关系咱们先看admin目录。
接下来看
<!-- admin-routing.module.ts -->
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { AdminDashboardComponent } from './admin-dashboard.component';
import { ManageCrisesComponent } from './manage-crises.component';
import { ManageHeroesComponent } from './manage-heroes.component';
import { AuthGuard } from '../auth-guard.service';
const adminRoutes: Routes = [
{
path: '',
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 {}
试想我们在主页面点击admin,他会怎么走,会走到admin子路由下面,此时对应的是path:'',canActivateChild: [AuthGuard]是什么鬼。
import { Injectable } from '@angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild,
NavigationExtras,
CanLoad, Route
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}
canLoad(route: Route): boolean {
let url = `/${route.path}`;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
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'
};
// Navigate to the login page with extras
this.router.navigate(['/login'], navigationExtras);
return false;
}
}
CanActivate, CanActivateChild, CanLoad 这三个是说是否能进入子路由。
我们来看这个过程:
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}
//这是第一步,能否激活子路由。
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting 初次进来isLoggedIn是false, 会走这里
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'
};
// Navigate to the login page with extras
this.router.navigate(['/login'], navigationExtras);
return false;
}
初次进来,会进canload方法,获取到当前的url为'/admin'被拦截,通过this.router.navigate(['/login'], navigationExtras)定位到login的组件页面。
注意canload只会在页面首次请求的那一次才有。
执行顺序:canload(only once) -> canActivate -> canActivateChild
Auth.service代码:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/delay';
@Injectable()
export class AuthService {
isLoggedIn = false; // 提供的登录状态为false
// store the URL so we can redirect after logging in
redirectUrl: string;
login(): Observable<boolean> {
return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true);
}
logout(): void {
this.isLoggedIn = false;
}
}
根据以上我们首次点击admin,此时还处于未登录状态,我们会被带到login界面。
<!-- login.component.ts -->
import { Component } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';
import { AuthService } from './auth.service';
@Component({
template: `
<h2>LOGIN</h2>
<p>{{message}}</p>
<p>
<button (click)="login()" *ngIf="!authService.isLoggedIn">Login</button>
<button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button>
</p>`
})
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) {
// Get the redirect URL from our auth service
// If no redirect has been set, use the default
let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/admin';
// 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([redirect], navigationExtras);
}
});
}
logout() {
this.authService.logout();
this.setMessage();
}
}
初次进入login未登录,message='logged out',点击login会执行authService的login方法,改变登录状态。
if (this.authService.isLoggedIn) {
// Get the redirect URL from our auth service
// If no redirect has been set, use the default
let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/admin';
// 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([redirect], navigationExtras);
}
之前我们在auth.service中设置了redirecturl为admin,所以会navigate到admin界面,并携带navigationExtras。
const adminRoutes: Routes = [
{
path: '',
component: AdminComponent,
canActivate: [AuthGuard],
children: [
{
path: '',
canActivateChild: [AuthGuard],
children: [
{ path: 'crises', component: ManageCrisesComponent },
{ path: 'heroes', component: ManageHeroesComponent },
{ path: '', component: AdminDashboardComponent }
]
}
]
}
];
好,现在成功进入admin,默认会加载AdminDashboardComponent。
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { SelectivePreloadingStrategy } from '../selective-preloading-strategy';
import 'rxjs/add/operator/map';
@Component({
template: `
<p>Dashboard</p>
<p>Session ID: {{ sessionId | async }}</p>
<a id="anchor"></a>
<p>Token: {{ token | async }}</p>
Preloaded Modules
<ul>
<li *ngFor="let module of modules">{{ module }}</li>
</ul>
`
})
export class AdminDashboardComponent implements OnInit {
sessionId: Observable<string>;
token: Observable<string>;
modules: string[];
constructor(
private route: ActivatedRoute,
private preloadStrategy: SelectivePreloadingStrategy
) {
this.modules = preloadStrategy.preloadedModules;
}
ngOnInit() {
// Capture the session ID if available
this.sessionId = this.route
.queryParamMap
.map(params => params.get('session_id') || 'None');
// Capture the fragment if available
this.token = this.route
.fragment
.map(fragment => fragment || 'None');
}
}
ngoninit的数据就是从auth.service里面原封不动过来的。。。
最后说一下:
loadChildren: 'app/admin/admin.module#AdminModule'
这个是加载的子模块名。感觉这一期讲不完,下一期再继续分享吧。。。