路由要解决的核心问题是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL表示。在angular中,页面由组件构成,因此URL和页面的对应关系实质上就是URL和组件的对应关系。
建立URL和组件的对应关系可通过路由配置来指定。路由配置包含了多个配置项。最简单的一个配置项包含path
和component
两个属性,path
属性将被angular用来生成一个URL,而component
属性则指定了该URL所对应的组件。
在定义了路由配置后,angular路由将以其为依据,来管理应用中的各个组件。
- 首先,当用户在浏览器上输入URL后,angular将获取该URL并将其解析生成一个
UrlTree
实例。 - 其次,在路由配置中寻找并激活与
UrlTree
实例匹配的配置项。 - 再次,为配置项中指定的组件创建实例。
- 最后,将该组件渲染于路由组件的模板中
<router-outlet>
指令所在位置。
基本用法
要将一个URL所对应的组件在页面中显示出来有三步:定义路由配置、创建根路由模块、添加<router-outlet>
指令标签。
路由配置
路由配置是一个Routes
类型的数组,数组的每一个元素即为一个路由配置项。
//app.routes.ts
import { Routes } from '@angular/router';
import { ListComponent } from './list/list.component';
import { CollectionComponent } from './collection/collection.component';
export const rootRouterConfig:Routes=[
//...
{ path:'list',component:ListComponent }, //http://localhost:4200/list
{ path:'collection',component:CollectionComponent } //http://localhost:4200/collection
]
创建根路由模块
根路由模块包含了路由所需的各项服务,是路由工作流程得以正常执行的基础。通过调用RouterModule.forRoot()
方法来创建根路由模块,并将其导入到应用的根模块AppModule
中。
//app.module.ts
import { ModuleWithProviders } from '@angular/core';
import { RouterModule } from '@angular/router';
import { rootRouterConfig } from './app.routes';
let rootRouterModule:ModuleWithProviders=RouterModule.forRoot(rootRouterConfig);
@NgModule({
imports:[rootRouterModule],
//...
})
export class AppModule {}
根路由模块默认提供的路由策略为PathLocationStrategy
。该策略要求应用必须设置一个base
路径,用于作为前缀来生成和解析URL。设置base
路径最简单的方式是在index.html
文件中设置<base>
元素的href
属性。
<head>
<base href="/">
</head>
添加RouterOutlet指令
RouterOutlet
指令的作用是在组件的模板中开辟出一块区域,用于显示URL所对应的组件。angular将模板中使用了<router-outlet>
标签的组件统称为路由组件。
<!--app.component.html-->
<main class="main">
<router-outlet></router-outlet>
</main>
路由策略
路由策略决定angular将使用URL的哪一部分来和路由配置项的path
属性进行匹配。angular提供了PathLocationStrategy
和HashLocationStrategy
两种路由策略,分别表示使用URL的path
部分和hash
部分来进行路由配置。
http://host[:port]/[/path][?query][#hash]
HashLocationStrategy介绍
该策略的原理是利用了浏览器在处理hash
部分的两个特性。
- 浏览器向服务器发送请求时不会带上
hash
部分的内容。 - 更改URL的
hash
部分不会向服务器重新发送请求,这使得在进行跳转的时候不会引发页面的刷新和应用的重新加载。
要使用该策略,只需要在注入路由服务时使用useHash
属性进行显式指定即可。
//app.module.ts
@NgModule({
imports:[RouterModule.forRoot(rootRouterConfig,{useHash:true})],
//...
})
export class AppModule{}
PathLocationStrategy介绍
该策略使用URL的path
部分来进行路由匹配,浏览器会将配置项对应的URL原封不动地发送给服务器。
作为angular的默认路由策略,其最大的优点在于为服务器端渲染提供了可能。要使用PathLocationStrategy
路由策略,必须满足三个条件:
一,浏览器需支持H5的history.pushState()
方法,正是这一方法使得RouterLink
指令在跳转时即便更改了URL的path
部分,却依然不会引起页面刷新。
二,需要在服务器进行设置,将应用的所有URL重定向到应用的首页。这是因为该策略所生成的URL在服务器上并不存在与其相对应的文件结构,如果不进行重定向,服务器将返回404错误。
三,需要为应用设置一个base
路径,angular将以base
路径为前缀来生成和解析URL。这样做的好处是服务器可以根据base
路径来区分来自不同应用的请求。
设置base
路径有两种方式,一种是设置<base>
标签,如果将base
路径变为app
,那URL也将变为http://localhost:4200/app/list
。
<base href="/app">
第二种方式是通过向应用注入APP_BASE_HREF
变量来实现。
//app.module.ts
import { Component,NgModule } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
@NgModule({
providers:[{provide:APP_BASE_HREF,useValue:'/app'}],
})
export class AppModule {}
如果这两种方式同时使用,第二种具有更高优先级。
路由跳转
当某个事件引发了跳转时,angular会根据跳转时的参数生成一个UrlTree
实例来和配置项进行匹配,如果匹配成功则显示相应的组件并将新URL更新在浏览器的地址栏上;如果匹配不成功则报错。
使用指令跳转
指令跳转通过使用RouterLink
指令来完成。该指令接收一个链接参数数组,angular将根据该数组来生成UrlTree
实例进行跳转。
<!--app/shared/footer.component.html-->
<nav>
<!--http://localhost:4200/collection-->
<a [routerLink]="['/location']"><i>收藏</i></a>
<a [routerLink]="['/llist']"><i>通讯录</i></a>
</nav>
如果不借助RouterLink
指令而以纯HTML的方式来定义超链接,则单击超链接后会使得整个页面被重新加载。
RouterLink
指令可以被应用在任何HTML元素上。
<button [routerLink]="['/collection']"><i>收藏</i></button>
当RouterLink
被激活时,还可以通过RouterLinkActive
指令为其相应的HTML元素指定CSS类。
/*footer.component.css*/
.active{
background-color:#3DD689;
}
<!--footer.component.html-->
<nav>
<a [routerLink]="['/collection']" routerLinkActive="active"><i>收藏</i></a>
</nav>
RouterLinkActive
指令除了可以作用于routerLink
所在的元素之外,还可以作用于这些元素的任意祖先元素。当该祖先元素下的任意routerLink
处于激活状态时,该祖先元素都将获得routerLinkActive
指定的CSS类。
<nav routerLinkActive="active">
<a [routerLink]="['/collection']"><i>收藏</i>
</nav>
使用代码跳转
RouterLink
指令仅响应click
事件,如果需要响应其他事件或者需要根据运行时的数据动态决定如何跳转,则可以通过调用Router.navigateByUrl()
或其兄弟方法Router.navigate()
来完成。
//list.component.ts
import { Router } from '@angular/router';
export class ListComponent implements OnInit{
constructor (private _router:Router){
setTimeout(()=>{
_router.navigateByUrl('/collection');
//或者_router.navigate(['/collection']);
},1000)
}
}
Router.navigateByUrl()
和Router.navigate()
方法的底层工作机制基本一致,最终都是通过调用Router.scheduleNavigation()
方法来执行跳转。不同的地方在于两个方法指定跳转的目标配置项的方式。Router.navigateByUrl()
方法通过一个URL字符串或UrlTree
实例来指定。
navigateByUrl(url:string|UrlTree,extras:NavigationExtras={skipLocationChange:false}):Promise<boolean>{
if(url instanceof UrlTree){
return this.scheduleNavigation(url,extras);
}else{
//解析URL字符串生成相应的UrlTree实例
const urlTree=this.urlSerializer.parse(url);
//使用UrlTree实例进行跳转
return this.scheduleNavigation(urlTree,extras);
}
}
Router.navigate()
方法与RouterLink
指令相似,通过链接参数数组来指定。
navigate(commands:any[],extras:NavigationExtras={skipLocationChange:false}):Promise<boolean>{
//通过链接参数数组生成UrlTree实例
return this.scheduleNavigation(this.createUrlTree(commands,extras),extras);
}
这两个方法除了可以通过第一个参数来指定目标配置项外,还支持extras
参数定义跳转的具体行为。如,如果想在不改变URL的情况下完成跳转,可通过以下代码:
_router.navigateByUrl('/collection',{skipLocationChange:true});
路由参数
Path参数
Path
参数是通过解析URL的path
部分来获取参数。在定义一个配置项的path
属性时,可以使用/
字符来对path
属性进行分段,如果一个分段以:
字符开头,则URL中与该分段进行匹配的部分将作为参数传递到组件中。
//app.routes.ts
export const ContactsAppRoutes:RouterConfig=[
{path:'detail/:id',component:DetailComponent}
];
只有当URL解析出来的分段数和path
属性的分段数一致时,才能匹配到该配置项。下面两个URL无法匹配到该配置项。
http://localhost:4200/detail //分段数为1
http://localhost:4200/detail/1/segment //分段数为3
给路由参数赋值,除了可以直接在浏览器上输入URL外,还可以通过RouterLink
指令或跳转方法来完成。
//angular会将链接参数数组的每一个非对象元素当成一个分段进行拼接
//因此下面的链接参数数组对应的path为`detail/1`
<a [routerLink]="['/detail',1]">
_router.navigate(['/detail',1]);
//或者直接指定path
_router.navigateByUrl('detail/1');
在组件中获取Path
参数需要导入ActivatedRoute
服务,该服务提供了两种方式,分别适用于不同页面间跳转和同一页面内跳转。
angular应用从一个页面跳转到另一个新的页面,实质上是从一个配置项跳转到另一个配置项。angular除了会为配置项所对应的组件创建实例外,还会为该配置项本身创建一个ActivatedRoute
实例来表示该配置项已被激活。该ActivatedRoute
实例包含了一个快照(即snapshot
属性),记录了从当前URL中解析出来的所有path
参数。
//detail.component.ts
//1.导入ActivatedRoute服务
import { ActivatedRoute } from '@angular/router';
//...
export class DetailComponent implements OnInit,OnDestroy{
contact_id:string;
constructor(private _activatedRoute:ActivatedRoute){
console.log('创建DetailCompoent组件实例');
}
ngOnInit(){
//2.通过快照获取Path参数
this.contact_id=this._activatedRoute.snapshot.params['id'];
console.log('参数id的值为:'+this.contact_id);
}
}
在页面跳转时获取参数值的例子:
<!--detail.component.html-->
<div class="detail-contain">
<a [routerLink]="['']" class="back-to-list">
<i class="icon-back"></i>所有联系人
</a>
<a [routerLink]="['/detail',nextContactId()]" class="back-to-list">
下一联系人
</a>
</div>
//detail.component.ts
export class DetailComponent implements OnInits,OnDestroy{
nextContactId(){
return parseInt(this.contact_id)+1;
}
}
点击下一联系人后,URL按照预期变成了http://localhost:4200/detail/2
,但页面上显示的仍是原联系人的信息。这是因为angular在处理同一页面内跳转时,不会重新创建组件的实例,所以组件的构造函数和ngOnInit()
方法都没有被调用到。虽然angular会将快照中参数id
的值更新为2,但没有将这个更新通知到组件。为解决这个问题,ActivedRoute
服务提供了一个Observable
对象,允许对参数的更新进行订阅。
//detail.component.ts
export class DetailComponent implements OnInit,OnDestroy{
contact_id:string;
private sub:any;
ngOnInit(){
this.sub=this._activatedRoute.params.subscribe(params=>{
this.contact_id=params['id'];
console.log('参数id的值为:'this.contact_id);
this.getById(this.contact_id);
});
}
ngOnDestroy(){
//为避免内存泄漏,在组件销毁时应该取消订阅
this.sub.unsubscribe();
}
}
Query参数
通过解析URL的query
部分也可以获取参数值。由于URL的query
部分不用于和匹配项进行匹配,因此每一个配置项可以拥有任意多个查询参数。
Query
参数也可以通过RouterLink
指令或跳转方法来赋值。
//http://localhost:4200/list?limit=5
<a [routerLink]="['/list']" [queryParams]="{limit:5}">
this._router.navigate(['/list'],{queryParams:{limit:5}});
this._router.navigateByUrl('/list?limit=5');
Query
参数的获取,需要借助于ActivatedRoute
服务提供的Observable
类型对象queryParams
来完成。
//list.component.ts
import { ActivateRoute } from '@angular/router';
export class ListComponent implements OnInit,OnDestroy{
contacts:any[];
private limit:number;
private sub:any;
constructor(private _activatedRoute:ActivatedRoute){}
ngOnInit(){
this.getContacts();
}
ngOnDestroy(){
this.sub.unsubscribe();
}
getContacts(){
this.sub=this._activatedRoute.queryParams.subscribe(params=>{
this.limit=parseInt(params['limit']);
if(this.limit){
this.contacts.splice(this.limit);
}
});
}
}
Matrix参数
页面上所有组件都可以访问Query
参数的内容,如果想精准地向某一个组件传递参数,则需要使用Matrix
参数。
子路由和附属Outlet
子路由
基本用法
一个组件可以被嵌入到另外一个组件中,从而建立起组件之间的多级嵌套关系。angular也允许一个路由组件被嵌入到另一个路由组件中,从而建立路由的多级嵌套关系。
//app.routes.ts
export const rootRouterConfig:Routes=[
{path:'detail/:id',component:DetailComponent,
children:[
{path:'',component:Annotation}, //http://localhost:4200/detail/:id
{path:'album',component:Album} //http://localhost:4200/detail/:id/album
]
}
]
Matrix参数
Matrix
参数通过在链接参数数组中插入一个对象来进行赋值。
<a [routerLink]="['/detail',this.contact_id,{after:'2017-01-01',before:'2017-01-01'},
'album',{after:'2017-02-02',before:'2017-02-02'}]">Link</a>
angular会将该对象的属性转化为以;
为分隔符的键值对,拼接到与该对象左边最近的URL分段上。
http://localhost:4200/detail/6;after=2017-01-01;before=2017-01-01/album;
after=2017-02-02;before=2017-02-02
这中在一个URL分段内使用;
分隔键值对的方式称为Matrix URI
。每一个URL分段都可以拥有任意多个键值对,每个键值对只为其所在分段服务。Matrix
参数的获取方式和Path
参数一样,可以通过ActivatedRoute
服务提供的快照和Observable
对象两种方式来获取。
附属Outlet
angular允许一个路由组件包含多个Outlet
,从而可以在一个路由组件中同时显示多个组件。其中,主要Outlet
有且仅有一个,附属Outlet
可以有任意多个,各个附属Outlet
通过不同的命名加以区分。每一个Outlet
均可以通过路由配置来指定其可以显示的组件,这使得angular可以灵活地对各个组件进行组合,从而满足不同场景的需求。
<!--detail.component.html-->
<div class="detail-contain">
<router-outlet></router-outlet>
<router-outlet name="aux"></router-outlet>
</div>
//app.routes.ts
export const rootRouterConfig:Routes=[
{ path:'detail/:id',component:DetailComponent,
children:[
//主要Outlet
{path:'',component:AnnotationComponent},
{path:'album',component:AlbumComponent},
//附属Outlet
{path:'annotation',component:AnnotationComponent,outlet:'aux'},
{path:'album',component:AlbumComponent,outlet:'aux'}
]
}
];
以id=1
为例,下表列出了各种可能的组合及其相应的URL和链接参数数组。在链接参数数组中,如果一个元素包含了outlets
属性,则表示该元素将用于为各个Outlet
进行配置项匹配。
路由拦截
angular的路由拦截,允许在从一个配置项跳转到另外一个配置项之前执行指定的逻辑,并根据执行的结果来决定是否进行跳转。angular提供了5类路由拦截:
- CanActivate,激活拦截
- CanActivateChild,用于控制是否允许激活子路由配置项
- CanDeactivate,反激活拦截
- Resolve,数据预加载拦截
- CanLoad,模块加载拦截
激活拦截与反激活拦截
激活拦截与反激活拦截用于控制是否可以激活或反激活目标配置项。
CanActivate
假设需要根据用户的登录状态来决定能否访问联系人编辑页。要实现这个功能,可以通过为联系人编辑页添加一个判断登录状态的CanActivate
拦截来实现。
首先,通过实现CanActivate
接口创建拦截服务。该接口只包含了一个canActivate()
方法,最简单的情况,当该方法返回true
时,表示允许通过CanActivate
拦截;当返回false
时,则表示不允许通过CanActivate
拦截,对目标配置项不予激活。
//can-activate-guard.ts
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
@Injectable()
export class CanActivateGuard implements CanActivate{
canActivate(){
if(/*用户已登录*/){
return true;
}else{
return false;
}
}
}
然后,在目标配置项中指定上述创建的服务作为其CanActivate
拦截服务。
//app.routes.ts
import { CanActivateGuard } from '../service/can-activate-guard';
export const rootRouterConfig:Routes=[{
path:'operate/id/:id',
component:OperateComponent,
canActivate:[CanActivateGuard]
}];
最后,将该服务注入到应用中。
//app.module.ts
import { CanActivateGuard } from '../services/can-activate-guard';
@NgModule({
providers:[CanActivateGuard]
})
export class AppModule{}
除了返回布尔值,canActivate()
方法还可以返回一个Observable
对象,当该对象触发true
时,表示允许通过拦截;触发false
时则表示不允许通过。这个特性使得CanActivate
拦截可以根据异步处理的结果来进行判断。
//can-activate-guard.ts
//...
@Injectable()
export class CanActivateGuard implements CanActivate{
canActivate(){
return new Observable<boolean>(observer=>{
observer.next(true);
observer.complete();
})
}
}
此外,angular还会给canActivate()
方法传递两个参数:
- ActivatedRouteSnapshot,表示所要激活的目标配置项,可以通过它访问配置项的相关信息。
- RouterStateSnapshot,表示应用当前所处的路由状态,其包含了当前所需的所有配置项。
//can-activate-guard.ts
import { CanActivate,ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
@Injectable()
export class CanActivateGuard implements CanActivate{
canActivate(route:ActivatedRouteSnapshot,state:RouterStateSnapshot){
//获取配置项信息
console.log(route.routeConfig);
//RouterStateSnapshot按照路由配置中的定义,
//将所需的配置项以树形结构方式组织起来
console.log(state.root);
return true;
}
}
CanActivateChild
CanActivateChild
拦截用于控制是否允许激活子路由配置项,其用法与CanActivate
拦截相似。
CanDeactivate
使用CanDeactivate
拦截的用法可分三步:
首先,通过实现CanDeactivate
接口创建拦截服务。该接口只包含了一个canDeactivate()
方法,该方法除了第一个参数为目标配置项对应组件的实例外,其余使用方式与canActivate()
方法一样。
//can-deactivate-guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate,ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
@Injectable()
export class CanDeactivateGuard implements CanDeactivate<any>{
canDeactivate(component:any,route:ActivatedRouteSnapshot,state:
RouterStateSnapshot
){
if(component.isModified()){
return true;
}else{
return false;
}
}
}
然后,在目标配置项中指定该服务作为其CanDeactivate
拦截服务。
//app.routes.ts
import { CanDeactivateGuard } from "../services/can-deactivate-guard";
export const rootRouterConfig:Routes=[{
path:"operate/id/:id",
component:Operate,
canActivate:[CanActivaateGuard],
canDeactivate:[CanDeactivateGuard]
}];
最后,将该服务注入到应用中。
//app.module.ts
import { CanDeactivateGuard } from "../services/can-deactivate-guard";
@NgModule({
providers:[CanDeactivateGuard]
})
export class AppModule {}
数据预加载拦截
数据预加载拦截适用于对数据进行预加载,当确认数据加载成功后,再激活目标配置项。
预加载过程可分四步:
首先,通过实现
Resolve<T>
泛型接口创建拦截服务。该服务只有一个resolve()
方法,用于执行数据预加载逻辑。该方法可以直接将数据返回,在异步情况下也可以通过Observable
对象触发。值得注意的是,所返回的任何数据都将存放于配置项的data
参数部分,如果没有预加载到期望的数据,只能通过代码跳转的方式来达到不激活目标配置项的目的。
//resolve-guard.ts
import { Injectable } from '@angualr/core';
import { Router,Resolve,ActivatedRouteSnapshot,RouterStateSnapshot } from '@angular/router';
import { ContactService } from '../services/contact.service';
@Injectable()
export class ResolveGuard implements Resolve<any>{
contacts:{};
constructor(private _router:Router,private _contactService:ContactService){}
resolve(route:ActivatedRouteSnapshot,state:RouterStateSnapshot){
//返回Observable对象
return this._contactService.getContactById(route.params['id']).map(
res=>{
if(res){return res;}
else{
//预加载失败,代码跳转至其他配置项
this._router.navigate(['/list']);
}
}
);
}
}
其次,在目标配置项中指定该服务作为其Resolve
拦截服务。
//app.routes.ts
import { ResolveGuard } from "../services/resolve-guard";
export const rootRouterConfig:Routes=[{
path:"operate/id/:id",
component:Operate,
canActivate:[CanActivateGuard],
resolve:{
contact:ResolveGuard
}
}];
然后,将该服务注入到应用中。
//app.module.ts
import { ResolveGuard } from "../services/resolve-guard";
@NgModule({
providers:[ResolveGuard]
})
export class AppModule {}
最后,在目标配置项所指定的组件中访问预加载的数据。
//operate.component.ts
export class OperateComponent implements OnInit{
constructor(private _activatedRoute:ActivatedRoute){}
ngOnInit(){
this._activatedRoute.data.subscribe(data=>{
console.log(data.contact);
});
}
}
模块的延迟加载
延迟加载实现
与根模块需要初始化各项路由服务不同,特性模块仅需要对其路由配置进行解析,因此子路由模块通过调用RouterModule.forChild()
方法来创建。
//operate.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { Routes,RouterModule } from '@angular/router';
import { OperateComponent } from '../widget/operate.component';
import { ContactService } from '../services/contact.service';
const operateRoutes:Routes=[
{path:"id/:id",component:OperateComponent},
{path:"isAdd/:isAdd",component:OperateComponent}
];
@NgModule({
imports:[BrowserModule,FormsModule,RouterModule.forChild(operateRoutes)],
declarations:[OperateComponent],
providers:[ContactService]
})
export class OperateModule {}
此后OperateComponent
组件便不再需要在根组件AppModule
中导入。
最后,需要对根模块的路由配置进行修改,通过配置项的loadChildren
属性来指定需要进行延迟加载的模块。
//app.routes.ts
export const rootRouterConfig:Routes=[
//OperateComponent组件的配置项已在OperateModule模块中定义,故在此删除
//{path:"id/:id",component:OperateComponent},
//{path:"isAdd/:isAdd",component:OperateComponent}
{path:'operate',loadChildren:'app/router/operate.module.ts#OperateModule'}
];
模块加载拦截
默认情况下,如果URL匹配到延迟加载的配置项,相应的特性模块便会被加载进来。如果想动态判断是否对该模块进行加载,可以使用CanLoad
拦截。
CanLoad
拦截的用法和CanActivate
等其他拦截类似,首先需要实现CanLoad
接口来创建拦截服务。由于在触发CanLoad
拦截时,相应的特性模块还未被加载,因此能传递给canLoad()
方法的只有延迟加载配置的信息。
//can-load-guard.ts
import { Injectable } from '@angular/core';
import { CanLoad,Route } from '@angular/router';
@Injectable()
export class CanLoadGuard implements CanLoad{
canLoad(route:Route){
//route参数为延迟加载配置项
console.log(route.path);//输出operate
if(/*允许加载*/){
return true;
}else{
return false;
}
}
}
//app.routes.ts
import { CanLoadGuard } from '../services/can-load-guard';
export const rootRouterConfig:Routes=[{
path:'operate',
loadChildren:'app/router/operate.module.ts#OperateModule',
canLoad:[CanLoadGuard]
}];
//app.module.ts
import { CanLoadGuard } from '../services/can-load-guard';
@NgModule({
providers:[CanLoadGuard]
})
export class AppModule {}