本文介绍 Angular 中的 Routing 相关内容,基本上能够覆盖所有的知识点,它们是:
- 子级路由和二级路由
- 路由参数
- 路由相关的样式、动画和响应
- 路由守卫和 resolve
- 路由懒加载

什么样的组件适合采用懒加载的方式?如下图所示,图中红色框中的组件会被懒加载:

其实规则是显而易见的,那就是首屏不会被 100% 加载的页面都可以使用懒加载。
App 的架构图

从 address bar 地址改变到相应的组件展示的过程

- url 改变
- 匹配路径(可能会发生重定向)
- 路由守卫
- 获取数据
- 激活对应组件
- 展示组件模板
- 等待再次 url 改变
课程大纲:
- 路由基础
- 导航到 featured 组件
- 路由参数
- 使用 resolve 获取数据
- 子路由
- 路由分组以及无组件路由
- 路由相关的样式、动画;路由监视
- 备选路由
- 路由守卫
- 路由懒加载

这些内容可以用一张图说清楚:

1. 路由基础
各式各样的路由路径的书写形式。

1.1 basePath
什么是基础路由?
Angular中的basePath是一个关键配置,它涉及到Angular应用的部署和资源定位。以下是对basePath的详细介绍:
-
定义与作用:
basePath在Angular项目中通常指的是资源文件和服务的基础路径。它对于定位静态资源(如图片、样式表等)以及API服务的URL非常重要。
-
配置方式:
- 在Angular项目中,
basePath可以通过多种方式配置。一种常见的方法是在angular.json配置文件中设置assets和styles等属性的路径。此外,在构建应用时,也可以通过命令行参数(如--base-href)来指定基础路径。
- 在Angular项目中,
-
与静态资源的关系:
- 当Angular应用被构建时,所有的静态资源(如HTML、CSS、JavaScript和图片文件)都会被放置在
dist/目录下的特定子目录中。basePath确保了当这些文件被部署到非根目录时,应用仍然能够正确地找到和加载它们。
- 当Angular应用被构建时,所有的静态资源(如HTML、CSS、JavaScript和图片文件)都会被放置在
-
部署考虑:
- 在将Angular应用部署到服务器时,如果应用不是部署在服务器的根目录下,而是部署在某个子目录中,那么就需要正确设置
basePath。这样,当浏览器请求资源时,服务器能够根据basePath正确地返回相应的文件。
- 在将Angular应用部署到服务器时,如果应用不是部署在服务器的根目录下,而是部署在某个子目录中,那么就需要正确设置
-
动态设置:
- 在某些情况下,可能需要根据环境动态设置
basePath。例如,开发环境和生产环境可能使用不同的基础路径。这时,可以通过环境变量或配置文件来动态指定basePath。
- 在某些情况下,可能需要根据环境动态设置
-
注意事项:
- 设置
basePath时需要确保路径的正确性,否则可能导致资源加载失败或应用无法正常运行。 - 在开发过程中,如果更改了资源的存放位置或调整了目录结构,也需要相应地更新
basePath配置。
- 设置
basePath在Angular项目中扮演着至关重要的角色,它确保了资源的正确加载和应用的稳定运行。
在入口的 index.html 文件中可以设置基础路由路径:

改变基础路由路径的两种方式:

1.2 导入路由模块
Angular 中的 RouterModule 本质上个服务,通过配置使用,提供了三个指令:
- RouterLink
- RouterLinkActive
- RouterOutlet
1.3 forRoot 和 forChild

RouterModule.forRoot() 和 RouterModule.forChild() 是在 Angular 路由中使用的两个重要方法,它们各自在 Angular 路由配置中扮演着不同的角色。
RouterModule.forRoot() 方法在 Angular 应用程序中只使用一次,通常在根模块(如 AppModule)中调用。这个方法的作用非常关键:首先,它声明了路由器指令,使得我们可以在模板中使用如 <router-outlet> 和 <router-link> 这样的路由相关指令。其次,它管理我们的路由配置,这意味着我们可以在这个方法中定义应用程序的路由规则,决定哪个 URL 路径应该导航到哪个组件。此外,forRoot() 方法还负责注册路由器服务,这是 Angular 路由功能的核心,它允许我们在应用程序中进行导航操作。
相比之下,RouterModule.forChild() 方法则用于特性模块(feature modules)。与 forRoot() 类似,forChild() 也声明了路由器指令并管理路由配置。然而,重要的是要注意,forChild() 并不会注册路由器服务。这是因为路由器服务已经在根模块中由 forRoot() 注册过了,无需在特性模块中重复注册。因此,forChild() 主要用于在特性模块中定义和管理该模块的路由配置,而不会干扰到根模块的路由设置。
总的来说,RouterModule.forRoot() 和 RouterModule.forChild() 两者共同构成了 Angular 路由的灵活配置体系,使得开发者能够在不同模块级别上精细地控制路由行为。
需要注意的是,不管是 forRoot 或者 forChild 都是在 imports 数组中被调用的!


1.4 forRoot 的配置项

1.5 路由重定向
如下图所示,重定向只会发生一次。因此如果地址是空那么只会重定向到 welcome 而不会到 home 去。

1.6 触发路由跳转以及路由占位符
路由跳转的方式如下所示:

将跳转标签和路由占位符放在一起:

1.7 routerLink 指令
如下所示, routerLink 的值可以给一个字符串或者一个数组,区别在于前者是简写方式,后者可以通过数组的其它元素传递信息。

1.8 两种不同的 url 风格
这属于老生常谈了。H5 风格的 url 路由以及基于 hash 的url。


1.9 总结和一些注意点
路由的path属性定义了访问特定组件的URL路径。在指定path时,不需要在路径前添加斜杠(/),这与传统的URL路径定义有所不同。特殊字符也有特定含义,比如双引号(")代表默认路由,而单引号(')则代表通配符路由。此外,路径的大小写也是敏感的,因此在定义时需要特别注意。
而component属性则用于指定当路由被匹配时要加载的React组件。这里需要注意的是,组件名不应是字符串形式,也不应被引号包围。同时,所使用的组件必须已经被正确导入到当前文件中,否则路由将无法正常工作。最后,路由定义的顺序也很重要,因为路由匹配是按照定义的顺序进行的。因此,更具体的路由应该定义在更泛化的路由之前,以避免错误的匹配。
在Angular前端框架中,RouterLink指令用于在模板中创建导航链接,使用户能够点击跳转到应用内的不同路由。以下是关于如何使用RouterLink指令的简要说明:
要将RouterLink指令添加到模板中的元素上,需要将其作为一个属性来引入。这个属性应该被添加到一个可点击的元素上,比如<a>标签或者<button>,以便用户可以点击它进行导航。
在Angular中,指令通常被放在方括号[]内,这表示我们正在进行属性绑定。对于RouterLink,我们需要绑定一个链接参数数组,这个数组定义了导航的目标路由。
数组的第一个元素通常是根URL片段,它对应着路由配置中的路径。数组中的其他元素可以是路由参数或额外的URL片段,这些将被用来构建完整的导航路径。
例如,如果我们有一个名为user-detail的路由,它接受一个id参数,我们可以这样使用RouterLink:
<a [routerLink]="['/user-detail', userId]">View User Detail</a>
在这个例子中,userId是一个在组件类中定义的变量,它包含了要查看的用户ID。当用户点击这个链接时,应用将导航到user-detail路由,并将userId作为参数传递。
RouterLink指令是一个强大的工具,它允许开发者在Angular应用中轻松创建复杂的导航结构。通过正确使用这个指令,我们可以提供直观且响应迅速的用户界面,从而提升用户体验。
2. Feature Module 及路由
An Angular module created with the express purpose of organizing the components for a specific application feature area.
Feature Module Router 伴随实现代码分割的 Feature Module而出现。
2.1 注册 forChild 的基本形式

参考代码:
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ProductListComponent } from './product-list.component';
import { ProductDetailComponent } from './product-detail.component';
import { ProductEditComponent } from './product-edit/product-edit.component';
import { SharedModule } from '../shared/shared.module';
@NgModule({
imports: [
SharedModule,
RouterModule.forChild([
{ path: 'products', component: ProductListComponent }
])
],
declarations: [
ProductListComponent,
ProductDetailComponent,
ProductEditComponent
]
})
export class ProductModule { }
2.2 路由命名策略
如下图所示,左边是好的路由命名规则,而右边不是好的命名规则。

2.3 使用代码触发路由跳转
使用 Router 服务实例上的 navigate 进行路由跳转:

跳转的时候也有简写形式:

如上图所示,我们也可以使用 navigateByUrl 方法进行路由跳转。它们的区别在于:
navigate方法和navigateByUrl方法都是Angular Router服务中用于页面导航的函数,但两者有所区别。navigate方法接受一个包含路由路径和参数的数组,提供了更灵活的路由方式,允许我们传递参数和配置选项,如queryParams、fragment等。而navigateByUrl方法则直接接受一个完整的URL字符串,更适合于直接跳转到某个具体的URL,使用上更为简便,但不如navigate方法灵活。简而言之,navigate提供细粒度的控制,而navigateByUrl则提供了快捷的跳转方式。
2.4 注册路由的时候需要考虑到的顺序问题
如下图所示,左边是模块注册的顺序而右边是路由最终的排序。从这张图上不难看出来,通过 forChild 注册的路由在前,通过 forRoot 注册的路由在后。

2.5 定义一个专门用来注册 forRoot 的模块
这样做的好处在于:
- 更好的代码组织方式
- 更加容易寻找路由的注册位置
- 将路由单独剥离出来单独维护
示例代码如下:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { WelcomeComponent } from './home/welcome.component';
import { PageNotFoundComponent } from './page-not-found.component';
@NgModule({
imports: [
RouterModule.forRoot([
{ path: 'welcome', component: WelcomeComponent },
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
])
],
exports: [RouterModule]
})
export class AppRoutingModule { }
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { AppRoutingModule } from './app-routing.module'; // 假设路由模块文件名是app-routing.module.ts
import { AppComponent } from './app.component';
import { WelcomeComponent } from './home/welcome.component';
import { PageNotFoundComponent } from './page-not-found.component';
import { ProductModule } from './products/product.module';
import { UserModule } from './user/user.module';
import { MessageModule } from './messages/message.module';
// 假设你有一个ProductData类来提供模拟数据
import { ProductData } from './products/product-data';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), // 假设你想要1秒的延迟
AppRoutingModule,
ProductModule,
UserModule,
MessageModule
],
declarations: [
AppComponent,
WelcomeComponent,
PageNotFoundComponent
],
bootstrap: [AppComponent]
})
export class AppModule { }
这里抛出一个问题:
为什么这里需要exportsRouterModule,以及为什么这样app.module中声明的几个组件就可以使用相关的指令了?
答案如下:
当
AppRoutingModule通过exports: [RouterModule]导出RouterModule时,任何导入了AppRoutingModule的模块(在这个例子中是AppModule;AppRoutingModule允许其他导入它的模块访问RouterModule中提供的可声明对象,主要是指令,如router-outlet、routerLink等)都可以在其组件模板中使用RouterModule提供的指令。 这就是为什么在AppModule中声明的组件可以使用如router-outlet和routerLink等路由相关指令的原因。 在Angular中,模块(NgModule)是组织应用代码的一种方式,它们可以封装组件、指令、管道以及服务等。NgModule之间的交互主要通过imports和exports两个数组来管理。这里的关键是理解
exports数组的作用:
-
封装与共享:
exports数组用于封装那些你想让其他模块能够使用的可声明对象。在这个例子中,AppRoutingModule封装了RouterModule,并通过exports数组将其共享给任何导入了AppRoutingModule的模块。 -
传递性:当一个模块导入另一个模块时,它不仅可以访问被导入模块中定义的服务和提供程序,还可以访问被导入模块
exports数组中列出的所有可声明对象。在这个案例中,AppModule导入了AppRoutingModule,因此AppModule中的组件可以访问和使用RouterModule中导出的所有指令。 -
简化导入:通过将
RouterModule放入AppRoutingModule的exports中,开发者无需在每个需要使用路由指令的模块中单独导入RouterModule。这简化了代码结构,减少了冗余,并使得路由功能更加模块化。
易错点: 使用模块的方式注册路由更加应该小心顺序,如下图所示:

3. 路由参数
在 Angular 中,路由参数大概可以分成下面三种:

3.1 配置路由参数

甚至可以这样:

3.2 传递路由参数
使用下图所示的方法传递路由参数:

仔细观察,你可以看出来使用 ['/products', 0, 'edit'] 去匹配 /products/:id/edit 这个路由,就不难理解为什么 routerLink 的值要是一个数组了。
3.3 获取路由参数
你可以使用下面两种方式获取路由中的参数:
- SnapShot 方式
- Observable 方式

3.3.1 SnapShot 的方式

然后在钩子函数 ngOnInit 中解析出路由参数,再根据路由参数发送网络请求获取对应的数据。

3.3.2 Observable 方式
如果url从 /products/5/edit 变成了 /products/0/edit, 如果 5 和 0 都是路由参数。那么对应的路由组件是不会发生重新渲染的。这个时候使用上述的 SnapShot 的方式就不合适了。
这种情况下就要使用 Observable 方式了:

或者,

3.3.3 处理 id = 0 的情况
id 为 0 表示的是新增一个 product:

3.3.4 两种方式的对比

3.4 可变路由参数
下图所示的 name code startDate endDate 都是固定参数,而与必选参数相对应的是可变参数:

可变参数相关信息如下所示:

从图上可以看出来,可变参数不是通过 plainText 的方式传递的。而是通过一个 {} 传递的。并且最终反映在 url 上的状态也是不同的:;A=B;C=D
但是,读取可变参数的方式和 required parameters 的方式是相同的。

3.5 路由查询参数
使用路由查询参数的方式记录列表筛选条件的好处在于,从详情页返回的时候可以仍然保持跳跃之前的筛选条件:

与可变参数或者固定参数的设置方式不同,查询参数的定义方式是通过额外的指令或者配置对象完成的。

需要注意的是,查询参数再路由跳转的时候不会自动保留下来,要完成保留筛选条件的功能需要额外的设置:

我们仍然可以从 snapshot 上面读取查询参数,只不过使用了不同的方法:

需要额外注意的一点是:从查询参数中得到的信息的数据格式都是字符串类型的,就算是布尔类型的也是这样。这一点要额外注意!

总结下:为什么我们需要查询参数?
Pass optional or complex information to a route that is optionally retained across routes
List component passes its current user selections to the Detail component which passes them back
本小节,我们介绍了三种路由参数:required, optional, query. 每种参数再学习的时候从 Configure, Populate, Read 三个角度出发。
4. Prefetching Data Using Route Resolvers
预加载数据的优势在于:
- 防止由于数据缺失只展示部分页面
- 有利于代码复用
- 有利于数据流出现错误的时候的纠错
在这一小节,需要掌握的内容有:
- 使用路由提供数据
- 使用路由的 resolver
- 构建一个路由 resolver 服务
- 使用路由 resolver
- 从 data 属性中读取路由 resolver 获取的数据
单就路由数据这一点,我们可以深挖出很多东西,从路由中提供数据的方式有以下主要六点:
- 必要路由参数
- 可变路由参数
- 查询路由参数
- 路由的 Data 属性
- 路由 resolver
- 使用 Angular 服务
4.1 路由的 data 属性
如下图所示,可以使用固定属性 data 来传递信息。

读取 data 中的信息也很简单,就是 snapshot 上的 data 属性。
4.2 使用路由 resolver 的三个步骤
- 创建一个路由 resolver 服务
- 将 resolver 添加到路由的配置项中
- 从路由的 data 属性中读取从 resolver 中拿到的数据
4.3 创建一个 resolver 服务的过程

从上图不难看出来实际上就是实现特定的 Resolve 接口,此接口有类型为 ActivatedRouteSnapshot 和 RouterStateSnapshot 的形参。返回值为包裹了有效数据的 Observable. 因为其本质还是一个服务,因此,可以将其他(业务)服务实例注入以获取想要的数据:

加了防错处理的 resolver 完整版代码以及要获取的数据的接口如下所示:

4.4 将路由 resolver 添加到路由配置项中去:

需要明确的是,一个路由配置对应的 resolver 可能不止一个,可以是很多个:

在 RouterModule.forCild 中配置 resolver:

4.5 获取路由 resolver 中的数据
获取 resolver 数据是非常简单的,首先拿到 data 属性,然后根据此 resolver 在配置时对应的 key 就可以得到数据了。

获取数据的时机一般是在 ngOnInit 钩子函数中:

4.6 resolver 数据共享
由于 route resolver 的本质是 service, 因此具有天然的 sharing 特性:

4.7 获取实时的 resolver 数据
使用 snapshot 获取的数据不具有动态性。也就是当路由参数发生变化的时候,而路由地址没有改变的时候,由于 ngOnInit 此时不会再次执行,导致数据与新路由不匹配。
这种情况下不能通过快照的方式获取路由信息,而应该转向使用 observable 的方式,如下所示:

上图有一点小问题,最好还是在同样的生命周期函数中完成对数据的订阅:

下图对比了一下这两种做法:

5. 子路由
本节的知识点为:
- 使用子路由
- 配置子路由
- 展示子路由配置的组件
- 激活子路由
- 子路由组件获取父级路由信息
- 跨路由表单校验

5.1 配置子路由
使用如下所示的方法来配置子路由。

这里有一个点需要额外注意,那就是在配置子路由的时候,父级路由自动就变成了一个 group, 所以在子路由的配置过程中不得不去考虑第一个 path 为空,也就是 '' 的情况。

关于父级路由变成 group 这件事情,在后面多个 route-outlet 标签的时候还有进一步的讨论。
5.2 子级路由跳转
在子级路由进行跳转的时候,我们使用相对路径,这样既简单又可以防止出错。相对路由的特点就是路由 path 不是以 / 开始的。

下面是相对路由和绝对路由的对比,以及如何使用代码进行相对路由跳转:


对比总结:

5.3 子级路由使用 resolver 的数据
我们当然可以给子级路由设置对应的 resolver 来获取数据。但是如果其父级已经通过 resolver 获取了数据,那么子级直接使用即可,不必每一个都获取一遍,子级使用父级数据的关键在于:route.parent.snapshot.data

当然也有响应的 observable 方法:

5.4 路由跳转和表单数据
发生路由跳转之后我们会将获取的数据回填到新页面的表单中去。这个时候一定要注意:应该先获取到表单实例,然后通过实例 reset 表单的状态和数据,reset 之后再回填数据。

5.5 跨路由表单校验问题
处理跨路由表单的校验问题的解决方案就是:手动校验

5.6 无组件父级路由
所谓无组件的父级路由实际上指的是,将原来的父级组件指定给路由路径为 '' 的子级路由。这样做的好处在于减少了 router-outlet 之间的嵌套层数。

结构如下所示:

对比修改前后:

6. 路由样式、路由动画以及路由监控
这一小节的内容包括:
- 给选中的路由添加额外的样式
- 给路由切换增加动画
- 监控路由事件
- 对路由事件(在不同的生命周期)做出响应
6.1 给选定路由添加特定的样式
原理其实很简单,就是在 routerLink 指令之后添加名为 routerLinkActive 的指令。而这个指令的作用就是当这个路由被激活的时候,绑定 routerLinkActive 指令的元素上会自动出现设置好的 className:

这个指令在路由组或者 nav 中才能发挥最大效用:

与此对应的样式可以写成下面这样:

但是也带来一个问题,那就是 A 页面的子级路由被激活的时候 A 页面的路由始终处于被激活的状态,这可能并不符合你的想法。因此,需要一种更加精确的路由激活方案,这个时候可以用到名为: routerLinkActiveOptions 的指令。

6.2 路由跳转时候的动画指定
要想设置路由跳转时候的动画,需要进行下面四个步骤:
- lmport BrowserAnimationsModule
- Define the desired animations
- Register the animation with a component
- Trigger the animation from the router outlet
6.2.1 定义一个动画
我们可以通过 * <=> * 的方式指定只要发生了动画状态的改变就执行此动画的命令。

{ optional: true }: 这个配置项告诉 Angular 动画系统,即使在动画执行期间元素的状态发生变化(例如,元素被移除或动画被手动取消),动画也应该继续执行,而不是立即停止。optional: true 允许动画在面对不确定或动态变化的情况下更加健壮地执行。
组件使用的动画会在组件的元数据中以数组的方式进行声明:

在声明了动画之后,就可以在模板中使用@动画名作为指令了。这个指令的格式为:[@动画名]="目前的状态名",如下所示:

上面的代码中,我们通过 #o="outlet" 的方式获取 router-outlet 的 handler.
6.2.2 路由事件
在一个路由发生的前后,分成了好多阶段,这些阶段都会触发对应的事件,因此有多少个阶段就会发生多少个事件。根据这种机制,我们可以更加精细化的控制整个过程。而这些事件有:
- navigationStart
- RoutesRecognized
- NavigationEnd
- NavigationCancel
- NavigationError

想要涉足这块内容,需要配置内置的调试工具,因为可能会影响到性能,所以这个工具需要手动配置一下的。配置是非常简单的,只需要在 forRoot 的配置项进行声明就可以了!

如此配置之后,每当发生新的路由事件的时候就会在控制台中将这些信息打印出来:

那么我们可以利用路由事件完成什么样的功能呢?
- 展示或者取消展示 spin 在合适的时机
- 做调试,debug
- 注入自定义逻辑,实现更加复杂的功能
6.2.3 如何实现对路由事件的监听
实现对路由事件的监听是非常简单的,只需要在服务的构造函数中订阅 Router 的实例属性 events 即可:

7. 命名 router-outlet 和 备选路由
本小节的内容如下所示:
- Using Secondary Routes
- Defining a Named RouterOutlet
- Configuring Secondary Routes
- Activating Secondary Routes
- Clearing Secondary Outlets
所谓命名和备选路由见下图的右上角:

我们只需要在 router-outlet 标签上添加名为 name 的属性就可以了!
7.1 Seconsdary Route
对于备用路由,我们可以用它来完成下面的任务:
- Dashboard
- 多窗口用户界面
- notes or comments
- messages
有了命名 router-outlet, 我们就可以将一个页面拆成多个单独的模块,如下所示:

那么命名的 router-outlet 是如何工作的呢?只需要在配置路由规则的时候通过 outlet 这个属性指定将要渲染的 router-outlet 的名字即可!

配置好之后,你肯定疑问这个要怎么样才能激活,实际上,这个可能和之前见到的url都不太一样。激活 secondary route 的 url 应该是:/products(popup:message) 其中 popup 的指的是子路由,而 message 则是子路由参数。
在 Angular 中,Secondary Route(辅助路由或次级路由)是一种特殊的路由配置,它允许你在同一个页面上显示多个路由视图。这通常用于创建模态窗口、侧边栏、弹出窗口等,这些元素需要在不重新加载主内容的情况下显示或隐藏。
如何配置 Secondary Route
-
定义路由出口:首先,你需要在组件的模板中定义多个路由出口。例如,你可以定义一个默认的路由出口和一个命名的路由出口用于辅助路由。
<!-- 默认路由出口 --> <router-outlet></router-outlet> <!-- 命名路由出口,用于辅助路由 --> <router-outlet name="popup"></router-outlet> -
配置路由:在你的路由模块中,你可以为辅助路由定义一个子路由,并通过
outlet属性指定它应该使用哪个命名路由出口。const routes: Routes = [ { path: 'products', component: ProductsComponent, children: [ { path: 'popup', component: PopupComponent, outlet: 'popup' // 使用命名路由出口 }, // 其他子路由配置 ] }, // 其他顶级路由配置 ]; -
导航到辅助路由:使用
Router服务导航到辅助路由时,你可以指定使用哪个路由出口。import { Router } from '@angular/router'; constructor(private router: Router) {} openPopup() { this.router.navigate([{ outlets: { popup: 'popup' }}], { relativeTo: this.route }); }在这个例子中,
outlets对象用于指定哪个命名路由出口应该被导航到。
使用场景
- 模态窗口:当你想要在页面上打开一个模态窗口,显示一些信息或表单,而不影响主页面内容时。
- 侧边栏:在需要一个侧边导航栏或菜单,可以独立于主内容进行导航的情况下。
- 弹出窗口:用于显示警告、通知或其他需要用户注意的信息。
优点
- 用户体验:可以提供更丰富的用户体验,允许用户在不离开当前页面的情况下与应用进行交互。
- 代码组织:通过将不同的视图逻辑分离到不同的组件和路由中,可以使应用的结构更清晰。
注意事项
- 确保辅助路由的配置正确,特别是在使用
outlet属性时。 - 管理好辅助路由的状态,确保在不需要时能够正确关闭或隐藏。
通过使用 Secondary Route,Angular 开发者可以创建更加动态和交互性强的 Web 应用。
7.2 使用指令完成带有 secondary route 的跳转

上图中,展示的实际上是两种跳转方案。上面的是简写,下面的是完全体。着重分析完全体。
在 Angular 中,路由数组 ['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }] 表示一个复合路由配置,它涉及到主路由和辅助路由的导航。下面是这个路由数组所代表的路由结构:
-
'/products': 这是主路由的路径,表示导航的起始点是一个名为products的路由。 -
product.id: 这是一个动态路由参数,表示products路由下的某个具体资源,例如一个产品详情页。这里的product.id应该是一个变量,代表当前要编辑的产品的 ID。 -
'edit': 这是products路由下的子路由路径,表示用户想要执行的操作,比如编辑产品。 -
{ outlets: { popup: ['summary', product.id] } }: 这是一个辅助路由配置,指定了在命名路由出口popup中应该显示的路由。这里,辅助路由的路径是'summary',并且它也接受一个动态参数product.id。
对应的路由配置可能如下所示:
const routes: Routes = [
{
path: 'products/:productId',
component: ProductComponent,
children: [
{
path: 'edit',
component: EditProductComponent,
},
{
path: 'summary',
component: SummaryComponent,
outlet: 'popup'
},
// 可能还有其他子路由
]
},
// 其他顶级路由
];
在这个配置中:
- 主路由
/products/:productId会匹配类似/products/1的 URL,其中:productId是一个动态参数,对应于product.id。 - 子路由
edit表示在产品详情页下进行编辑操作的路由。 - 辅助路由
summary配置了outlet: 'popup',表示它应该在命名的路由出口popup中显示。
在模板中,你需要定义相应的路由出口以匹配这些路由:
<!-- 主内容区域 -->
<router-outlet></router-outlet>
<!-- 辅助内容区域,例如弹出窗口 -->
<router-outlet name="popup"></router-outlet>
当你使用路由数组 ['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }] 进行导航时,Angular 路由器会解析这个数组,并根据配置的路由和提供的参数来激活相应的组件和视图。主路由出口将显示 EditProductComponent,而辅助路由出口 popup 将显示 SummaryComponent,并且两个组件都会接收到 product.id 参数。
7.3 使用代码完成等价的跳转
使用 route.navigate 方法完成路由跳转。

值得注意的是第三种方式。可以将所有的组成部分都看成是命名的 router-outlet 而原来默认的自动获取了名称primary, 通过这样的方式实现了路由跳转时书写格式的统一。
由于上面的跳转相当于到地址 /products/5/edit(popup:summary/5), 因此还可以使用 navigateByUrl 进行直接的跳转:
this.router.navigateByUrl('/products/5/edit(popup:summary/5)');
这里换一种方式展示一下:this.router.navigateByUrl('(primary:products/5/edit)(popup:summary/5)');
小节:secondary 相关的路由写在主路由后面的括号里面,虽然很奇怪,但是很有用!
7.4 清除第二路由
有创建就有清除,清除第二路由必须是以显式方式,不然会一直传递下去。

可以看出来,outlet 是需要定点清除的,但是如果使用的是 navigateByUrl 就可以一次性全部清除。
7.5 使用辅助路由完成组件的加载和卸载

8. 路由守卫
使用路由守卫,作用在于:
- 限制路由访问权限
- 在路由跳转之前发出警告
- 在跳转至另一个路由之前预先获取数据
8.1 路由守卫种类以及作用顺序
如下图所示:

canDeactivate: 在离开某个路由的时候触发,用于检查是不是有没有保存的内容;检查是否所有的操作已经运行完成canLoad:特别针对的是异步加载的路由组件,用于检测是否有足够的权限查看。canActivateChild:在进入某个子路由之前校验,在子路由路径改变的时候触发,用于验证前置条件是否到位canActivate: 在进入某个路由之前进行校验,用于权限控制,前置条件检验resolve: 预加载数据
8.2 创建路由守卫
创建一个路由守卫是很简单的,因为其本质上也就只是一个实现了特定接口的服务而已,如下所示:

上述的服务实现了 CanActivate 接口,所以它是一个 CanActivate 种类的路由守卫。这个接口返回值可以是:Observable<boolean l UrlTree> l Promise<boolean l urlTree> l boolean l UrlTree

看一个示例:这段代码是 TypeScript 语言编写的 Angular 守卫(Guard),用于在路由激活之前检查用户是否已经登录。如果没有登录则会跳转至登陆页面。
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service'; // 假设 AuthService 是你的认证服务
@Injectable({
providedIn: 'root' // 表示这个服务在根注入器中提供,应用的任何地方都可以使用
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.checkLoggedIn();
}
private checkLoggedIn(): boolean {
if (this.authService.isLoggedIn) {
return true;
} else {
this.router.navigate(['/login']); // 如果用户未登录,重定向到登录页面
return false;
}
}
}
代码说明:
-
@Injectable({ providedIn: 'root' }): 这是 Angular 的装饰器,用于声明一个类是可注入的。providedIn: 'root'表示该服务在 Angular 的根注入器中注册,因此它可以在应用的任何地方被注入。 -
AuthGuard类实现了CanActivate接口,这意味着它可以作为一个路由守卫来使用。CanActivate接口要求实现canActivate方法,该方法决定是否可以激活给定的路由。 -
constructor: 构造函数注入了AuthService和Router服务。AuthService用于检查用户登录状态,Router用于在用户未登录时导航到登录页面。 -
canActivate方法:这是CanActivate接口的一部分,它接收两个参数,ActivatedRouteSnapshot和RouterStateSnapshot,分别表示即将激活的路由和当前的路由状态快照。这个方法返回一个 Observable、Promise 或者直接返回一个布尔值,表示是否可以激活路由。 -
checkLoggedIn方法:这是一个私有方法,用于检查用户是否已经登录。如果AuthService的isLoggedIn属性为true,则返回true,表示用户已登录,可以激活路由。如果不是,调用Router的navigate方法跳转到登录页面,并返回false,表示路由激活被禁止。 -
代码中使用了类型注解(如
boolean和UrlTree),这些是 TypeScript 的类型系统的一部分,用于确保代码的类型安全。 -
AuthService应该是一个服务类,你需要根据你的应用实现它,它应该至少有一个isLoggedIn属性或方法来检查用户的登录状态。
8.3 使用路由守卫
使用路由守卫的方式是简单的,只需要在配置路由规则的时候配置一下就可以了!

8.4 路由守卫和子路由
路由守卫可以绑定给每一个子路由或者由其父级路由代为执行:

下面展示了如何在父级上添加路由守卫:

8.5 高级功能 - 登录之后重定向
一个很常见但是极具用户体验的功能:用户没有登录但是访问了页面A,A并不是登录之后的默认欢迎页。现在要实现的功能就是,登录之后直接显示A页面而不是显示默认的欢迎页。
为了实现这个功能,首先需要解决的问题是:如何在不同的路由之间传递信息?
在路由之间传递信息有三种预案:
- 通过路由参数
- 通过单例服务实例
- 通过路由的 data 属性
由于路由的 data 属性在某些路由的生命周期中不可见,因此最好的方式还是使用服务在不同的路由之间传递信息。

功能实现代码及说明
此功能包含了两个 Angular 组件:AuthGuard 和 LoginComponent。
AuthGuard 类
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Observable, UrlTree } from '@angular/router';
import { AuthService } from './auth.service'; // 假设 AuthService 是你的认证服务
@Injectable({
providedIn: 'root' // 表示这个服务在根注入器中提供
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.checkLoggedIn(state.url);
}
private checkLoggedIn(url: string): boolean {
if (this.authService.isLoggedIn) {
return true;
} else {
this.authService.redirectUrl = url; // 存储需要重定向的 URL
this.router.navigate(['/login']); // 重定向到登录页面
return false;
}
}
}
LoginComponent 类
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { AuthService } from './auth.service'; // 假设 AuthService 是你的认证服务
@Component({
// 组件的元数据,比如 selector, template, styles 等
})
export class LoginComponent {
errorMessage: string;
pageTitle = 'Log In';
constructor(private authService: AuthService, private router: Router) {}
login(loginForm: NgForm) {
if (loginForm && loginForm.valid) {
const userName = loginForm.form.value.userName; // 获取用户名
const password = loginForm.form.value.password; // 获取密码
this.authService.login(userName, password); // 调用 AuthService 进行登录
if (this.authService.redirectUrl) {
this.router.navigateByUrl(this.authService.redirectUrl); // 如果有重定向 URL,则导航到该 URL
this.authService.redirectUrl = null; // 登录成功后清除重定向 URL
} else {
this.router.navigate(['/products']); // 否则导航到默认页面,例如产品列表
}
} else {
this.errorMessage = 'Please enter a user name and password.'; // 表单无效时显示错误消息
}
}
}
代码说明
-
@Injectable({ providedIn: 'root' }): 这是 Angular 的装饰器,用于声明一个类是可注入的。providedIn: 'root'表示该服务在 Angular 的根注入器中注册,可以在应用的任何地方使用。 -
AuthGuard类实现了CanActivate接口,用于保护路由,确保只有登录的用户才能访问特定的路由。 -
canActivate方法:这是CanActivate接口的一部分,它决定是否可以激活路由。这个方法调用checkLoggedIn方法,并传入当前路由的 URL。 -
checkLoggedIn方法:这是一个私有方法,检查用户是否已经登录。如果用户未登录,它将存储当前 URL 并重定向用户到登录页面。 -
LoginComponent类:这是一个登录表单组件,它使用AuthService来进行用户认证。 -
login方法:这是LoginComponent的方法,当用户提交登录表单时被调用。如果表单有效,它将调用AuthService的login方法进行登录,并根据登录结果导航到适当的页面。 -
AuthService:这是一个假设的服务,你需要根据你的应用实现它。它应该至少有一个isLoggedIn属性来检查用户登录状态,一个login方法来进行用户认证,以及一个redirectUrl属性来存储需要重定向的 URL。
衍生一下,锁屏也可能会用到这个功能。
8.6 canDeactivate 路由守卫
如下图所示,构建一个简单的 canDeactivate 路由守卫。

不难看出,与 canActiviate 路由守卫不同的地方在于,此路由需要指定要离开的组件。不仅如此还有目前的状态以及下一个状态。
如何判断出组件中的数据已经被修改
下图提供了一种简单的比较方法:

使用 canDeactiviate 路由实现 safe change guard 功能
如下所示,代码在路由跳转之前比较 UI 组件中数据和已保存数据,如果不相同则会弹窗警告。

这里有一个疑问:为什么点击 confirm 之后会成功跳转?
个人理解,系统弹窗直接阻塞整个页面,点击确认之后:
return confirm(`Navigate away and lose all changes to ${productName}?` );
实际上相当于是
return true );
9. 路由懒加载
- Building and Serving Our Files: 构建懒加载的必要文件
- Preparing for Lazy Loading:准备懒加载
- Lazy Loading:懒加载
- canLoad Guard:canLoad 路由守卫
- Preloading Feature Modules:提前加载 feature 模块
- Custom Loading Strategies:自定义预加载策略
所谓路由懒加载其实指的是:

9.1 准备懒加载
- 使用一个 feature 模块
- 将子路由成组管理
- 保证此 feature 模块不会被其它模块引用
9.2 懒加载路由配置规则

我们使用了 loadChildren 代替了 component,其值本质上没有变化依然是 component. 只不过 import 函数使其变成懒加载的了。
9.3 懒加载路由的子路由需要遵循成组的规则
如下图所示的 product.module 模块对应的是 '/products' 这个路由。但是这个模块是通过 懒加载 完成的。因此在 product.module 内部,其子路由需要采用成组的方式进行配置,如下面图的右侧展示的正是 '/products' 这个路由的子路由的配置方式,见:
{
path: '',
component: ProductListComponent,
}

9.4 懒加载的路由的守卫
对于懒加载的模块,使用 canLoad 类型的路由守卫,如下所示:

9.5 canLoad 类型的路由守卫和多路由服务
如下图所示,实现了 CanLoad 接口的服务可以用来充当 canLoad 类型的路由守卫:

一个守卫可以实现多个接口(如上图所示),根据守卫名称按需选用即可:

9.6 比懒加载更强的预加载
需要注意的是,尽管在中文上它们的叫法差别比较大,但是在英文中。预加载是 easy lazy loading ,而懒加载是 lazy loading. 从这里也不难看出预加载实际上是懒加载的某种改进。

预加载的几种策略:
- 不预加载任何资源
- 预加载所有资源
- 自定义预加载资源
如果要预加载所有的资源,那是十分简单并且直接的,只需要在指定路由规则的时候添加一个配置项 preloadingStrategy: PreloadAllModules 即可(只能设置在 forRoot 上吗,forChild 不可以吗?):

9.7 预加载和路由守卫
一旦决定了所有的异步资源都需要通过懒加载的方式进行,那么此时就不能在父级中使用 canLoad 种类的路由守卫。不过,鉴于我们可以在一个服务中实现多种路由守卫,其实要做的只是换掉元信息中的 key 就可以了, 将 canLoad 替换成 canActivate!


9.8 自定义预加载策略
实现自定义的路由预加载策略有两个步骤:
- 创建一个预加载策略服务

上述服务配置的策略为路由配置规则中 data 中 preload 为真的路由会被预先加载。
- 将此服务配置到路由规则中去

配置项还是那个:preloadingStrategy 字段,只不过此时用自定义的 SelectiveStrategy 代替了原来内置的 preloadAllModules.
9.9 自定义路由策略服务的具体实现
我们可以在需要预加载的路由的 data 属性中标记此路由需要进行预加载,这样在策略服务中就可以从 data 中读取出来这个信息,然后就可以对此路由对应的组件进行预加载了!
import { Injectable } from '@angular/core';
import { Route, PreloadingStrategy, LoadChildren } from '@angular/router';
import { Observable, of } from 'rxjs'; // 确保 rxjs 被正确导入
@Injectable({
providedIn: 'root' // 使用 'root' 作为提供的注入器
})
export class SelectiveStrategy implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
// 检查路由的 data 对象是否有 'preload' 属性,并且它的值是 true
if (route.data && route.data['preload']) {
// 如果需要预加载,调用 load 函数并返回其结果
return load();
} else {
// 如果不需要预加载,返回一个空的 Observable
return of(null);
}
}
}
要配合上述 SelectiveStrategy 预加载策略,你的路由配置需要利用 Angular 的路由懒加载特性,并使用 loadChildren 函数来指定加载的模块或组件。同时,你需要在路由的 data 属性中添加 'preload': true 来标记那些需要预加载的路由。
以下是一个相应的路由配置示例:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// 假设这是你要懒加载的组件或模块的路径
const routes: Routes = [
{
path: 'lazy',
loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule),
data: {
// 标记这个路由需要预加载
preload: true
}
},
{
path: 'another-lazy',
loadChildren: () => import('./another-lazy/another-lazy.module').then(m => m.AnotherLazyModule),
data: {
// 这个路由不需要预加载
preload: false
}
},
// ... 其他路由配置
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: SelectiveStrategy // 使用 SelectiveStrategy 作为预加载策略
})],
exports: [RouterModule]
})
export class AppRoutingModule {}
代码说明:
-
路由配置 (
routes数组):定义了应用的路由。这里有两个懒加载的路由示例。 -
懒加载路由:使用
loadChildren函数来指定如何懒加载模块。import()是一个返回Promise的动态导入函数,它返回模块的加载结果。 -
路由的
data属性:通过data属性的'preload': true标记,你可以告诉SelectiveStrategy这个路由需要预加载。 -
SelectiveStrategy的使用:在RouterModule.forRoot()方法中,通过preloadingStrategy选项将SelectiveStrategy作为预加载策略传入。 -
AppRoutingModule:这是一个 Angular 模块,它声明了应用的路由配置,并将其导出,以便其他模块可以导入它。
需要注意的是,上述代码中的 import() 函数调用是 ES6 动态导入语法,它返回一个 Promise 对象,该对象在模块加载完成后解析为模块本身。.then(m => m.LazyModule) 部分是指定模块加载后如何从模块中获取根模块类。你需要根据实际的模块路径和导出的模块类名进行调整。
实践与练习
1. 使用路由守卫在所有跳转之前进行鉴权,失败则不跳转。
原理就是调用已经存在的鉴权服务查看是否有权限,如下所示:
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from "@angular/router";
import { LoginService } from "@c8y/ngx-components";
import { Observable } from "rxjs";
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private loginService: LoginService) {
}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
const credentials = {
tenant: "",
user: "",
password: "",
};
const isPasswordGrantLogin = this.loginService.isPasswordGrantLogin(credentials);
return isPasswordGrantLogin;
}
}
2. 在详情页离开的时候使用路由守卫总是进行保存提醒。

import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from "@angular/router";
import { Observable } from "rxjs";
@Injectable({
providedIn: 'root',
})
export class SafeChangeGuard implements CanDeactivate<any> {
canDeactivate(
component: any,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot,
): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
if (component.needsDoubleCheck) {
return confirm(`Are you sure to leave this page?`)
}
return true;
}
}
在路由守卫中,我们通过注入的方式拿到了组件实例,并根据实例属性 needsDoubleCheck 判断是否需要在跳转之前进行询问。因此,在组件中应该增加此属性:

3. 在同一个路由守卫上实现两种不同的功能。
4. 完成锁屏/解锁功能,并使用路由守卫在解锁之后回到锁屏前页面。
5. 设置所有的详情页为懒加载,并证明懒加载成功。
设置一个模块 ModuleA 为懒加载需要以下几个步骤(注意,懒加载只针对模块起效!):
- 在引用 ModuleA 的地方,将普通引用换成动态引用,同时删除文件中的静态 import 和 imports 数组中的相关模块。
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatBadgeModule } from '@angular/material/badge';
import { MatIconModule } from '@angular/material/icon';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AlarmListComponent } from './alarm-list.component';
// import { AlarmDeatailsComponent } from './alarmdetails/alarm-details.component';
// import { AlarmDetailsModule } from './alarmdetails';
import { MatRippleModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TruncatedTextModule } from '../truncated-text';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import {
CoreModule,
NavigatorNode,
HOOK_NAVIGATOR_NODES,
FormsModule
} from '@c8y/ngx-components';
import { RouteGuard } from '../guards/storage.guard';
import { SharedModule } from '../shared/shared.module';
import { AuthGuard } from '../guards/auth.guard';
const routes: Routes = [
{ path: '', redirectTo: 'alarmlist', pathMatch: 'full' },
{
path: 'alarmlist',
canActivate: [RouteGuard],
children: [
{
path: '',
component: AlarmListComponent,
pathMatch: 'full',
},
{
path: ':id',
loadChildren: () => import('./alarmdetails').then(m => m.AlarmDetailsModule),
// component: AlarmDeatailsComponent,
canActivate: [AuthGuard],
}
]
}
];
const userlist = new NavigatorNode({
label: 'Alarm List',
icon: 'list',
path: '/alarmlist',
routerLinkExact: false,
priority: 90,
});
export const navigatorNodes = {
provide: HOOK_NAVIGATOR_NODES,
useValue: { get: () => userlist },
multi: true
};
@NgModule({
declarations: [AlarmListComponent],
imports: [
RouterModule.forChild(routes),
CoreModule,
SharedModule,
MatIconModule,
MatBadgeModule,
MatDividerModule,
MatButtonModule,
MatCardModule,
MatTableModule,
MatSortModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatSnackBarModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatRippleModule,
TruncatedTextModule,
BrowserAnimationsModule,
MatSlideToggleModule,
FormsModule,
// AlarmDetailsModule,
],
providers: [navigatorNodes]
})
export class AlarmListModule {
}
见上述代码中被注释的内容。
- 在 ModuleA 中设置子路由然后指向对应的组件(必须设置子路由,尽管其可能是空的)。因为懒加载的对象是模块而不是组件,所以如果没有子路由则虽然懒加载成功但是显示不出来任何组件。如下所示:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AlarmDeatailsComponent } from './alarm-details.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TruncatedTextModule } from '../../truncated-text';
import {
CoreModule,
} from '@c8y/ngx-components';
import { SharedModule } from '../../shared/shared.module';
import { AlarmCardComponent } from '../alarmcard/alarmcard.component';
import { CommonModule } from '@angular/common';
const routes: Routes = [{
path: '',
component: AlarmDeatailsComponent,
pathMatch: 'full',
}]
@NgModule({
declarations: [AlarmDeatailsComponent, AlarmCardComponent],
imports: [
RouterModule.forChild(routes),
CommonModule,
CoreModule,
SharedModule,
MatProgressSpinnerModule,
TruncatedTextModule,
],
providers: [],
exports: [AlarmCardComponent],
})
export class AlarmDetailsModule {
}
- 检查 ModuleA 的 import 和 imports 数组中是否有 BrowserModule 和 BrowserAnimationsModule,如果有则去掉。
- 检查 ModuleA 的 import 和 imports 数组中是否有 CommonModule, 如果没有则加上。
- 对 ModuleA 的 imports 数组中引入的其它模块都进行步骤 3 和 步骤 4 的校验。
第五步很重要,例如下面代码中:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AlarmDeatailsComponent } from './alarm-details.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TruncatedTextModule } from '../../truncated-text';
import {
CoreModule,
} from '@c8y/ngx-components';
import { SharedModule } from '../../shared/shared.module';
import { AlarmCardComponent } from '../alarmcard/alarmcard.component';
import { CommonModule } from '@angular/common';
const routes: Routes = [{
path: '',
component: AlarmDeatailsComponent,
pathMatch: 'full',
}]
@NgModule({
declarations: [AlarmDeatailsComponent, AlarmCardComponent],
imports: [
RouterModule.forChild(routes),
CommonModule,
CoreModule,
SharedModule,
MatProgressSpinnerModule,
TruncatedTextModule,
],
providers: [],
exports: [AlarmCardComponent],
})
export class AlarmDetailsModule {
}
ModuleA 引入的 TruncatedTextModule 模块中引入了 BrowserModule, 导致懒加载不成功,所以也应该将 TruncatedTextModule 中的 BrowserModule 换成 CommonModule. 见注释部分:
import { NgModule } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TruncatedTextComponent } from './truncated-text.component';
import { SharedModule } from '../shared/shared.module';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [
TruncatedTextComponent,
],
imports: [
// BrowserModule,
CommonModule,
SharedModule,
MatTooltipModule
],
exports: [
TruncatedTextComponent,
]
})
export class TruncatedTextModule { }
证明懒加载成功: 当跳转到对应的路由的时候才通过网络请求加载对应的模块文件(本质上是 js 文件)
可以通过注释的方式对动态加载的包自定义命名:做法为:
loadChildren: () => import(/* webpackChunkName: "alarmdetails" */ './alarmdetails').then(m => m.AlarmDetailsModule),
效果为:
开发环境下 webpack 的打包日志:
生产打包之后,也改了名:
6. 设置某个详情页为预加载,并自定义预加载策略,最后证明确实预加载了。
预加载是懒加载的升级版本,因此预加载的前提就是懒加载。实现一个简单的预加载是非常简单的。
- 第一步,到路由 forRoot 的配置项中加入配置项
preloadingStrategy: PreloadAllModules,如下所示:preloadingStrategy: PreloadAllModules,
ngRouterModule.forRoot([], {
enableTracing: false,
useHash: true,
preloadingStrategy: PreloadAllModules,
}),
- 第二步,检查预加载是否成功
自定义预加载策略:本质上还是服务
// iot-preload-strategy.service.ts
import { Injectable } from "@angular/core";
import { PreloadingStrategy, Route } from "@angular/router";
import { Observable, of } from "rxjs";
@Injectable({
providedIn: 'root',
})
export class IotPreloadStrategy implements PreloadingStrategy {
preload(route: Route, fn: () => Observable<any>): Observable<any> {
if (route.data && route.data['preload']) {
return fn();
} else {
return of(null)
}
}
}
注意 preload 方法的返回值是 Observable 类型的!
使用 IotPreloadStrategy 策略替换 PreloadAllModules.
ngRouterModule.forRoot([], {
enableTracing: false,
useHash: true,
preloadingStrategy: IotPreloadStrategy,
// preloadingStrategy: PreloadAllModules,
}),
注意此时我们没有给 AlarmDetails 模块的路由 data 上设置 preload, 也就是说无法通过策略。现在的效果为:不加载,直到对应路由被触发。
而如果改变设置为:
const routes: Routes = [
{ path: '', redirectTo: 'alarmlist', pathMatch: 'full' },
{
path: 'alarmlist',
canActivate: [RouteGuard],
children: [
{
path: '',
component: AlarmListComponent,
pathMatch: 'full',
},
{
path: ':id',
loadChildren: () => import(/* webpackChunkName: "alarmdetails" */ './alarmdetails')
.then(m => m.AlarmDetailsModule),
// component: AlarmDeatailsComponent,
canActivate: [AuthGuard],
data: {
preload: true,
}
}
]
}
];
则 alarmdetails.js 会被预加载了。
7. 监听路由事件,打印跳转信息
完成一个记录每次跳转用时的服务,然后以漂亮的字体将相关信息打印出来。
constructor(
...
private routeDataService: RouteDataService,
) {
this.id = this.route.snapshot.paramMap.get('id');
this.router.events.subscribe(
event => {
if (event instanceof NavigationStart) {
this.navigationStartTime = +new Date();
}
if (
event instanceof NavigationEnd ||
event instanceof NavigationCancel ||
event instanceof NavigationError
) {
const { url } = event;
this.navigationEndTime = +new Date();
this.routeDataService.setNewInfo({
start: this.navigationStartTime,
end: this.navigationEndTime,
url,
});
}
}
)
}
服务的代码为:
import { Injectable } from "@angular/core";
@Injectable({
providedIn: 'root',
})
export class RouteDataService {
private infoStack = [];
private logStyle = "color:#fff;background:cyan;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;";
constructor() { }
getInfo() {
return this.infoStack;
}
setNewInfo(info) {
this.infoStack.push(info);
}
get length() {
return this.infoStack.length;
}
print() {
console.log(
'%cRoute Info Stack:',
this.logStyle,
this.infoStack,
);
}
}
8. 使用备用路由完成模态框的弹出和关闭,及信息传递。
8.1 首先建立基本的组件架构
在 iotDevicelist 目录下新增 addModal 目录,在文件目录上形成父子结构。

接下来逐个构建 addModal 中的文件内容。
8.2 addModal/index.ts
export * from './add-modal.module';
8.3 addModal/add-modal.module.ts
import { NgModule } from '@angular/core';
import { SharedModule } from '../../shared/shared.module';
import { AddModalComponent } from './add-modal.component';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AddModalComponent,
],
imports: [
BrowserModule,
SharedModule,
RouterModule.forChild([]),
MatCardModule,
MatButtonModule,
],
providers: [],
exports: [AddModalComponent],
})
export class IotAddDeviceModule {
}
有两个点需要注意:
RouterModule.forChild([]),的意义不在于无意义的注入空的路由数组,其意义在于能够在此模块的组件中使用路由相关的 provides directives, 没有这一步就不能使用 routerLink 这些指令。exports: [AddModalComponent],的意义在于:将子组件暴露出去,这个时候只要莫格模块中引入 IotAddDeviceModule 这个模块,就相当于 declaration 了 AddModalComponent.
8.4 addModal/add-modal.component.ts
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
@Component({
selector: 'app-add-modal',
templateUrl: './add-modal.component.html',
styleUrls: ['./add-modal.component.less'],
})
export class AddModalComponent {
title = '';
constructor(private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
this.title = this.route.snapshot.queryParamMap.get('name');
}
clearModal() {
this.router.navigateByUrl('/iotdevice');
}
}
关于上述代码有三点需要额外说明:
- 使用 Router 实例做跳转。
- 使用 ActivatedRoute 实例获取路由参数,容易和上面弄混。
- clearModal 方法中使用 router 上面的 navigateByUrl 方法,而不是常见的 navigate, 这里充分体现出 navigateByUrl 方法具有清除路由参数的功能,这很重要!
8.5 addModal/add-modal.component.less
.iot-modal {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
z-index: 9999;
background-color: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}
.example-card {
max-width: 400px;
}
.example-header-image {
background-image: url('https://material.angular.io/assets/img/examples/shiba1.jpg');
background-size: cover;
}
这里设置了模态框为全屏遮罩的,并给了一个灰色的背景。
8.6 addModal/add-modal.component.html
<span class="iot-modal">
<mat-card class="example-card" appearance="outlined">
<mat-card-header>
<div mat-card-avatar class="example-header-image"></div>
<mat-card-title>Shiba Inu</mat-card-title>
<mat-card-subtitle>Dog Breed {{ title }}</mat-card-subtitle>
</mat-card-header>
<img
mat-card-image
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
alt="Photo of a Shiba Inu"
/>
<mat-card-content>
<p>
The Shiba Inu is the smallest of the six original and distinct spitz
breeds of dog from Japan. A small, agile dog that copes very well with
mountainous terrain, the Shiba Inu was originally bred for hunting.
</p>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="clearModal()">BACK</button>
<button mat-button routerLink="/iotdevice">CANCEL</button>
</mat-card-actions>
</mat-card>
</span>
对于上面的代码有以下说明:
- 使用 clearModal 方法或者
routerLink="/iotdevice"指令都可以清除备用路由。我惊奇的发现:原来routerLink="/iotdevice"对应的就是 navigateByUrl 而[routerLink]=[]对应的是 navigate, 不过上面的两种做法都是等价的。 - 上面使用了 Material 的 Card 作为模态框,Bootstrap 中有模态框,但是 Material 中却没有!
- 类名 iot-modal 成功生效了,这没有什么问题。
8.7 将子组件/子模块 AddModal 放在父组件能够够得着的地方!
在文件 ad-hoc-moni-list.module.ts 中,引入 IotAddDeviceModule 然后放在 imports 数组中;同时引入 AddModalComponent 并配置好路由,此文件中新增的内容如下:
import { IotAddDeviceModule } from './addModal';
import { AddModalComponent } from './addModal/add-modal.component';
const routes: Routes = [
{
path: 'iotdevice',
component: AdHocMoniListComponent,
canActivate: [RouteGuard],
children: [{
path: 'addnew',
component: AddModalComponent,
outlet: 'addnew',
}]
}
];
...
...
@NgModule({
declarations: [
AdHocMoniListComponent,
],
imports: [
...,
IotAddDeviceModule,
],
...
...
关于上面的代码,这里有一点需要澄清:
- 加了 outlet 配置项的 children 路由,和不加 outlet 的完全是两码子事。也就是说现在无法通过
iotdevice/addnew的 url 找到对应的字组件,只能够通过iotdevice(addnew:addnew)的方式。
8.8 从 list 组件中通过点击按钮的方式跳转到模态框中
<button
[routerLink]="[{ outlets: { addnew: 'addnew' } }]"
[queryParams]="{ name: 'lovely dog', date: '2024-07-09' }"
mat-button
aria-label="Add New"
class="img-btn"
>
<img src="./add.png" alt="Add New" />
</button>
<router-outlet name="addnew"></router-outlet>
上述代码中,我们通过指令的方式完成打开模态框的操作,打开之后模态框渲染到命名路由 addnew 这个位置:
[routerLink]="[{ outlets: { addnew: 'addnew' } }]"规定了将要去的路由地址为:/iotdevice/(addnew:addnew)[queryParams]="{ name: 'lovely dog', date: '2024-07-09' }"增加了查询参数,所以整个路由形式为:[queryParams]="{ name: 'lovely dog', date: '2024-07-09' }"- 在子路由中我们是通过这样的方式获取查询参数的:
ngOnInit() {
this.title = this.route.snapshot.queryParamMap.get('name');
}
- 对于 url:
http://localhost:9000/apps/iot-template/#/iotdevice/(addnew:addnew)?name=lovely%20dog&date=2024-07-09做以下理解:[协议]: http[主机]: localhost[端口]: 9000[后端路由]: /apps/iot-template[前端路由]: /iotdevice/(addnew:addnew)[查询参数]: name=lovely%20dog&date=2024-07-09
9. 完成二级模态框的弹出和关闭,及信息传递。
在 8 的基础之上新增路由规则,新增命名路由。但是这次我决定不再使用包裹一个 Component 的 Module 的这种重量级的做法,取而代之的是另外一种父子组件的组织结构,即轻量级的做法,步骤如下:
9.1 新增子组件 password

不难看出,这次这个名为 password.component.ts 的组件外部并没有使用模块包裹起来,而是直接裸露的,其代码如下:
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { QueryParamService } from "../../services/query-param-stack.service";
@Component({
selector: 'app-password',
template: `<span class="iot-modal">
我是密码输入框,提供的密码是:{{password}} <a (click)="goAddModal()">点击返回</a>
</span>`,
styles: [`
.iot-modal {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
z-index: 9999;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
}
`]
})
export class IotPasswordComponent {
password = '';
lastParmas = {};
constructor(
private router: Router,
private route: ActivatedRoute,
private query: QueryParamService,
) {
this.password = this.route.snapshot.queryParamMap.get('password');
this.lastParmas = this.query.pop() ?? {};
}
goAddModal() {
this.router.navigate([
'/iotdevice', {
outlets: { password: null, addnew: 'addnew' }
}
], {
queryParams: {
...this.lastParmas,
}
})
}
}
关于上面的代码,有以下需要注意的点:
- 我们将原来 split 的代码放置到了一个 component.ts 中,这使得开发小型组建的时候异常的敏捷。
- 使用 template 取代了之前的 templateUrl, 使用了 styles 取代了之前的 styleUrls, 并且我们在 styles 数组第一个元素中写的样式能够非常顺利的在上面的 template 中使用。
- 不仅如此 template 中也可以非常丝滑的使用下面 class 中的属性和方法,本组件内部的数据交换和 split 的代码一样丝滑。
- password 是调用此组件传递的信息;lastParmas 是前一个路由的查询参数;QueryParamService 是新增的一个服务,内部创建了一个栈,起作用是保存上一个路由的查询参数。
- 通过下面两句代码就能获取上述内容:
this.password = this.route.snapshot.queryParamMap.get('password');
this.lastParmas = this.query.pop() ?? {};
- goAddModal 方法是回到另外一个模态框的方法,点击【点击返回】按钮之后跳转。经过尝试,使用 router 上面的 navigate 方法能够从一个命名路由跳转到另外一个命名路由,需要注意的是 outlets 在数组内,queryParams 在数组外
- 我们使用形如
{ password: null, addnew: 'addnew' }的方式清除当前的命名路由。
9.2 将新增的组件放在父组件够得到的地方
在 ad-hoc-moni-list.module.ts 中引入新增组件并为其配置命名导航的路由规则,可以看出来比起 addnew 对应的组件,现在的做法要敏捷的多。
import { IotPasswordComponent } from './password/password.component';
const routes: Routes = [
{
path: 'iotdevice',
component: AdHocMoniListComponent,
canActivate: [RouteGuard],
children: [
{
path: 'addnew',
component: AddModalComponent,
outlet: 'addnew',
},
{
path: 'password',
component: IotPasswordComponent,
outlet: 'password',
}
]
}
];
通过上述的代码,我们成功配置了如同 addnew 一样的 password 命名路由。
9.3 布置锚点
在 ad-hoc-moni-list.component.html 中,在另一个命名路由的旁边添加此次新增的锚点。
<router-outlet name="addnew"></router-outlet>
<router-outlet name="password"></router-outlet>
9.4 从一级模态跳转到二级模态
在 add-modal.component.html 文件的 template 中增加跳转至另外一个模态框的方法:<button mat-button (click)="goPass()">PASS</button>, 然后在对应的组件中添加同名方法:
goPass(password = 123456) {
if (this.params) this.query.push(this.params.params);
this.router.navigate(['/iotdevice', {
outlets: {
password: 'password',
addnew: null,
}
}], {
queryParams: {
password,
}
});
}
this.query 是新增服务的注入实例;password 是传递给下一个模态框的信息。而 this.params 则是从 route 中订阅得到的:
ngOnInit() {
this.title = this.route.snapshot.queryParamMap.get('name');
this.route.queryParamMap.subscribe(data => {
this.params = data;
})
}
这个实践需要注意的点为:
- 轻量级的子组件创建和使用
- 命名路由之间相互跳转和信息传递
- 使用服务的方式而不是查询茶参数的方式完成信息的传递
8 和 9 雷点
雷点就是:下面代码中的 span 可千万不能换成 div!!!
template: `<span class="iot-modal">
我是密码输入框,提供的密码是:{{password}} <a (click)="goAddModal()">点击返回</a>
</span>`,
10. 完成子级路由并在子级路由组件中使用父级路由的数据。
在 9 的基础之上,在 password.component.ts 组件上添加如下代码:
...
@Component({
selector: 'app-password',
template: `<span class="iot-modal">
我是密码输入框,提供的密码是:{{password}} <a (click)="goAddModal()">点击返回{{parentData.path}}</a>
</span>`,
styles: [`...`]
})
export class IotPasswordComponent {
...
parentData;
constructor(
...
) {
...
this.parentData = this.route.parent.snapshot.data;
}
...
}
完全体代码为:
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { QueryParamService } from "../../services/query-param-stack.service";
@Component({
selector: 'app-password',
template: `<span class="iot-modal">
我是密码输入框,提供的密码是:{{password}} <a (click)="goAddModal()">点击返回{{parentData.path}}</a>
</span>`,
styles: [`
.iot-modal {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
top: 0;
z-index: 9999;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
}
`]
})
export class IotPasswordComponent {
password = '';
lastParmas = {};
parentData;
constructor(
private router: Router,
private route: ActivatedRoute,
private query: QueryParamService,
) {
this.password = this.route.snapshot.queryParamMap.get('password');
this.lastParmas = this.query.pop() ?? {};
this.parentData = this.route.parent.snapshot.data;
}
goAddModal() {
this.router.navigate([
'/iotdevice', {
outlets: { password: null, addnew: 'addnew' }
}
], {
queryParams: {
...this.lastParmas,
}
})
}
}
其实从实现上是相当简单的,只要有 route 实例的地方就有 parentData:
this.parentData = this.route.parent.snapshot.data;
那么父组件的 data 来自哪里呢?不管来自 resolver 的挂载,还是预先写在路由规则上,只要在 data 中就能够被拿到!
const routes: Routes = [
{
path: 'iotdevice',
component: AdHocMoniListComponent,
canActivate: [RouteGuard],
data: { path: 'iotdevice' },
children: [
{
path: 'addnew',
component: AddModalComponent,
outlet: 'addnew',
},
{
path: 'password',
component: IotPasswordComponent,
outlet: 'password',
}
]
}
];
注意上面的 data: { path: 'iotdevice' },!
11. 穿插使用三种路由参数:固定参数,可变参数和查询参数。
首先,必须明确 Angular 中的路由种类。
- 首先有固定路由参数,包括以
/开头的,如/data一样的固定路由参数; - 然后是可变路由参数,以
/:开头的,如/:id一样的可变路由参数,必要路由参数放在参数列表的第一位。固定路由参数和可变路由参数都是放在参数数组[]中的。以键值对的方式给出。并且通过同一个 API 接口this.route.snapshot.paramMap.get取值。 - 最后是查询路由参数,查询路由参数的位置在
[]之后的{queryParams:{}}中,以键值对的形式存在,但是查询路由参数使用的却不是paramMap接口,它有自己的接口,queryParamMap.get。 - 因此使用 navigate 做跳转到的时候,基本形式为:
navigate(['a',{b}],{queryParams: {c}})a 为固定路由位置;b 为可变路由位置;c 为查询路由参数位置。 - 如果使用的是路由指令做跳转,那么基本形式为:
[routerLink]="['a',{b}]" [queryParams]="{c}".
12. 完成从详情页回退依然保留表单查询参数的功能。
如果你不想保留上一个路由的查询参数,那么你就使用 queryParams:{} 中的键值对直接指定;而如果你想要在路由跳转前后保持相同的查询参数,那么就是使用 queryParamsHandling : true. 这两种设定处于同一级,并且 queryParamsHandling 会覆盖 queryParams 的设定值。
goAddModal() {
this.router.navigate([
'/iotdevice', {
outlets: { password: null, addnew: 'addnew' },
}
], {
queryParams: {
...this.lastParmas,
backFromPassword: true,
},
queryParamsHandling: 'preserve',
})
}
上述方法执行之后,新的 url 中只会剩下保留的查询参数:
http://localhost:9000/apps/iot-template/#/iotdevice/(addnew:addnew)?password=123456