一、简介
- Angular 的路由服务是一个可选的服务,它用来呈现指定的 URL 所对应的视图。它并不是Angular 核心库的一部分,而是位于 @angular/router 包中。像其他 Angular 包一样,路由服务在用户需要时才从此包中导入。
[1]. 创建路由模块
- 默认情况下,用户在使用Angular CLl 命令
ng new
构建 Web 应用程序时系统会提示是否需要路由服务功能,用户可以在命令后添加选项参数--routing
来指定需要路由服务功能。当选择需要路由服务功能时,Angular CLl 命令将会生成一个独立的路由模块文件,文件名默认为app-routing.module.ts
。 - 在生成的
app-routing.module.ts
文件中可以看出,AppRoutingModule
类由@NgModule()
装饰器声明,说明它是一个NgModule
类,我们称它为 Web 应用程序的路由模块。Web 应用程序的路由模块用于封装路由器配置,它可以在根模块和特性模块级别上使用。 - Web 应用程序的路由模块具有以下特征:
- 路由模块不需要 declarations 属性,即不需要声明组件、指令和管道。
RouterModule.forRoot(routes)
方法将会注册并返回一个全局的 RouterModule 单例对象imports 元数据导入这个单例对象。- exports 元数据导出 RouterModule 单例对象,这里是专门提供给根模块导入的。
- 路由模块最终由根模块导入。执行
ng new
命令时,Angular 已经帮我们在根模块(src/app/app.module.ts
)的imports 元数据中导入了路由模块,这是一个默认选项。
-
使用命令创建一个新的带路由的项目
ng new demo-route --routing
-
在
src/app
路径下将会生成一个app-routing.module.ts
文件import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = []; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
-
在
src/app/app.module.ts
文件中将会引入路由模块import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; // 引入路由模块 import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AppRoutingModule, // 导入 ], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
[2]. 理解路由服务
- 在
src/appapp-routing.module.ts
文件中可以看到AppRoutingModule 类代码中引用了 Routes 和 RouterModule 对象,它们都是@angular/router
包中导入的系统路由对象。Routes 类用于创建路由配置; RouterModule 也是一个独立的 NgModule 类,用于为用户提供路由服务,这些路由服务包括在 Web 应用程序视图之间进行导航的指令。RouterModule 类中提供了路由服务,该路由服务是全局的一个单例服务,同时还提供了一些路由指令,如 RouterOutlet 和 routerLink 等路由指令。 - AppRoutingModule 类中导出了 RouterModule 对象,Web 应用程序的根模块中导入了AppRoutingModule 类,即导入了 RouterModule 对象。RouterModule 对象注册了一个全局的路由服务,该路由服务让 Web 应用程序的根组件可以访问各个路由指令。
- 如果在特性模块中需要使用路由指令,那么需要在特性模块中导入 RouterModule 模块,这样它们的组件模板中才能使用这些路由指令。
- RouterModule 对象有一个
forChild()
方法,该方法可以传入 Route 对象数组。尽管forChild()
和forRoot()
方法都包含路由指令和配置,但是forRoot()
方法可以返回路由对象。由于路由服务会改变浏览器的 Location 对象(可以理解为地址栏中的 URL),而Location 对象又是一个全局单例对象,所以路由对象也必须是全局单例对象。这就是在根模块中必须只使用一次forRoot()
方法的原因,特性模块中应当使用forChild()
方法。 - 另外需要注意:导入模块的顺序很重要,尤其是路由模块。因为当 Web 应用程序中有多个路由模块时,路由器会接受第一个匹配路径的路由,所以应将 AppRoutingModule 类放置在根模块的 imports 元数据中的最后一项。
二、简单的路由配置
[1]. 基本路由配置
-
Route 对象数组中的每个 Route 对象都会把一个 URL 映射到一个组件。 Route 对象是一个接口类型,它支持静态、参数化、重定向和通配符路由,以及自定义路由数据和解析方法。该接口中的 path 属性用来映射 URL。路由器会先解析 path 属性,然后构建最终的 URL,这样允许用户使用相对或绝对路径在 Web 应用程序的多个视图之间导航。path 属性的值需要满足以下规则。
- path 属性的值的类型是一个字符串,字符串不能以斜杠“/”开头。
- path 属性的值可以为空“‘’”,表示Web 应用程序的默认路径,通常是 Web 应用程序的首页地址。
- path 属性的值可以使用通配符字符串“**”。如果请求的 URL 与定义路由的任何路径都不匹配,则路由器将选择此路由。
- 如果请求的 URL 找不到匹配项,那么一般要求显示的配置为类似“Not Found”的视图或重定向到特定视图。
- 路由配置的顺序很重要,路由器仅会接受第一个匹配路径的路由。
const routes: Routes = [ // 路由中的空路径“”表示 Web 应用程序的默认路径,当 URL 为空时就会访问。默认路 //由会重定向到路径“/main”,显示其对应的 DashboardComponent 组件内容 { path: '', redirectTo: '/main', pathMatch: 'full' }, // 当URL为“/main'”时,路由将会显示 DashboardComponent 组件的内容 { path: 'main', component: DashboardCOmponent }, // 路由中的“**”路径是一个通配符。当所请求的 URL 不配前面定义的任何路径时,路由器就会选择此路由。 {path: '**', component: PageNotFoundComponent} ];
[2]. 路由器出口 router-outlet
-
路由器出口(Router Outlet)是一个来自
RouterModule
模块类的指令,它的语法类似于模板中的插值表达式。它扮演一个占位符的角色,用于在模板中标出一个位置,路由器将会在这个位置显示对应的组件内容。简单地说,前面我们介绍的路由配置中的组件内容都将在这个占位符中显示。路由器出口指令的用法如下。<router-outlet></router-outlet>
-
在由 Angular CLI命令 ng new 构建的 Web 应用程序中,可以在根模块中找到路由器出口标签(默认是:src/app/app.component.html)。当完成了路由配置,有了渲染组件的路由器出口后,用户可以在浏览器中输入URL。当 URL满足匹配的路由配置规则时,其对应的组件内容将显示在路由器出口的位置。
-
使用命令
ng g c first-cpn
和ng g c second-cpn
分别创建组件first-cpn.component.ts
和second-cpn.component.ts
,如果是手动创建的这两个文件需要添加到app.module.ts
文件中 -
first-cpn.component.ts
文件内容如下import { Component } from '@angular/core'; @Component({ selector: 'app-first-cpn', template: `<p>first 组件</p> <button routerLink="../second-cpn">跳到 second 组件</button>`, }) export class FirstCpnComponent {}
-
second-cpn.component.ts
文件内容如下import { Component } from '@angular/core'; @Component({ selector: 'app-second-cpn', template: `<p>second 组件</p> <button routerLink="../first-cpn">跳到 first 组件</button>`, }) export class SecondCpnComponent {}
-
app.component.html
文件内容如下<router-outlet></router-outlet>
-
在
app-routing.module.ts
文件中配置路由import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { FirstCpnComponent } from './first-cpn/first-cpn.component'; import { SecondCpnComponent } from './second-cpn/second-cpn.component'; const routes: Routes = [ { path: '', redirectTo: '/first-cpn', pathMatch: 'full', }, { path: 'first-cpn', component: FirstCpnComponent, }, { path: 'second-cpn', component: SecondCpnComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {}
[3]. 命名路由器出口(多个路由出口)
-
在实际应用中,有时会遇到类似这样的问题:在某个页面弹出一个对话框,然后要求在 Web应用程序中的不同页面之间切换时,这个对话框也始终保持打开状态,直到对话框完成任务或者用户手动取消。显然,这个对话框的 URL 在设计上应该是对应不同的路由,而主路由出口在同一时间仅支持一个路由。Angular 提供了命名路由出口来解决类似这样的问题。
-
命名路由出口相对主路由出口来说,一般称为第二路由。同一个模块视图可以有多个命名路由出口,这些命名路由出口可以在同一时间显示来自不同路由的内容。第二路由就是在路由器出口标签中增加了一个 name 属性,代码如下。
<router-outlet name="popup"></router-outlet>
[4]. 使用路由器链接
-
Angular 中提供了路由器链接指令 routerLink 用于实现相同的导航功能。由于Angular 是单页面应用程序,在 Web 应用程序中不应重新加载页面,因此 routerink 指令导航到新的 URL,在不重新加载页面的情况下,将新组件的内容替换为路由器出口标签。routerLink 指令的简单用法如下。
<a routerLink="/users">Users</a>
-
当用户单击路由器链接时,路由器会先找到路由配置中的 path 为“/users”的组件,然后将其内容渲染在路由出口标签位置。
-
routerLink 指令还包含以下一些属性。
- queryParams属性:负责给路由提供查询参数,这些查询参数以键/值对([k: string]:any)的方式出现,跳转过去就类似于/user?id=2。
- skipLocationChange 属性:内容跳转,路由保持不变。换句话说,就是停留在上一个页面的 URL 而不是新的 URL。
- fragment属性:负责定位客户端页面的位置,值是一个字符串。以“#”附加在 URL 的末尾,如
/second-cpn?debug=true#education
。
<a [routerLink]="['/second-cpn']" [queryParams]="{ debug: true }" fragment="education">跳转到secondCpn页面</a>
-
假设有这样的路由配置:
[{ path:'user/:name',component: UserComponent}]
如果要链接到user/:name
路由,使用 routerink 指令的具体写法如下。- 如果该链接是静态的,可以使用
<a routerLink=" /user/bob ">
链接到 user 组件</a>
。 - 如果要使用动态值来生成该链接,可以传入一组路径片段,如
<a routerLink="[/user,userName] ">链接到 user 组件</a>
,其中 userName 是个模板变量。
- 如果该链接是静态的,可以使用
-
路径片段也可以包含多组,如
[/team',teamld,'user',userName,{details: true}]
表示生成一个到/team/11/user/bob;details=true
的链接。这个多组的路径片段可以合并为一组,如['/team/11/user',userName, {details: true}]
.
[5]. 路由链接的激活状态
-
单击 routerLink 指令中的链接,意味着当前的路由链接被激活,
routerLinkActive
指令会在宿主元素上添加一个 CSS 类。因此 Angular 中的routerLinkActive
指令一般和routerLink
指令配合使用,代码如下。<a routerLink="/user/bob" routerLinkActive="active">跳转到 bob 组件</a>
-
当 URL 是
/user
或/user/bob
时,当前的路由链接为激活状态,active 样式类将会被添加到<a>标签上。如果 URL 发生变化,则 active 样式类将自动从 <a>标签上移除。 -
默认情况下,路由链接的激活状态会向下级联到路由树中的每个层级,所以父、子路由链接可能会被同时激活。由于上述代码片段中
/user
是/user/bob
的父路由,因此它们当前的路由链接状态都会被激活。要覆盖这种行为,可以设置routerLinkActive
指令中的routerLinkActiveOptions
属性值为"{exact: true}"
,这样只有当 URL 与当前 URL精确匹配时路由链接才会被激活。routerLinkActiveOptions 属性的用法如下<a routerlink="/user/bob" routerlinkActive="active" [routerLinkActiveOptions]="{ exact: true }">跳转到 bob 组件</a>
-
使用命令
ng g c first-cpn
和ng g c second-cpn
和ng g c third-cpn
分别创建组件first-cpn.component.ts
和second-cpn.component.ts
和third-cpn.component.ts
,如果是手动创建的这三个文件需要添加到app.module.ts
文件中 -
first-cpn.component.ts
文件内容如下import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-first-cpn', template: ` <p>first</p> `, }) export class FirstCpnComponent { }
-
second-cpn.component.ts
文件内容如下,注意:该组件有一个路由出口router-outlet
用于显示其子组件import { Component } from '@angular/core'; @Component({ selector: 'app-second-cpn', template: `<p>second</p> <router-outlet></router-outlet>`, }) export class SecondCpnComponent {}
-
third-cpn.component.ts
文件内容如下,注意:该组件将要作为second-cpn.component.ts
组件的子组件import { Component } from '@angular/core'; @Component({ selector: 'app-second-cpn', template: `<p>second</p> <router-outlet></router-outlet>`, }) export class SecondCpnComponent {}
-
路由模块
src/app/app-routing.module.ts
文件如下import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; // 引入三个组件 import { FirstCpnComponent } from './first-cpn/first-cpn.component'; import { SecondCpnComponent } from './second-cpn/second-cpn.component'; import { ThirdCpnComponent } from './third-cpn/third-cpn.component'; const routes: Routes = [ { path: '', redirectTo: 'first-cpn', pathMatch: 'full', }, { path: 'first-cpn', component: FirstCpnComponent, }, { path: 'second-cpn', component: SecondCpnComponent, children: [ { path: 'third-cpn', // 作为second-cpn 组件的子组件 component: ThirdCpnComponent, }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {}
-
src/app/app.component.html
文件如下<nav> <a routerLink="./first-cpn" routerLinkActive="active" >跳转到 firstCpn 组件</a> | <a routerLink="./second-cpn" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">跳转到 secondCpn 组件</a> | <a [routerLink]="['/second-cpn/third-cpn']" routerLinkActive="active">跳转到 thirdCpn 组件</a> | </nav> <router-outlet></router-outlet>
-
点击三个链接,跳转到不同页面,
third-cpn
组件将在second-cpn
组件的路由出口中显示。
三、路由状态
- 由 Angular 开发的 Web 应用程序的页面是由若干个组件视图组成的,当 Web 应用程序在组件之间导航时,路由器使用页面上的路由器出口来呈现这些组件,然后在 URL 中反映所呈现的状态。换句话说,一个URL 将对应若干个可呈现或可视化的组件视图。我们称 Web 应用程序中所有的这些可视化的组件视图及其排列为路由器状态。为此,路由器需要用某种方式将 URL 与要加载的可视化的组件视图相关联。Angular 中定义了一个配置对象来实现此目标,这个配置对象不仅维护着路由器状态,而且描述了给定 URL 呈现哪些组件。
[1]. 路由器状态和激活路由状态
- 路由器状态在 Angular 中用
RouterState
对象表示,RouterState 对象维护的是一个路由器状态树,表示所有的路由器状态。Angular 中用ActivatedRoute
对象表示激活路由状态。因此,RouterState
对象中包含了ActivatedRoute
对象。
[2]. ActivatedRoute 对象及其快照对象
- 每个 ActivatedRoute 对象都提供了从任意激活路由状态开始向上或向下遍历路由器状态树的一种方式,以获得关于父、子、兄弟路由的信息。在 Web 应用程序中,我们可以通过注入
ActivatedRoute
对象来获取当前路由的相关数据信息,ActivatedRoute
对象也可用于遍历路由器状态树。通过ActivatedRoute
对象获取路由的数据信息的方式主要有两种:一种是通过snapshot
属性,获取当前路由的快照对象,快照对象的类型是ActivatedRouteSnapshot
类型;另一种是直接通过params
属性获取,它返回的是一个Observable<Params>
对象类型。 - ActivatedRoute 对象和其快照对象的区别如下。
- 每当导航添加、删除组件或更新参数时,路由器就会创建一个新的快照对象。
- 快照对象是一个不变的数据结构,它仅表示路由器在特定时间的状态。在 Web 应用程序中快照对象的表现方式是,该数据结构在组件的生命周期中仅执行一次,如在
ngOninit()
方法中执行一次,代表某一时刻的一个激活路由状态的快照版本。 ActivatedRoute
对象类似于快照对象,不同之处在于它表示路由器随时间变化的状态,换句话说,它是一个可观察的数据流对象(Observable 类型 )。因此,在Web 应用程序中需要通过订阅的方式获取其值,进而要求取消订阅(Unsubscrib),甚至要求实现销毁方法。如OnDestroy()
。ActivatedRoute
对象的snapshot
属性返回的值是ActivatedRouteSnapshot
类型的快照对象。- 在实际应用中,ActivatedRoute 对象可以返回可观察(Observable)对象,只要路由状态发生了变化,订阅在 ActivatedRoute 对象上的方法都会再次执行,直到取消订阅为止。这也是Angular 编程中的核心亮点之一。
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent implements OnInit { constructor(private route: ActivatedRoute) {} ngOnInit() { // 快照 console.log(this.route.snapshot); // 订阅 this.route.data.subscribe((data) => { console.log(data); }); } }
-
src/app/app.component.html
文件如下import { Component } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-root', template: `<div>{{ title }}</div> <button (click)="goto('home')">跳转到 Home</button> <button (click)="goto('produce')">跳转到 Produce</button> `, }) export class AppComponent { title = '路由用例'; constructor(private router: Router, private route: ActivatedRoute) { console.log('该信息只执行一次:title = ' + this.title); let params = this.route.snapshot.fragment; console.log('该信息只执行一次:fragment = ' + params); // 订阅 ActivatedRoute 对象的 fragement 属性的返回值 this.route.fragment.subscribe((fragment: string) => { console.log('订阅信息:' + fragment); }); } goto(path) { // 调用 Router 对象的 navigate 方法进行导航,设置不同的 # 片段 this.router.navigate(['/'], { fragment: path }); } }
-
在浏览器的控制台中的输出如下,注意浏览器 url 的变化
四、路由器触发事件(路由器生命周期)
-
与组件生命周期类似,路由器也有生命周期。在路由的导航周期中,路由器会触发一系列事件。我们可以通过在 RouterModule.forRoot()方法中添加{enableTracing: true} 参数来查看路由器触发的事件
@NgModule({ imports: [ RouterModule.forRoot(routes, { enableTracing: true, // 控制台输出所有路由器出发的事件 }), ], exports: [RouterModule], }) export class AppRoutingModule {}
-
在浏览器的控制台中可以看到如下信息:
-
在路由的导航周期中,一些值得注意的事件如下:
- NavigationStart 事件:表示导航周期的开始。
- NavigationCancel事件:取消导航,如可用在路由守卫(Route Guards)中,拒绝用户访问此路由。
- RoutesRecognized 事件:当URL与路由匹配时,触发此事件。
- NavigationEnd 事件:在导航成功结束时触发。
-
使用命令
ng g c first-cpn
和ng g c second-cpn
分别创建组件first-cpn.component.ts
和second-cpn.component.ts
,如果是手动创建的这三个文件需要添加到app.module.ts
文件中 -
first-cpn.component.ts
文件内容如下import { Component, OnInit, OnDestroy } from '@angular/core'; import { Router, RoutesRecognized } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-first-cpn', template: ` <p>first</p> `, }) export class FirstCpnComponent implements OnInit, OnDestroy { private routeSubscription: Subscription; constructor(private router: Router) {} ngOnInit(): void { this.routeSubscription = this.router.events .pipe(filter((event) => event instanceof RoutesRecognized)) .subscribe((event: RoutesRecognized) => { console.log('当前路由状态:', event.state); }); } ngOnDestroy(): void { // 取消订阅 this.routeSubscription.unsubscribe(); } }
-
second-cpn.component.ts
文件内容如下,注意:该组件有一个路由出口router-outlet
用于显示其子组件import { Component } from '@angular/core'; @Component({ selector: 'app-second-cpn', template: `<p>second</p>`, }) export class SecondCpnComponent {}
-
路由模块
src/app/app-routing.module.ts
文件如下import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; // 引入两个组件 import { FirstCpnComponent } from './first-cpn/first-cpn.component'; import { SecondCpnComponent } from './second-cpn/second-cpn.component'; const routes: Routes = [ { path: '', redirectTo: 'first-cpn', pathMatch: 'full', }, { path: 'first-cpn', component: FirstCpnComponent, }, { path: 'second-cpn', component: SecondCpnComponent, }, ]; @NgModule({ imports: [ RouterModule.forRoot(routes, { enableTracing: true, // 控制台输出所有路由器出发的事件 }), ], exports: [RouterModule], }) export class AppRoutingModule {}
-
src/app/app.component.html
文件如下<nav> <a routerLink="./first-cpn" routerLinkActive="active" >跳转到 firstCpn 组件</a> | <a routerLink="./second-cpn" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">跳转到 secondCpn 组件</a> </nav> <router-outlet></router-outlet>
-
点击两个链接,跳转到不同页面,当路由处于
RoutesRecognized
状态时,会触发订阅事件
五、在路由中传递参数
[1]. 传递配置参数
-
在配置路由时,用户可以通过路由配置中的 Route 对象的 data 属性来存放与每个具体路由有关的数据。该数据可以被任何一个 ActivatedRoute 对象访问,一般用来保存如页面标题、导航以及其他静态只读数据。
const routes: Routes = [ { path: 'first', component: FirstComponent, data: { title: 'First Page' } }, { path: 'second', component: SecondComponent, data: { title: 'Second Page' }, }, { path: 'third', component: Thirdcomponent, data: { title: 'hird page' } }, { path: '**', redirectTo: 'first' }, ];
-
data 属性接收一个键/值对([name: string]: any)类型的数据对象;有多个键/值对时,以逗号分隔。在代码中,参数可通过 ActivatedRoute 对象的 Snapshot 属性获取。
constructor(private actRoute: ActivatedRoute) { // 通过ActivatedRoute对象的snapshot属性获取参数 this.title = this.actRoute.snapshot.data['title']; // 通过订阅获取 this.actRoute.data.subscribe((data) => { console.log(data.title); }); }
[2]. 传递路径参数
我们可以将路径参数作为 URL路径的一部分传递给路由配置中的组件。路径参数分为必选参数和可选参数,这涉及路由的定义。
1). 传递必选参数
-
必选参数在 URL中的格式如
localhost:4200/user/123
,其中 123 就是传递的必选参数。必选参数在路由配置中是这么定义的。{ path:"user/:id',component:UserComponent}
-
上面的代码创建了一个包含必选参数 id 的路由,这个路由中的“:id”相当于在路径中创建了一个空位,这个空位不补全是无法导航的。
this.router.navigate(['/user']);//跳转错误,无效路由 this.router.navigate(['/user', 123]);//正确跳转,跳转URL为/user/123
-
在模板视图中,含有必选参数的路由是这么定义的,代码如下。
<a [routerLink]="['/user',userId]">链接到user组件</a>
-
在代码中,通过路由对象的 navigate()方法可导航到含有必选参数的路由。
import {Router, ActivatedRoute} from '@angular/router'; constructor(private router: Router, private actRoute: ActivatedRoute){} gotoUser() { this.router.navigate(['/user',user.id]);//导航到user/123的路由 } ngOnInit(){ this.user_id=this.actRoute.snapshot.params.id;// 通过快照对象的方式获取值 // 通过订阅获取 this.actRoute.params.subscribe((data)=>{ this.user_id = data.id; }) }
-
用例参考下边的传递可选参数
2). 传递可选参数
-
可选参数在 URL 中的格式如
localhost:4200/users;a=123;b=234
,其中 a=123;b=234 就是传递的可选参数。可选参数是 Web 应用程序在导航期间传递任意信息的一种方式。 -
和必选参数一样,路由器也支持通过可选参数导航。在实际应用中一般是先定义完必选参数之后,再通过一个独立的对象来定义可选参数。 可选参数不涉及模式匹配,并在表达上具有巨大的灵活性。通常,对强制性的值(如用于区分两个路由路径的值)使用必选参数;当这个值是可选的、复杂的或多值的时,使用可选参数。可选参数的导航方式在 Web 应用程序中是这样的。
// 正确跳转。不涉及模式匹配,参数可传可不传 this.router.navigate(['/user']); //正确跳转,跳转URL为 localhost:4200/users;a=123;b=234 this.router.navigate(['/user',{a:123,b:234}]);
-
在模板视图中,含有可选参数的路由是这么定义的,代码如下,
<a [routerLink]="['/users',{a:123, b: 234}]">返回</a>
-
在代码中,通过路由对象的 navigate()方法可导航到含有可选参数的路由。
import { Router, ActivatedRoute} from '@angular/router'; constructor(private router: Router, private actRoute: ActivatedRoute) {} //导航到localhost:4200/users;a=123;b=234的路由 gotoUser() { this.router.navigate(['/users', {a:123, b:234}]); } ngOnInit() { this.actRoute.paramMap .pipe(map((params) => params.get('a'))) .subscribe((data) => { console.log('a', data); }); }
-
使用命令
ng g c first-cpn
和ng g c second-cpn
分别创建组件first-cpn.component.ts
和second-cpn.component.ts
,如果是手动创建的这三个文件需要添加到app.module.ts
文件中 -
src/app/app.component.html
文件如下, 跳转到 secondCpn 组件携带必选参数,跳转到 first组件携带可选参数<nav> <a [routerLink]="['/second-cpn', 123]">必选参数到 secondCpn 组件</a> | <a [routerLink]="['/first-cpn', { a: 123, b: 234 }]">可选参数到 first 组件</a> </nav> <router-outlet></router-outlet>
-
first-cpn.component.ts
文件内容如下import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-first-cpn', template: `<button (click)="gotoFirstCpn()"> 携带参数跳转到secondCpn组件 </button>`, }) export class FirstCpnComponent implements OnInit { constructor(private router: Router, private actRoute: ActivatedRoute) {} ngOnInit() { // this.actRoute.paramMap // .pipe(map((params) => params.get('a'))) // .subscribe((data) => { // console.log('a', data); // }); this.actRoute.params.subscribe((data) => { // {a: '123', b: '234'} console.log(data); }); } //导航到second-cpn/123的路由 gotoFirstCpn() { this.router.navigate(['/second-cpn', 123]); } }
-
second-cpn.component.ts
文件内容如下,注意:该组件有一个路由出口router-outlet
用于显示其子组件import { Component } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-second-cpn', template: `<p>{{ user_id }}</p> <button (click)="gotoFirstCpn()">携带可选参数到firstCpn组件</button>`, }) export class SecondCpnComponent { user_id: number | string; constructor(private router: Router, private actRoute: ActivatedRoute) {} ngOnInit() { this.user_id = this.actRoute.snapshot.params.id; // 通过快照对象的方式获取值 console.log(this.user_id); } gotoFirstCpn() { this.router.navigate(['/first-cpn',{a:123,b:234}]); } }
-
路由模块
src/app/app-routing.module.ts
文件如下import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; // 引入两个组件 import { FirstCpnComponent } from './first-cpn/first-cpn.component'; import { SecondCpnComponent } from './second-cpn/second-cpn.component'; const routes: Routes = [ { path: '', redirectTo: 'first-cpn', pathMatch: 'full', }, { path: 'first-cpn', component: FirstCpnComponent, }, { path: 'second-cpn/:id', component: SecondCpnComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {}
-
点击页面的链接或按钮,跳转到不同页面,浏览器控制台会显示各自获取到的数据
3). 传递查询参数
-
查询参数在URL中的格式如
localhost:4200/user?id=123
,其中id=123就是传递的查询参数从技术角度讲,查询参数类似可选参数,也不涉及模式匹配,并在表达上具有巨大的灵活性。 -
查询参数的导航方式在 Web 应用程序中是这样的。
// 正确跳转。不涉及模式匹配,参数可传可不传 this.router.navigate(['/user']); // 正确跳转,跳转URL为10calhost:4200/use?id=123 this.router.navigate(['/user'],{ queryparams:{id:123} });
-
Router 对象的 navigate()方法的定义如下。
navigate(commands: any[], extras?: NavigationExtras): Promise<boolean>;
- navigate()方法的第二个参数类型是 NaviqationExtras 接口,它的定义如下
export declare interface NavigationExtras { relativeTo?: ActivatedRoute | null; queryParams?: Params | null; fragment?: string; preserveQueryParams?: boolean; queryParamsHandling?: QueryParamsHandling | null; preserveFragment?: boolean; skipLocationChange?: boolean; replaceUrl?: boolean; state?: { [k: string]: any; }; }
-
通过 fragment 关键字传递参数。将会生成链接
/user/bob#education
this,router,navigate(['/user',user.name],{ fragment: 'education'});
-
queryParamsHandling
参数的含义:是否需要将当前 URL中的查询参数传递给下一个路由,有三个参数。- merge:这是默认选项,将新的查询参数与现有的查询参数合并。如果有相同的参数名,则新的值将覆盖旧的值。
- preserve: 保留现有的查询参数,忽略新的查询参数。这意味着导航过程中不会更改现有的查询参数。
- ’ ’ (空字符串): 不处理查询参数。导航过程中不会传递任何查询参数。。
//如当前的URL是 user;a=123?code=bbb,跳转之后的链接为 /others/1?code=bbb, this.router.navigate(['/others',1], {queryParamsHandling:'preserve'});
-
页面接收查询参数
this.activatedRoute.queryParams.subscribe((data) => { this.id= data.id; });
[3]. 用例
-
使用如下命令创建一个新的带路由的项目
ng new route-demo2 --routing
-
使用如下命令创建两个组件和一个接口
# 创建组件1 ng g c user-list # 创建组件2 ng g c user-detail # 创建接口 ng generate interface user-face 的缩写 ng g i user-face
-
修改接口文件
src/app/user-face.ts
如下export interface UserFace { id: number; name: string; email: string; }
-
修改路由模块
src/app/app-routing.module.ts
如下:import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; // 引入自己创建的组件 import { UserListComponent } from './user-list/user-list.component'; import { UserDetailComponent } from './user-detail/user-detail.component'; const routes: Routes = [ // 默认路径导航到用户列表页面 { path: '', redirectTo: 'users', pathMatch: 'full', }, // 用户列表页 { path: 'users', // 子路由, children: [ { path: '', component: UserListComponent, data: { title: '用户列表页面' }, // 传递配置参数 }, { path: ':id', component: UserDetailComponent, data: { title: '用户详情页面' }, // 传递配置参数 }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {}
-
编辑用户详细组件
src/app/user-detail/user-detail.component.ts
import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-user-detail', template: ` <h4>标题:{{ title }}</h4> <div (click)="gotoUser()"> {{ userId }} | {{ userName }} | {{ userEmail }} </div> <div> <a [routerLink]="['/users', { a: 123, b: 234 }]">演示可选参数</a><br /> <a [routerLink]="['/users', { a: 123, b: 234 }]" [queryParams]="{ c: 345 }" >演示查询参数</a > </div> `, }) export class UserDetailComponent implements OnInit { title = ''; // 从路由配置参数获取 userId: string; // 路径参数:必选参数 userName: string; // 路径参数:可选参数 userEmail: string; // 注入 Router 和 ActivatedRoute 对象 constructor(private router: Router, private actRoute: ActivatedRoute) {} ngOnInit(): void { // 方式一:通过快照对象的方式获取来自路由配置的参数 // this.title = this.actRoute.snapshot.data.title; // 方式二:通过订阅的方式获取路由配置的参数 this.actRoute.params.subscribe((data) => { console.log(data); this.title = data.title; }); let params = this.actRoute.snapshot.params; const { id, name, email } = params; this.userId = id; this.userName = name; this.userEmail = email; } // 导航到含有可选参数的路由 gotoUser() { this.router.navigate(['/users', { a: 123, b: 234 }]); } }
-
编辑用户列表组件
src/app/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { UserFace } from '../user-face'; @Component({ selector: 'app-user-list', template: ` <h4>{{ title }}</h4> <div *ngFor="let item of users; let i = index"> <p> {{ i + 1 }}、<a [routerLink]="['/users/', item.id, {name: item.name, email: item.email}]">{{ item.name }}</a> </p> </div> `, }) export class UserListComponent implements OnInit { title = ''; users: UserFace[] = [ { id: 1, name: 'user001', email: 'email@user1.com' }, { id: 2, name: 'user002', email: 'email@user2.com' }, { id: 3, name: 'user003', email: 'email@user3.com' }, { id: 4, name: 'user004', email: 'email@user4.com' }, { id: 5, name: 'user005', email: 'email@user5.com' }, ]; constructor(private actRoute: ActivatedRoute) {} ngOnInit(): void { // 通过快照对象的方式获取来自路由配置的参数 this.title = this.actRoute.snapshot.data.title; // 通过快照对象的方式获取来自路由路径的可选参数 let params = this.actRoute.snapshot.params; // 订阅来自路由路径的可选参数 this.actRoute.paramMap .pipe(switchMap((params) => of(params.get('a')))) .subscribe((data) => { console.log('a', data); }); // 方式一:订阅来自路由路径的查询参数 this.actRoute.queryParamMap .pipe(switchMap((params) => of(params.get('c')))) .subscribe((data) => { console.log('c', data); }); // // 方式二:订阅来自路由路径的查询参数 // this.actRoute.queryParams.subscribe((data) => { // console.log(data); // console.log('c', data.c); // }); } }
-
编辑跟组件
src/app/app.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: `<router-outlet></router-outlet>`, }) export class AppComponent {}
-
启动项目
ng serve --open
六、路由守卫
[1]. 概述
- Angular 中一共提供了5种不同类型的路由守卫,每种路由守卫都按特定的顺序被调用。路由器会根据使用路由守卫的类型来调整路由的具体行为,这5种路由守卫如下。
- CanActivate 守卫,用来处理导航到某路由的逻辑。
- CanActivateChild 守卫,用来处理导航到某子路由的逻辑。
- CanDeactivate 守卫,用来处理从当前路由离开的逻辑。
- Resolve 守卫,用来在路由激活之前获取业务数据。
- CanLoad 守卫,用来处理异步导航到某特性模块的逻辑。
- 使用如下命令生成路由守卫:执行命令时,终端窗口将提示用户选择需要实现哪种类型的路由守卫;用户也可以在命令行中添加选项参数
--implements=CanActivate
来指定要实现的路由守卫类型。ng generate guard 文件名 [参数]
- 生成的文件路径:
src/app/文件名.guard.ts
, 用例的文件名为my-can-activate
@Injectable()
装饰器用来提供依赖注入服务,元数据providedIn
属性的值为 root,表示注入的服务是全局的单例服务,即 MyCanActivateGuard 类的服务可以在根模块或者其他模块中调用。- 由于执行命令时,选择的是CanActivate守卫,因此MyCanActivateGuard类实现 Canctivate 守卫。CanActivate守卫中默认canActivate()方法注入了两个参数
ActivatedRouteSnapshot
类型的 next 参数和RouterStateSnapshot
类型的 state 参数。该方法的返回值是boolean | UrTree
类型的3种形式之一(Observable、Promise 和基本类型 )。初始化代码中没有任何业务逻辑,默认返回 true。
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class MyCanActivateGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { return true; } }
[2]. 配置路由守卫
-
有了路由守卫之后,用户可以在路由配置中添加路由守卫来守卫路由。路由守卫返回一个值,以控制路由器的行为。
- 如果它返回 true,导航过程会继续。
- 如果它返回 false,导航过程就会终止,且用户留在原地。
- 如果它返回 UrlTree(Angular 中提供的解析 URL 的对象),则取消当前的导航,并且导航到返回的这个 UrlTree。
-
在路由模块中,Route 接口提供了属性供配置具体的路由守卫
interface Route { path?: string; pathMatch?: string; matcher?: UrlMatcher; component?: Type<any>; redirectTo?: string; outlet?: string; canActivate?: any[]; // 配置 CanActivate 守卫 canActivateChild?: any[]; // 配置 CanActivateChild 守卫 canDeactivate?: any[]; // 配置 CanDeactivate 守卫 canLoad?: any[]; // 配置 CanLoad 守卫 data?: Data; resolve?: ResolveData; // 配置 Resolve 守卫 children?: Routes; loadChildren?: LoadChildren; runGuardsAndResolvers?: RunGuardsAndResolvers; }
-
以canActivate 属性为例,它接收的是一个数组对象,因此可以配置一个或者多个 CanActivate守卫。配置一个 CanActivate 守卫的代码如下。其他路由守卫的配置与 CanActivate 守卫的配置类似。
{ path: 'users', children: [ { path: '', component: UserListComponent, data: { title: '用户列表页面' }, }, { path: ':id', component: UserDetailComponent, data: { title: '用户详细页面' }, canActivate: [MyCanActivateGuard], // 配CanActivate守卫 }, ], },
-
如果配置了多个路由守卫,那么这些路由守卫会按照配置的先后顺序执行。如果所有路由守卫都返回 true,就会继续导航。如果任何一个路由守卫返回了 false,就会取消导航。如果任何一个路由守卫返回了 UrlTree,就会取消当前导航,并导航到这个路由守卫所返回的 UrlTree。
[3]. CanActivate 守卫应用
CanActivate
是一个实现CanActivate
接口的路由守卫,该路由守卫决定当前路由能否激活。如果CanActivate
守卫返回 true,就会继续导航,如果返回了 false,就会取消导航。如果返回了UrITree,就会取消当前导航,并转向导航到返回的 UrlTree。CanActivate
接口中有个canActivate()
方法,该接口的定义如下。interface CanActivate { canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree; }
canActivate()
方法中注入了两个类型的参数,可以直接在方法中调用这些参数的属性或方法完成具体的业务逻辑。这两个参数的类型分别是RouterStateSnapshot
和ActivatedRouteSnapshot
,我们已经知道RouterState
对象维护的是一个全局路由器状态树,ActivatedRoute
对象维护的是激活路由状态树。那么,RouterStateSnapshot
和ActivatedRouteSnapshot
代表的是这两个状态树的瞬时状态。canActivate()
方法的返回值是boolean | UrTree
类型的3 种形式之一。CanActivate
守卫一般用来对用户进行权限验证,如判断是否是登录用户、判断凭证是否有效等。
[4]. CanActivateChild 守卫应用
-
CanActivateChild
守卫实现CanActivateChild
接口,该路由守卫决定当前路由的子路由能否被激活。CanActivateChild
守卫的应用场景与CanActivate
守卫类似,不同之处在于CanActivate
守卫保护的是当前路由,而CanActivateChild
守卫配置在父路由上,对它的子路由进行保护。{ path: 'users', canActivate: [MyCanActivateChildGuard], // 配CanActivateChild守卫 children: [ { path: '', component: UserListComponent, data: { title: '用户列表页面' }, }, { path: ':id', component: UserDetailComponent, data: { title: 用户详细页面 }, canActivate: [MyCanActivateGuard], // 配CanActivate守卫 }, ], },
[5]. CanDeactivate 守卫应用
-
CanDeactivate
守卫实现CanDeactivate
接口,该路由守卫用来处理从当前路由离开的逻辑,应用的场景一般是提醒用户执行保存操作后才能离开当前页面。CanDeactivate
接口中有canDeactivat()
方法,该接口的定义如下。interface CanDeactivate<T> { canDeactivate( component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree; }
-
canDeactivate()
方法的第一个参数就是CanDeactivate
接口指定的泛型类型的组件,可以直接调用该组件的属性或方法,如根据要保护的组件的状态,或者调用方法来决定用户是否能够离开。
[6]. Resolve 守卫应用
-
Resolve
守卫实现Resolve
接口,该路由守卫用来在路由激活之前获取业务数据。Resolve
接口中有resolve()
方法,该接口的定义如下。interface Resolve<T> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T; }
-
该路由守卫一般应用在 HTTP 请求数据返回有延迟,导致模板视图无法立刻显示的场景中。如HTTP 请求数据返回之前模板上所有需要用插值表达式显示值的地方都是空的,这会造成用户体验不佳。Resolve 守卫的解决办法是,在进入路由之前 Resolve 守卫先去服务器读数据,把需要的数据都读好以后,带着这些数据再进入路由,立刻把数据显示出来。
-
resolve()
方法返回的值是泛型类型,它一般对应着组件视图中的数据对象。该数据对象存储在路由器状态中,在组件类中可以通过下面的方式获取。constructor(private route: ActivatedRoute) {} ngOnInit() { // 通过订阅的方式获取resolve() 方法返回的值 this.route.data.subscribe(data => this.users = data.users); }
[7]. CanLoad 守卫应用
-
CanLoad
守卫实现CanLoad
接口,该路由守卫用来处理异步导航到某特性模块的逻辑。CanLoad
接口中有canLoad()
方法,该接口的定义如下。interface CanLoad { canLoad(route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree; }
-
在业务场景中,
CanLoad
守卫用来保护对特性模块的未授权加载,如在路由配置中,配置CanLoad
守卫来保护是否加载路由。{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] }
-
上述配置中,loadChildren 属性中的语法是异步延迟加载模块,在 CanLoad 守卫中代码如下
canload(route: Route): boolean{ // route为准备访问的目的地址 let url =`${route.path}`; // 判断是否继续加载,返回boolean return this.checkLogin(url); }
六、路由器的延迟加载(异步加载)
[1]. 概述
- Angular 是通过模块来处理延迟加载的。每个 Web 应用程序都有一个名为 NgModule 类的根模块,根模块位于 Web 应用程序的 app.module.ts 文件中,并包含所有导入模块和组件声明。根模块中导入的所有模块是在编译时捆绑在一起并推送到浏览器的。默认情况下,模块的 NgModule类都是急性加载的,也就是说所有模块会在 Web 应用程序加载时一起加载,无论是否立即使用它们。因此,当 Web 应用程序想要促进延迟加载时,需要将根模块分成若干个较小的特性模块,然后仅将最重要的特性模块首先加载到根模块中。
- Angular 的路由器提供了延迟加载功能:一种按需加载根模块的模式。延迟加载本质上可以缩小初始加载包的尺寸,从而减少 Web 应用程序的初始加载时间。对配置有很多路由的大型 Web 应用程序,推荐使用延迟加载功能。
[2]. 实施延迟加载
- 所谓延迟加载是指延迟加载特性模块,因此在 Web 应用程序中除了根模块外,至少需要一个额外的特性模块。实施延迟加载特性模块有3 个主要步骤。
- (1)创建一个带路由的特性模块
- (2)删除默认的急性加载。
- (3)配置延迟加载的路由。
- 实际上,以上3 个步骤可以通过一条 Angular CLI 命令完成
- 注意区分 ng generate module 命今中的选项 --route 与 选项 --routing,前者是创建延迟加载路由,后者是创建普通的路由。如果两者同时使用,–route 选项将覆盖 --routing 选项。
ng generate module 模块名 --route 延迟加载特性模块的路径 --module app.module
-
使用
ng generate module
命令附带 --route 选项时,该命令将告诉 Angular CLl命令,新建一个延迟加载的特性模块,并且不需要在根模块中对其引用。 命令中的 --route 选项参数 featurepath 表示将生成一个路径为 featurepath 的延迟加载路由,并且将其添加到由–module 选项指定的模块声明的 routes 数组中。命令中的 --module 选项参数 app 表示在指定的模块文件(省略了扩展名,指的是 app.module.ts 模块文件)中添加延迟加载路由的配置。ng generate module my-feature --route featurepath --module app
-
打开
src/app/app-routing.module.ts
文件可以看到新创建的特性模块的路由已经添加进 routes 数组。延迟加载使用 Routed 对象的 loadChildren 属性,其后是一个使用浏览器内置的 import(…) 语去进行动态导入的函数。其导入路径是到当前模块的相对路径。import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'featurepath', loadChildren: () => import('./my-feature/my-feature.module').then(m => m.MyFeatureModule) }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {}
-
打开
src/app/my-feature/my-feature-routing.module.ts
可以看到如下内容,Angular CLI 命令把RouterModule.forRoot(routes)
方法添加到根模块路由中,而把RouterModule.forChild(routes)
方法添加到各个特性模块中。这是因为forRoot(routes)
方法将会注册并返回一个全局的单例 RouterModule 对象,所以在根模块中必须只使用一次 forRoot()方法,各个特性模块中应当使用 forChild()方法import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { MyFeatureComponent } from './my-feature.component'; const routes: Routes = [{ path: '', component: MyFeatureComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class MyFeatureRoutingModule { }
[3]. 用例
-
新建一个项目
参数参考:https://v10.angular.cn/cli/new
- -minimal: 不需要测试框架
-s:- -inlineStyle 别名,内联样式
-t:- -inlineTemplate 别名,内联模板ng new demo-load-route --routing --minimal --style=css -s -t
-
常见两个特性模块:将在 src/app 目录下生成 features 文件夹并再文件夹下生成对应的目录
#创建带路由的users模块,并且配置为延迟加载模块 ng g m features/users --route users --module app.module #创建带路由的posts模块,并目配置为延迟加载模块 ng g m features/posts --route posts --module app.module
每个命令会创建三个文件(如果新建项目时 没有带 --minimal 参数,会多生成一个 .spec.ts 结尾的测试文件)
-
此时
src/app/app-routing.module.ts
文件内容如下:import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'users', loadChildren: () => import('./features/users/users.module').then((m) => m.UsersModule) }, { path: 'posts', loadChildren: () => import('./features/posts/posts.module').then((m) => m.PostsModule) } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule {}
-
修改
src/app/app.component.ts
内容如下import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <h1>{{title}}</h1> <button routerLink="/users">用户信息</button> <button routerLink="/posts">留言信息</button> <button routerLink="/">主页</button> <router-outlet></router-outlet> `, styles: [] }) export class AppComponent { title = 'demo-load-route'; }
-
在点击按钮切换不同的模块时,可以在浏览器控制台的
网络
中看到第一次点击后才加载对应的路由(多次点击只在第一次加载)