|| 0 前言
在对接设计师所给的设计图进行页面的过程中,有一块区域使用到了面包屑的设计。而项目用的组件库是ng-zorro-antd
,自然而然的,便用到了nz-breadcrumb
这个控件。
虽说ng-zorro
给出的面包屑控件,根据文档的指示,基本可以解决大部分需求。但是遇到一些深层定制化的需求的时候,就不仅仅是config data
那么简单了。
举例一个实际场景:在供应商列表页面,点击供应商名称,跳转至供应商详细,按照面包屑的生成结构,就是
供应商列表>供应商详细
。但如果要求要求显示供应商列表>:供应商名称
。那这个面包屑要动态显示。因为供应商详细
是静态唯一的,而:供应商名称
是多样可变的。
最后,去ng-zorro开发团队的开发博客和源码地址拜读了一圈,结合发布者订阅模式进行了二次开发,实现重撒面包屑,实现了预期目标。另外解决了页面刷新时,面包屑会消失的问题。
在阅读控件源码以及查找资料的过程中,发现Angular路由机制的门道还蛮深的。想着不如借此机会,琢磨一下控件代码的实现逻辑,进而深化一下自己的知识结构。因此有了本文的记录。
本文中存在的任何错误与不足,欢迎指正,在此提前感谢。
接下来文章会从以下两个点做探讨:
-
各模块中的Routes,本质上就是一个不断嵌套的路由树。
-
为什么ng-zorro的文档要指出:懒加载的时候,data属性要绑在父路由?
|| 项目结构
// 这里只列出重要部分:src-->app下的文件结构
|-- app
|-- component 组件
|-- breadcrumb 面包屑组件
|-- component.module.ts
|-- layout
|-- basic-layout 基础渲染页 // /page/*的component都从这里面的<router-outlet></router-outlet>进行映射
|-- layout.module.ts
|-- page
|-- agreement-list 协议管理页
|-- vendor-list 供应商列表页
|-- vendor 供应商详细页
|-- page.module.ts
|-- app.component.ts
|-- app.modules.ts
|-- app-routing.module.ts 根路由模块
|| 1 routing-module中的routes,本质上就是不断嵌套的路由树
1.1先来看一下app-routing.module.ts
和page.module.ts
中的routes
// app-routing.module.ts中的routes
const routes: Routes = [
{path: '', redirectTo: 'page', pathMatch: 'full'},
{path: 'page', loadChildren: () => import('./page/page.module').then(m => m.PageModule), data: {breadcrumb: '供应商列表'}}
{path: '**', redirectTo: 'page'}
]
// page.module.ts中的routes
const routes: Routes = [
// router执行顺序会按照顺序自上而下执行,请尽量扁平化的实现
{path: '', redirectTo: 'vendor-list', pathMatch: 'full'},
// 复用的basic-layout.component可以重复写入
{
// 因为子路由的渲染出口是在父路由的页面上
// 因此当嵌套路由配置完成之后,在嵌套的父级页面上,我们需要定义一个 <router-outlet> 标签用来指定子路由的渲染出口
path: 'vendor-list', component: BasicLayoutComponent,
children: [
{path: '', component: VendorListComponent},
{path: 'vendor/:type/:id', component: VendorComponent, data: {breadcrumb: '供应商详情'}},
]
}, {
path: 'agreement-list', component: BasicLayoutComponent, data: {breadcrumb: '协议管理'},
children: [
{path: '', component: AgreementListComponent}
]
},
{path: '**', redirectTo: 'vendor-list'},
]
1.2根据两个路由的从属关系,将loadChildren
的部分转化成路由结构,拼出来应该是这样的:
{ path: 'page',
data: {breadcrumb: '供应商列表'},
children: [
{path: '', redirectTo: 'vendor-list', pathMatch: 'full'},
{
path: 'vendor-list', component: BasicLayoutComponent,
children: [
{path: '', component: VendorListComponent},
{path: 'vendor/:type/:id', component: VendorComponent, data: {breadcrumb: '供应商详情'}},
]
},
{
path: 'agreement-list', component: BasicLayoutComponent, data: {breadcrumb: '协议管理'},
children: [
{path: '', component: AgreementListComponent},
]
},
{path: '**', redirectTo: 'vendor-list'}
]}
1.3转化成视图的话就长下面这样:
|| 2 为什么懒加载的时候,data属性要绑在父路由?
2.1先了解一下懒加载
以及data
属性的定义。
懒加载
,顾名思义,是不急于加载的意思。只有当一个路由完全匹配,才会加载对应切割的代码模块。
在app-routing.module.ts
中:
//app-routing.module.ts
{path: 'page', loadChildren: () => import('./page/page.module').then(m => m.PageModule), data: {breadcrumb: '供应商列表'}},
其中,loadChildren
后跟的箭头函数是个异步加载方法,也就是说,当我们输入路由“/page”的时候,才会加载page.module。
再来看看data
这个属性。
根据node_modules/@angular/router/router.d.ts
中,对于data
的解释:
data
只是个静态资源。所以在加载对应路由之前,会先加载静态资源。即data
会在loadChildren
之前被加载。
另外需要注意的是,实际上,懒加载
这件事,不是路由
去实现的,而是由webpack
进行分割打包。而Angular
又对底层的webpack
语句进行了封装,通过辨识 loadChildren
让webpack
去切割代码块。
2.2为什么需要在父层路由写data?
先上breadcrumb.component.html
和breadcrumb.component.ts
代码:
<!--breadcrumb.component.html start-->
<nz-breadcrumb>
<nz-breadcrumb-item>首页</nz-breadcrumb-item>
<nz-breadcrumb-item *ngFor="let breadcrumb of breadcrumbs">
<a [routerLink]="[breadcrumb.url, breadcrumb.params]">{{breadcrumb.label}}</a>
</nz-breadcrumb-item>
</nz-breadcrumb>
<!--breadcrumb.component.html end-->
// breadcrumb.component.ts
import {ActivatedRoute, NavigationEnd, Router, PRIMARY_OUTLET} from '@angular/router';
import {filter} from 'rxjs/operators';
import { startWith } from 'rxjs/internal/operators/startWith';
···
constructor(
private activatedRoute: ActivatedRoute, // 当前路由服务
private router: Router
) { }
ngOnInit() {
// console.log(this.activatedRoute.snapshot.params);
console.log(this.activatedRoute.data.subscribe(data => console.log(data)));
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
// 执行初始化渲染。因为对于app-routing下的子组件来说,子组件的生命周期在NavigationEnd结束后才执行init,错过了路由变化的监听
// startWith: 在subscribe开始之前,一开始就同步发出,常被用来保存程式的起始状态。
startWith(true)
).subscribe(event => {
console.log(this.activatedRoute);
const root: ActivatedRoute = this.activatedRoute.root;
this.childIndex = 1;
this.breadcrumbs = this.getBreadcrumbs(root);
});
}
getBreadcrumbs(
route: ActivatedRoute,
url: string = '',
breadcrumbs: BreadcrumbItem[] = []): BreadcrumbItem[] | undefined {
const ROUTE_DATA_BREADCRUMB = 'breadcrumb';
// get children route
const children: ActivatedRoute[] = route.children;\
// if there is no children route, return
if (children.length === 0) {
return breadcrumbs;
}
// 这里的遍历打印出来,发现会执行多次,是因为会从app-routing.module.ts作为第一层算起,从上往下找,直到找到component所在的层级,则遍历结束。
for (const child of children) {
console.log('第' + (this.childIndex++) + '次遍历');
if (child.outlet === PRIMARY_OUTLET) {
console.log(child.snapshot.url, route.snapshot.data);
const routeUrl: string = child.snapshot.url.map(segment => segment.path).join('/');
const nextUrl = url.replace(/\/$/, '') + `${routeUrl}`;
// 此处要做routeUrl的非空判断,否则会因为some.module.ts的path为''而塞入一个虚路由,出现双皮奶
if (routeUrl !== '' && route.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB)) {
const breadcrumb: BreadcrumbItem = {
label: child.snapshot.data[ROUTE_DATA_BREADCRUMB],
params: child.snapshot.params,
url: nextUrl
};
breadcrumbs.push(breadcrumb);
console.log(breadcrumb);
}
// 递归
return this.getBreadcrumbs(child, url, breadcrumbs);
}
}
}
···
在breadcrumb.component.ts
中,面包屑的生成逻辑为:通过this.activatedRoute.root
获取当前路由的根路由,然后从根路由向下获取所有子节点。因此,在输入locahost:4200/page/vendor-list
的时候,获取的所有子节点如下图所示。而标红部分也就是ActivatedRoute
。
接下来,让我们修改data
的放置位置,对比上文1.1
中,app-routing.module.ts
和page.module.ts
中data
放置的位置。观察breadcrumb.component.ts
中console.log(child.snapshot.url, route.snapshot.data)
的输出结果,来进行对比。
// breadcrumb.component.ts
···
console.log(child.snapshot.url, route.snapshot.data);
···
另外,需要说明的是,child.snapshot.url
不为空且route.snapshot.data
中有breadcrumb才会被推进Array。
// breadcrumb.component.ts
···
if (routeUrl !== '' && route.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB)) {
···
2.2.1 根据1.1
中的data
的放置位置,输出结果为:
url(不可为空) | data(要有breadcrumb) | 结果 |
---|---|---|
'page' | {} | 剔除 |
‘vendor-list’ | {breadcrumb:’供应商列表’} | 存入Array |
'' | {breadcrumb:’供应商列表’} | 剔除 |
2.2.2 修改data
放置位置
// app-routing.module.ts 去除data
{path: 'page', loadChildren: () => import('./page/page.module').then(m => m.PageModule)},
// page.module.ts 放置data
{path: 'vendor-list', component: BasicLayoutComponent, data: {breadcrumb: '供应商列表'},
输出结果为:
url(不可为空) | data(要有breadcrumb) | 结果 |
---|---|---|
'page' | {} | 剔除 |
‘vendor-list’ | {} | 剔除 |
'' | {breadcrumb:’供应商列表’} | 剔除 |
再回想一下,上文中指出的面包屑生成逻辑:从根路由层层扒拉出path
和data
中的breadcrumb
。从和2.2.2
可知,从根路由的data
中就没有breadcrumb
,自然拼不出面包屑。
2.2.3当然,有可能你已经发现,在2.2.1
中,我在父层路由绑定了data,为什么path
为'page'
的时候,输出的data
没有breadcrumb
?
经过一翻研究,在此做出如下解释:
① ActivatedRoute
的定义:包含当前激活插座的组件的路由信息。
也就是说,就是根据路径path
追踪到真正实例化的组件component
部分,才是真正的激活路由。
②在node_modules/@angular/router/router.d.ts
中,有这么一段话:
而app-routing.ts
中,懒加载路由是没有指定component的,所以data流向了子路由,在breadcrumb.component.ts
的遍历方法中,'page'
层没打印出{breadcrumb: 'xxx'}
,而它的子路由'vendor-list'
和''
会拿到。
接下来,我们通过上述两点,结合1.2
中agreement-list
的嵌套路由,以及面包屑组件中的实现逻辑url不为空,且data中有breadcrumb
:
···
{
path: 'agreement-list', component: BasicLayoutComponent, data: {breadcrumb: '协议管理'},
children: [
{path: '', component: AgreementListComponent},
]
},
···
由此可知,真正实例化的组件是AgreementListComponent
这一层,BasicLayoutComponent
只是作为映射层,但是AgreementListComponent
层的path
为空,所以要将data
放到父层路由。
|| 3 其他补充
3.1为什么刷新页面,面包屑会丢失
NG-ZORRO 开发博客:自动生成面包屑一文中给出了原因和答案。
对于component的非子组件,
ngOninit
钩子在路由事件之前执行;而component的子组件,子组件路由结束事件在生命周期执行钩子前完成。
而从各个component的ngOninit
执行顺序和路由监听可知,除了app.component.ts
的ngOninit在路由变化前执行,其余xxxxx.component.ts
的ngOninit
都在路由周期结束之后才执行。
因为breadcrumb.component.ts
的ngOninit
执行在路由事件之后,ngOninit
中是执行监听路由变化的方法,生命周期在路由事件之后,两者相互错过,所以出现刷新页面导致面包屑丢失。
解决方案:
监听路由事件的时候,增加pipe,使用rxjs的startWith(true),强制在初始化的时候先执行。
|| 4 参考资料
[01]真香官网
[02]NG-ZORRO 开发博客:自动生成面包屑
[03]Angular路由管理过程浅谈
[04]Lazy Loading Angular - Code Splitting NgModules with Webpack
[05]顺便可以深化了解的内容:重新认识angular生命周期
|| 5 总结
文章脉络写在开头,结尾不再赘述。文章中有任何不足,可以直接评论区指出啦。
很感激在琢磨的过程中,前辈们的思路提供,当中有好几次的琢磨方向都劈了叉(比如以为路由中的component不可以被重复使用,以为懒加载是路由做的事情等等),感谢他们提供的一些思考方向,让我少走更多的弯路。
**
最后一趴,和正文没太大关系了。随意比比一下自己的研究目的和心情,顺带聊下这段时间的部分反思。
*分析了下自己所执行的工作角色,根本上来说就是去不断的解决问题。
除了技术实现的问题,还有更多需要结合业务场景去思考解决的问题。解决问题的过程让人痛苦与快乐并发,问题解决的那一刻是高光时刻。
*然后吧,自己又是个一个极其喜欢记录问题/兴趣点,然后去花时间琢磨琢磨自得其乐的人。
就是喜欢琢磨事,只要感兴趣的,什么事都可以。
这里的问题记录,指的是为什么答案提供者是怎样的解法。比如,为什么ng-zorro
提出懒加载的时候要把data
放在父路由。
无论是业务或是技术,都建议具体问题具体分析,从一个感兴趣的点/问题点去切入并拓宽,再回头看理论,比起一开始拿着一个框架图去走马观花扫览一通,更有助于扎实的吸收。
*在给出解决办法前的那一段思考过程十分珍贵。站在什么思考角度,用怎样的思考逻辑。这有助于对某一个问题的解决方案有了更深的认知,去更好的构建个人知识体系。
*这应该说得上是我对沉淀
的执着。
但如果开发节奏时常保持在一个完成就用,下一个
的状态。经常完成一个需求之后,就马不停蹄的继续开发下一个需求环节。这样过于动态的开发节奏和沉淀的静态需求之间就成了矛盾。
加上一些其他琐碎和意外裹挟而来。敏感体质放大那些焦虑和失落感。而在处理疏通自己的负面情绪,是我极其不擅长的领域。只会看它累在问题记录表上越堆越高,直到它们自己一股脑的发出来。这种内耗是极其要命的。
有相当一段时间,对于常看的学习网站,我连打开浏览都做不到。觉得自己是一块快速掉电的电池,工作和生活中的琐碎一般耗掉90%的电量,红色的余量支撑一个积极的心态都岌岌可危。看动画片不香吗?厨房它不香吗?出去走走不好吗?为什么不去做一些可以忘记我当前角色的事情呢。
*不得不说,逃避虽然可耻但是有效。
毕竟再怎么鼓励自己,给自己打气,和自己说:坚持住,保持那样的吸收状态,你会更强云云。这些都抵不过在心头真实盘踞的失落焦躁的心情。想要沉淀
,就得先把这些盖在上面的情绪清理掉,否则就不能恢复我想要的节奏。
*学会有效的休息和有效的工作,使两者进行互相的正向反馈互相制衡,这个能力太重要了。
很是佩服那些工作再忙,也依旧在社区辛勤分享的作者们。时间挤挤是会有,但是拿挤出的时间选择学习、积累、内化,和梳理、产出、分享,并能够保持积极的心态和节奏,这点是真真厉害。
*只要自己想做的事在进行中,那慢一点也不要紧。耐心点。只要你在做着喜欢的事
和想做的事
,确认做的事情源于自己的选择。那无论你是选择看更多的影片或者是报更多的学习课程,其实都可以。想做就去做。
比比到这,差不多了。比正文都长了。
最后,时间进入今年的下半程了,大家都要健康好运哇。