
引言
在传统的页面开发中,我们通过在浏览器中输入不同的页面路径,来访问不同的的 html 页面。但在 SPA(single page application)页面,我们的框架通过匹配不同的路由地址,来按照一种约定的规则动态将我们页面里的内容替换为我们在模板中编写的内容。
在业务设计中,我们可能通过路由进行信息的传递、收集。对于一些面向 C 端的产品,可能还需要统计不同的路由访问的次数,对相关信息进行上报、分析等。
在开发过程中,我们则需要对路由的事件进行监听,对一些关键的参数,通常也会采用路由传递参数。
因此,不论在哪个方面,路由在 SPA 项目中的作用都是至关重要的。
需求分析
在业务开发初期,对于路由的设计会相对简单。但随着业务的不断拓展,很可能会需要对动态路由导航的支持。例如对于下面这种路由导航:


该组件,支持对数据的实时展示,也支持动态时间段的筛选。跟过路由,便可以大概获取到关键性的一些参数。
我们来查看他的路由结构:
import { Routes } from '@angular/router';
...
export const routing: Routes = [
{
path: '',
component: MainPageComponent,
children: [
{
path: UrlPath.REAL_TIME,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: '/' + UrlPath.MAIN
},
{
path: ':' + UrlPathId.APPLICATION,
resolve: {
serverTime: ServerTimeResolverService
},
data: {
showRealTimeButton: true,
enableRealTimeMode: true
},
component: MainContentsContainerComponent
}
]
},
{
path: ':' + UrlPathId.APPLICATION,
children: [
{
path: '',
pathMatch: 'full',
data: {
path: UrlPath.MAIN
},
component: UrlRedirectorComponent
},
{
path: ':' + UrlPathId.PERIOD,
children: [
{
path: '',
pathMatch: 'full',
data: {
path: UrlPath.MAIN
},
component: UrlRedirectorComponent
},
{
path: ':' + UrlPathId.END_TIME,
data: {
showRealTimeButton: true,
enableRealTimeMode: false
},
component: MainContentsContainerComponent
}
]
}
]
},
{
path: '',
pathMatch: 'full',
component: EmptyContentsComponent,
}
]
},
{
path: '**',
component: MainPageComponent
},
];
对于这种参数不固定的动态路由,如果此时我们需要针对路由信息进行上报、根据路由参数动态匹配组件,并进行路由参数的提取,我们有以下两种处理方案:
-
在每个需要的地方去注入路由实例,获取对应的信息。在不同的页面初始化的时候,对页面的信息进行上报。
-
在全局注册一个服务(service), 用于收集路由信息, 根据当前信息去匹配出一些必要的参数。并在 service 中统一管理对路由事件的上报。
哪一种方案更好,这里就不讨论了。
设计路由收集器
根据需求,在全局位置上,我们会有一个路由收集器,负责收集页面中的参数。 我们通过 ActivatedRoute 来获取当前组件相关的路由信息, 并实现一个方法将路由中的信息提取出来,设置在 service 中。
ActivatedRoute 可用于遍历路由器的状态
常用的属性有:
snapshot, 用于获取当前路由树的快照信息。
params, 返回当前路由范围内矩阵参数的observable。
queryParams, 返回当前路由范围内查询参数的observable。
firstChild, 用于获取当前路由的第一个子路由。
children, 用于获取当前路由的所有子路由。
关于 ActivatedRoute 更多使用,可以参考官方文档
编写 route-collector.interface.ts 声明文件:
// route-collector.interface.ts
import { ParamMap, ActivatedRoute } from '@angular/router';
export interface RouteCollectorInterface {
collectUrlInfo(activatedRoute: ActivatedRoute): void;
setData(dataMap: Map<string, string>, routeData: ParamMap): void;
}
实现路由搜索服务
根据设计好的路由收集的接口,我们需要去编写具体的业务逻辑。在 app.module.ts 中,注入一个 service 服务,并实现 RouteCollectorInterface 接口:
// route-collect.service.ts
import { Injectable } from '@angular/core';
import { ParamMap, ActivatedRoute } from '@angular/router';
@Injectable()
export class RouteCollectorService implements RouteInfoCollectorService {
constructor(
private routeManageService: RouteManagerService,
private analyticsService: AnalyticsService,
) {}
collectUrlInfo(activatedRoute: ActivatedRoute): void {
console.log('activatedRoute ==>', activatedRoute);
}
private setData(dataMap: Map<string, string>, routeData: ParamMap): void {
routeData.keys.forEach((key: string) => {
dataMap.set(key, routeData.get(key));
});
}
}
在 app.component.ts 中,我们在页面初始化, 去监听路由的变化事件:
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { RouteCollectorService } from './service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(
private routeCollectorService: RouteCollectorService,
private router: Router,
private activatedRoute: ActivatedRoute,
) {}
ngOnInit() {
this.listenToRouteChange();
}
private listenToRouteChange(): void {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => {
this.routeCollectorService.collectUrlInfo(this.activatedRoute);
});
}
}
在根组件中,我们监听 NavigationEnd 事件,将此时的路由信息 activatedRoute 传递给 collectUrlInfo 方法。运行项目,打开控制台,我们可以看到输出信息:

查看当前路由快照信息:

可以看到,ActivatedRoute 在快照中保留了当前组件路由的组件树。
遍历路由树,取出所要的数据:
// route-collect.service.ts
import { Injectable } from '@angular/core';
import { ParamMap, ActivatedRoute } from '@angular/router';
@Injectable()
export class RouteCollectorService implements RouteInfoCollectorService {
constructor(
private routeManageService: RouteManagerService,
private analyticsService: AnalyticsService,
) {}
collectUrlInfo(activatedRoute: ActivatedRoute): void {
// 定义 路由参数 Map, 及路由查询参数 Map
const pathParamMap = new Map<string, string>();
const queryParamMap = new Map<string, string>();
let routeChild = activatedRoute.snapshot.firstChild;
while (routeChild) {
// 设置路由参数
this.setParamsMap(pathParamMap, routeChild.paramMap);
// 设置路由查询参数
this.setParamsMap(queryParamMap, routeChild.queryParamMap);
routeChild = routeChild.firstChild;
}
console.dir('pathParamMap', pathParamMap);
console.dir('queryParamMap', queryParamMap);
}
// 用于提取路由参数及路由查询参数 Map 中的信息
private setParamsMap(dataMap: Map<string, string>, routeData: ParamMap): void {
routeData.keys.forEach((key: string) => {
dataMap.set(key, routeData.get(key));
});
}
}
通过 while 循环,遍历所有的子路由,取出对应的 params 信息并收集到 Map 中,打印结果可以看到:

// route-collect.service.ts
...
collectUrlInfo(activatedRoute: ActivatedRoute): void {
// 定义 路由参数 Map, 及路由查询参数 Map
const pathParamMap = new Map<string, string>();
const queryParamMap = new Map<string, string>();
let routeChild = activatedRoute.snapshot.firstChild;
let configData = {};
while (routeChild) {
// 设置路由参数
this.setParamsMap(pathParamMap, routeChild.paramMap);
// 设置路由查询参数
this.setParamsMap(queryParamMap, routeChild.queryParamMap);
// 设置路由配置信息
configData = { ...configData, ...routeChild.data };
routeChild = routeChild.firstChild;
}
console.dir('configData', configData);
}
...
打印配置信息,可以看到:

可以看到,针对当前路由配置的 data 和状态参数都被获取过来了。
路由信息使用建议
完成了对路由参数、路由查询参数、路由配置信息的动态获取。我们只需要在我们需要的地方保存我们的路由信息,在一个合适的时机去发起 track 动作,来向服务器汇报我们的路由信息了。
关于路由信息储存,这里推荐两种保存信息的方案:
- 使用 service 服务中保存路由信息,在需要的地方注入对应的 service 实例即可。
- 将数据保存在 @ngrx/store 状态中。
对于方案一,我们可以在对应的 service 中随意操作我们收集到信息,这种方式在全局可用,使用的时候只需要注入对应实例即可,拓展性较强,缺点是我们需要额外定义 Observable 流来广播我们的路由信息,需要多写代码去和组件中其他的 Observable 组合来使用。
对于方案二,我们在 @ngrx/store 中保存我们路由信息的状态,通过定义不同的 Action 来操作我们的路由信息即可。这种方式在配合 @ngrx/effect 副作用的时候,在 组件 和 service 中获取该信息都变的更加方便,并且 Ngrx 会帮我们把这种获取转换为 Observable 流方便我们在组件中结合其他 Observable 使用,缺点是需要额外引入 @ngrx 第三方业务体系。
两种方案各有利弊,在使用中还需要结合具体的业务场景来做。
总结
在项目中,我们经常配置各种路由来渲染我们的组件并进行参数传递。对于这种业务,应考虑在一个特定的地方去处理路由信息,以某种机制来统一分发路由状态,将业务中所需要的所有信息统一收集,统一分发,统一上报。
这样可以在一定程度上减轻组件内与路由相关的业务。
感谢您的阅读~
