美文网首页
从nz-breadcrumb源码窥探Angular路由

从nz-breadcrumb源码窥探Angular路由

作者: 贪心xiong | 来源:发表于2020-07-14 21:17 被阅读0次
    图源Behance | <Daily UI | #056 | Breadcrumbs> | Rabee Balakrishnan

    || 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.tspage.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的解释:

    node_modules/@angular/router/router.d.ts

    data只是个静态资源。所以在加载对应路由之前,会先加载静态资源。即data会在loadChildren之前被加载。

    另外需要注意的是,实际上,懒加载这件事,不是路由去实现的,而是由webpack进行分割打包。而Angular又对底层的webpack语句进行了封装,通过辨识 loadChildrenwebpack去切割代码块。

    2.2为什么需要在父层路由写data?

    先上breadcrumb.component.htmlbreadcrumb.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

    ActivatedRoute(包含当前激活插座的组件的路由信息)

    接下来,让我们修改data的放置位置,对比上文1.1中,app-routing.module.tspage.module.tsdata放置的位置。观察breadcrumb.component.tsconsole.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:’供应商列表’} 剔除

    再回想一下,上文中指出的面包屑生成逻辑:从根路由层层扒拉出pathdata中的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.2agreement-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.tsngOninit都在路由周期结束之后才执行。
    因为breadcrumb.component.tsngOninit执行在路由事件之后,ngOninit中是执行监听路由变化的方法,生命周期在路由事件之后,两者相互错过,所以出现刷新页面导致面包屑丢失。

    解决方案:

    监听路由事件的时候,增加pipe,使用rxjs的startWith(true),强制在初始化的时候先执行。


    startWith():一开始就同步发出,常被用来保存程式的起始状态。

    || 4 参考资料

    [01]真香官网
    [02]NG-ZORRO 开发博客:自动生成面包屑
    [03]Angular路由管理过程浅谈
    [04]Lazy Loading Angular - Code Splitting NgModules with Webpack
    [05]顺便可以深化了解的内容:重新认识angular生命周期

    || 5 总结

    文章脉络写在开头,结尾不再赘述。文章中有任何不足,可以直接评论区指出啦。

    很感激在琢磨的过程中,前辈们的思路提供,当中有好几次的琢磨方向都劈了叉(比如以为路由中的component不可以被重复使用,以为懒加载是路由做的事情等等),感谢他们提供的一些思考方向,让我少走更多的弯路。



    **
    最后一趴,和正文没太大关系了。随意比比一下自己的研究目的和心情,顺带聊下这段时间的部分反思。

    *分析了下自己所执行的工作角色,根本上来说就是去不断的解决问题。

    除了技术实现的问题,还有更多需要结合业务场景去思考解决的问题。解决问题的过程让人痛苦与快乐并发,问题解决的那一刻是高光时刻。

    *然后吧,自己又是个一个极其喜欢记录问题/兴趣点,然后去花时间琢磨琢磨自得其乐的人。

    就是喜欢琢磨事,只要感兴趣的,什么事都可以。

    这里的问题记录,指的是为什么答案提供者是怎样的解法。比如,为什么ng-zorro提出懒加载的时候要把data放在父路由。

    无论是业务或是技术,都建议具体问题具体分析,从一个感兴趣的点/问题点去切入并拓宽,再回头看理论,比起一开始拿着一个框架图去走马观花扫览一通,更有助于扎实的吸收。

    *在给出解决办法前的那一段思考过程十分珍贵。站在什么思考角度,用怎样的思考逻辑。这有助于对某一个问题的解决方案有了更深的认知,去更好的构建个人知识体系。

    *这应该说得上是我对沉淀的执着。

    但如果开发节奏时常保持在一个完成就用,下一个的状态。经常完成一个需求之后,就马不停蹄的继续开发下一个需求环节。这样过于动态的开发节奏和沉淀的静态需求之间就成了矛盾。

    加上一些其他琐碎和意外裹挟而来。敏感体质放大那些焦虑和失落感。而在处理疏通自己的负面情绪,是我极其不擅长的领域。只会看它累在问题记录表上越堆越高,直到它们自己一股脑的发出来。这种内耗是极其要命的。

    有相当一段时间,对于常看的学习网站,我连打开浏览都做不到。觉得自己是一块快速掉电的电池,工作和生活中的琐碎一般耗掉90%的电量,红色的余量支撑一个积极的心态都岌岌可危。看动画片不香吗?厨房它不香吗?出去走走不好吗?为什么不去做一些可以忘记我当前角色的事情呢。

    *不得不说,逃避虽然可耻但是有效。

    毕竟再怎么鼓励自己,给自己打气,和自己说:坚持住,保持那样的吸收状态,你会更强云云。这些都抵不过在心头真实盘踞的失落焦躁的心情。想要沉淀,就得先把这些盖在上面的情绪清理掉,否则就不能恢复我想要的节奏。

    *学会有效的休息和有效的工作,使两者进行互相的正向反馈互相制衡,这个能力太重要了。

    很是佩服那些工作再忙,也依旧在社区辛勤分享的作者们。时间挤挤是会有,但是拿挤出的时间选择学习、积累、内化,和梳理、产出、分享,并能够保持积极的心态和节奏,这点是真真厉害。

    *只要自己想做的事在进行中,那慢一点也不要紧。耐心点。只要你在做着喜欢的事想做的事,确认做的事情源于自己的选择。那无论你是选择看更多的影片或者是报更多的学习课程,其实都可以。想做就去做。

    比比到这,差不多了。比正文都长了。

    最后,时间进入今年的下半程了,大家都要健康好运哇。

    相关文章

      网友评论

          本文标题:从nz-breadcrumb源码窥探Angular路由

          本文链接:https://www.haomeiwen.com/subject/qdizcktx.html