基于Angular2+的Tab页+侧边菜单比较完整的解决方案

一、具体需求:

1. 页面布局

左侧导航,右侧侧边辅助菜单,中间主业务界面;左侧导航是根据用户权限动态生成的。

2. 支持Tab页

中间的主业务界面是通过Tab页的方式实现,可以根据左侧导航栏的点击行为动态打开或切换到相应的Tab页,当然,这里的Tab页要支持Tab页基本应该具有的功能特点,包括打开、切换和关闭(拖换Tab页次序功能后期可以试试加上)。

3. Tab页特性

Tab页内的内容是一个Angular的组件,在Tab页切换的时候要求组件状态保持(不重新加载,即组件数据不丢失)。

4. 辅助菜单功能

辅助菜单完成的功能是对主业务界面的具体数据进行操作,比如主业务是“用户管理”,显示的是用户数据表,辅助菜单显示的就是用户数据表的具体某一行的内容(如姓名、工号、电话……)。

5. 辅助菜单特性

辅助菜单根据主业务表显示的组件动态切换内容,也要求状态保持。

二、技术路线:

1. 界面布局

分为左中右三部分,左侧导航和右侧辅助菜单分别是一个组件。

2. 支持Tab页

使用mat-tab-group标签实现,Tab页内加载组件通过正常的前端路由实现。

3. Tab页特性

状态保持通过自定义路由策略(RouteReuseStrategy)实现。

4. 辅助菜单功能

通过辅助路由实现,难点在于主业务部分与辅助菜单部分传值方式。

5. 辅助菜单特性

根据具体业务逻辑,加载不同的主路由与辅助路由,辅助路由部分同样采用自定义路由策略来实现状态保持。

三、具体实现:

1. 页面布局:

code

<app-header></app-header>
<div class="app-body">
  <app-sidebar></app-sidebar>
  <!-- Main content -->
  <main class="main">
    <!-- Breadcrumb -->
    <!--<ol class="breadcrumb"><app-breadcrumbs></app-breadcrumbs></ol>-->
    <div>
      <mat-tab-group [(selectedIndex)]="tabIndex">
        <mat-tab *ngFor="let link of navLinks">
          <ng-template mat-tab-label>
            <span (click)="tabChanged(link.code)">{{link.label}}
              <i class="fa fa-close fa-lg" style="margin: 2px 0px 0px 5px" (click)="closeTab(link.code)"></i>
            </span>
          </ng-template>
        </mat-tab>
      </mat-tab-group>
      <router-outlet></router-outlet>
    </div>
  </main>
  <app-aside></app-aside>
</div>
<app-footer></app-footer>

布局解释

  • <app-sidebar></app-sidebar>导航部分。
  • <app-aside></app-aside>辅助菜单部分。
  • 两者中间是主业务界面部分。

Note

  1. 先说主业务界面部分,Angular Material天然提供了一种可以通过Tab页的方式加载组件的解决方案如下:

    <nav mat-tab-nav-bar>
      <a mat-tab-link
        *ngFor="let link of navLinks"
        [routerLink]="link.path"
        routerLinkActive #rla="routerLinkActive"
        [active]="rla.isActive">
        {{link.label}}
      </a>
    </nav>
    <router-outlet></router-outlet>
    

    这种解决方案有以下缺陷:

    • 当Tab页标签多于界面一行可容纳的数量时,导致出现第二行Tab页标签,而不是在一行Tab页标签首尾出现左右可移动的箭头,来使用户可以左右点击实现Tab页标签可见窗口的滑动。
    • 不支持组件的状态保持,Tab页的切换会导致非当前Tab页相应组件的销毁(揣测Angular或许提供防止组件销毁的接口,但是没仔细去调研)。

    主界面Tab页逻辑代码部分:

    public tabChanged(code) {
      for (let count = 0; count < this.navLinks.length; count++) {
        if (this.navLinks[count].code === code) {
          this.tabIndex = count;
          this.router.navigateByUrl(this.navLinks[count].path + '?paramsExtras=' + this.paramsExtras).then();
          this.toggleWorkShop(this.params[this.navLinks[count].moduleName].isOpenedWorkshop);
          break;
        }
      }
    };
    public closeTab(code) {
      for (let count = 0; count < this.navLinks.length; count++) {
        if (this.navLinks[count].code === code) {
          const paramsExtras = {
            reference: this.navLinks[count]['path'],
            isLast: false
          };
          if (count !== 0) {
            this.tabIndex = count - 1;
            this.router.navigateByUrl(this.navLinks[count - 1].path + '?paramsExtras=' +  JSON.stringify(paramsExtras)).then();
            this.toggleWorkShop(this.params[this.navLinks[count - 1].moduleName].isOpenedWorkshop);
          } else {
            if (this.navLinks.length !== 1) {
              this.tabIndex = count;
              this.router.navigateByUrl(this.navLinks[count + 1].path + '?paramsExtras=' +  JSON.stringify(paramsExtras)).then();
              this.toggleWorkShop(this.params[this.navLinks[count + 1].moduleName].isOpenedWorkshop);
            } else {
              this.tabIndex = count;
              paramsExtras.isLast = true;
              this.router.navigateByUrl('tasks/(manage-board//sub:user-workshop)' + '?paramsExtras=' +  JSON.stringify(paramsExtras)).then();
              this.toggleWorkShop(this.params['manageboardModule'].isOpenedWorkshop);
            }
          }
          this.navLinks.splice(count, 1);
          break;
        }
      }
    }
    public updateArrays(linkInfoStringify) {
      const linkInfo = JSON.parse(linkInfoStringify);
      let updateFlag = true;
      for (let count = 0; count < this.navLinks.length; count++) {
        if (linkInfo.code === this.navLinks[count].code) {
          this.tabIndex = count;
          this.router.navigateByUrl(linkInfo.path + '?paramsExtras=' + this.paramsExtras).then();
          updateFlag = false;
          break;
        }
      }
      if (updateFlag) {
        this.navLinks.push(linkInfo);
        this.tabIndex = this.navLinks.length - 1;
        this.router.navigateByUrl(linkInfo.path + '?paramsExtras=' + this.paramsExtras).then();
        this.toggleWorkShop(this.params[linkInfo.moduleName].isOpenedWorkshop);
      }
    }
    public toggleWorkShop(newTabIsOpenedWorkshop) {
      if (!document.querySelector('body').classList.contains('aside-menu-hidden') || newTabIsOpenedWorkshop) {
        document.querySelector('body').classList.toggle('aside-menu-hidden');
      }
    }
    

    代码解释:

    • navLinks数组元素结构形如{path:"url",label:"labelName",code:"privilegeCode",moduleName:"moduleName"},是由导航项点击传过来的。
    • Tab页的切换触发方式有两种,一种是点击Tab页标签,一种是点击左侧导航菜单。
    • 在点击Tab页标签时,会调用tabChange函数,并传入模块所对应的权限代码code,然后遍历navLinks数组,如果找到对应code的项,则读出其中的前端路由,将当前索引值记下,然后跳转同时终止循环,并判断此刻辅助菜单的打开状态。
    • 在点击左侧导航菜单项时,在导航类中触发本类中的updateArrays函数,将一个navLinks数组元素传入,判断该元素内的code值是否已经存在,即是否已经在Tab页的label栏中存在(激活状态或者未激活状态),如果已经存在,则将当前组件所对应的Tab页激活,记录下索引并跳转,且终止循环;如果当前code并不存在,则将当前code所对应的内容压入navLinks数组,并把当前激活状态的Tab设为数组最后一项,并判断此刻辅助菜单的打开状态。
    • 在点击Tab页标签上的“×”时,会调用closeTab函数,遍历navLinks数组找到相应的code,判断要关闭的Tab是否位于第一个,如果不是,将Tab索引置为关闭页的前一个,然后通过前端路由跳转到那个组件;如果是第一个,则先判断当前是否只剩唯一的一个Tab,如果不是,则将Tab索引置为关闭页的后一个,跳转。如果只剩一个,则跳转到一个固定的前端路由地址。
    • 传的参数中referenceisLast是要穿给自定义路由策略组件的,自定义路由策略组件在截获路由参数时,将reference键所对应的存储删掉,然后判断isLast的值,如果为真,则将缓存中的所有内容都删除。
  2. 左侧导航部分,界面代码如下:

    <nav class="sidebar-nav">
      <ul class="nav">
        <ng-template ngFor let-navitem [ngForOf]="navObj">
          <li *ngIf="isDivider(navitem)" class="nav-divider"></li>
          <ng-template [ngIf]="isTitle(navitem)">
            <app-sidebar-nav-title [title]='navitem'></app-sidebar-nav-title>
          </ng-template>
          <ng-template [ngIf]="!isDivider(navitem)&&!isTitle(navitem)">
            <app-sidebar-nav-item [item]='navitem'></app-sidebar-nav-item>
          </ng-template>
        </ng-template>
      </ul>
    </nav>
    

    逻辑代码如下:

    const srcArray = navigation;
    const distArray = [];
    const privilegeCodeObj = this.ls.getObject('privilegeCode');
    for (let priviCodeLvlCount_1 = 0; priviCodeLvlCount_1 < srcArray.length; priviCodeLvlCount_1++) {
      if (srcArray[priviCodeLvlCount_1].privilegeCode === 'static') {
        distArray.push(srcArray[priviCodeLvlCount_1]);
      } else {
        if (privilegeCodeObj[srcArray[priviCodeLvlCount_1].privilegeCode]) {
          distArray.push(srcArray[priviCodeLvlCount_1]);
          const srcChildrenArray = srcArray[priviCodeLvlCount_1]['children'];
          const distChildrenArray = [];
          for (let priviCodeLvlCount_2 = 0; priviCodeLvlCount_2 < srcChildrenArray.length; priviCodeLvlCount_2++) {
            if (privilegeCodeObj[srcChildrenArray[priviCodeLvlCount_2].privilegeCode]) {
              distChildrenArray.push(srcChildrenArray[priviCodeLvlCount_2]);
              this.full.params[srcChildrenArray[priviCodeLvlCount_2].moduleName] = srcChildrenArray[priviCodeLvlCount_2].moduleParams;
            }
          }
          distArray[distArray.length - 1]['children'] = distChildrenArray;
        }
      }
    }
    this.navObj = distArray;
    

    代码说明:
    全导航列表内容存在于一个ts文件中,ts文件结构如下:

    export const navigation = [
      {
        title: true,
        name: '吉大医疗云平台',
        privilegeCode: 'static',
        wrapper: {
          element: 'span',
          attributes: {}
        },
        class: 'text-center'
      },
      {
        name: '医院管理系统',
        privilegeCode: 'static',
        url: 'tasks/manage-board',
        icon: 'icon-speedometer',
        moduleName: 'manageboardModule',
        moduleParams: {
          canOpenWorkshop: false,
          isOpenedWorkshop: false
        }
      },
      {
        title: true,
        name: '功能列表',
        privilegeCode: 'static',
        wrapper: {
          element: 'span',
          attributes: {}
        },
        class: 'text-center'
      },
      {
        name: '系统管理',
        privilegeCode: 'AAAB0000',
        url: 'tasks/(system-manage/user-manage//sub:user-workshop)'
        icon: 'icon-note',
        children: [
          {
            name: '用户管理',
            privilegeCode: 'AAABAA00',
            url: 'tasks/system-manage/user-manage',
            icon: 'icon-note',
            moduleName: 'userModule',
            moduleParams: {
              workTabSelected: 0,
              canClickModifyTab: false,
              isModifyTabChanged: false, // 工作区是否有修改
              modifyParams: {},
              primaryTableData: [],
              primaryTableTotalRows: 0,
              isOpenedWorkshop: true,
              indexSelected: 0
            }
          }]
      }
    ];
    

    因为导航栏是在用户登录后加载的,而在用户登录时已将用户权限代码列表存于LocalStorage中,只需在此处从LocalStorage中读出,并根据用户权限列表内容生成用于生成导航栏的数组。此处LocalStorage中的权限内容的数据结构是一个元素形为{"privilegeCode":true/false}的JSON数组,此处没有选择使用Set这种数据类型,是因为LocalStorage的存储需要序列化,而Set这种数据结构在序列化与反序列化时的具体操作有难度。需要说明的是,ts结构中除了用于生成导航所包含的信息外,还有用于定义辅助菜单的一些数据结构,虽然跟导航关系不大,但是从数据结构上来讲放在这里管理是比较合理的,可以同导航数据一并载入内存,具体功能这里先不讲。

  3. 辅助菜单界面部分:

    <aside class="aside-menu">
      <router-outlet name="sub"></router-outlet>
    </aside>
    
  4. 全局路由定义:

    export const routes: Routes = [
      {
        path: '',
        redirectTo: '/login',
        pathMatch: 'full',
      },
      {
        path: 'tasks',
        component: FullLayoutComponent,
        children: [
          {
            path: 'manage-board',
            component: ManageboardComponent
          },
          {
            path: 'user-workshop',
            outlet: 'sub',
            component: UserWorkshopComponent
          },
          {
            path: 'system-manage',
            loadChildren: './views/system-manage/system-manage.module#SystemManageModule'
          }
        ]
      },
      {
        path: 'login',
        component: SimpleLayoutComponent,
        children: [
          {
            path: '',
            loadChildren: './views/login/login.module#LoginModule',
          }
        ]
      }
    ];
    

    user-workshop路由定义为出口在sub的一个辅助路由,此处仅为声明,具体打开途径已内建到主界面路由中。另外,案例来讲,应该把辅助路由写到一个模块或者写到所属的主界面路由的模块中,但是在实现过程中会有技术问题,比如在试图将辅助路由写到一个组件中时,并不能支持带命名的outlet。另外值得一提的是,如果在路由中想要加入主路由与辅助路由,则主路由前必须不能是blank,并且既然用到了主路由与辅助路由,那个在跳转的时候就一定要将主路由与辅助路由写全(如果没有业务中没有辅助路由则写一个统一的空白辅助路由),不然会报各种奇葩错,个人感觉Angular在路由模块的处理上不是很完美,Angular的Github上也答复得也不让人满意。

  5. 主路由与辅助路由组件之间数据传递:
    采取的策略是将需要传的数据规范化,写到主布局文件中,通过主路由与辅助路由组件到可以访问到主布局文件的特点,实现数据的传递。值得一提的是,由于主路由组件与辅助路由组件都可以利用到Angular的双向绑定的特点,主布局文件中的变量应该都是位于主路由组件与辅助路由组件检测的scope中,所以该特性使得我们业务中的一些逻辑不需要自己再另写代码。主布局文件中应定义的数据结构在前边提到过,也是从本地读入,具体结果如下:

    moduleName: 'userModule',
    moduleParams: {
      workTabSelected: 0,
      canClickModifyTab: false,
      isModifyTabChanged: false, // 工作区是否有修改
      modifyParams: {},
      primaryTableData: [],
      primaryTableTotalRows: 0,
      isOpenedWorkshop: true,
      indexSelected: 0
    }
    

moduleName定义的是这个总的JSON对象中模块所对应的键名,moduleParams是具体存数据的结构。下面说明一下每个存储结构具体的含义:

  • workTabSelected:右侧也是一个Tab页的结构,可能包含增加、修改和帮助等等,这个字段标识了当前所对应的Tab页。
  • canClickModifyTab:这个字段根据业务需要,决定修改Tab是否可以点击。
  • isModifyTabChanged:记录了修改Tab中的数据是否已经修改过。
  • modifyParams:主从路由直接具体业务数据交换处。
  • primaryTableData:主路由中的数据表数据存在这里,主要完成的事情就是,如果在辅助路由中完成了数据变更,在辅助路由中操作这个数据域,可以利用Angular的双向绑定特性,完成主路由中界面的自动更新,而无需手动去做。
  • primaryTableTotalRows:记录了主路由数据表的数据总行数。这个字段跟分页有关,关于分页的实现,会具体写一篇来介绍。
  • isOpenedWorkshop:记录了辅助路由所对应的区域界面是否是打开状态。
  • indexSelected:这个字段记录了我们修改时,修改的那条记录在主界面中所对应的数组的索引,通过这个数字,可以直接操作primaryTableData的具体索引处的数据。
  1. 主路由某具体组件的业务逻辑代码:
    核心部分在于主数据表中对checkbox的点击行为的处理,处理函数如下:

    onCheckBoxClick(index, event) {
      if (this.full.params['userModule'].workTabSelected === 2) {
        if (this.full.params['userModule'].isModifyTabChanged) {
          this.showToastInfo(0);
          event.preventDefault();
        } else {
          if (this.checkBoxSelected[index]) {
            delete this.checkBoxSelected[index];
            event.target.parentElement.parentElement.setAttribute('style', 'background-color:');
          } else {
            const count = Object.keys(this.checkBoxSelected).toString();
            this.checkBoxSelected[count].parentElement.parentElement.setAttribute('style', 'background-color:');
            delete this.checkBoxSelected[count];
            this.checkBoxSelected[index] = event.target;
            event.target.parentElement.parentElement.setAttribute('style', 'background-color: #e5f1fb');
          }
          this.full.params['userModule'].workTabSelected = Object.keys(this.checkBoxSelected).length === 1 ? 2 : 1;
        }
      } else {
        if (this.checkBoxSelected[index]) {
          delete this.checkBoxSelected[index];
          event.target.parentElement.parentElement.setAttribute('style', 'background-color:');
        } else {
          this.checkBoxSelected[index] = event.target;
          event.target.parentElement.parentElement.setAttribute('style', 'background-color: #e5f1fb');
        }
      }
      const checkBoxSelectedLength = Object.keys(this.checkBoxSelected).length;
      this.canDelFlag = checkBoxSelectedLength > 0;
      this.full.params['userModule'].canClickModifyTab = checkBoxSelectedLength === 1;
      if (checkBoxSelectedLength === 1) {
        const count = Object.keys(this.checkBoxSelected)[0];
        this.full.params['userModule'].modifyParams = {
          staffId: this.full.params['userModule'].primaryTableData[count].staffId,
          userName: this.full.params['userModule'].primaryTableData[count].userName,
          idNum: this.full.params['userModule'].primaryTableData[count].idNum,
          phoneNum: this.full.params['userModule'].primaryTableData[count].phoneNum,
          staffType: this.full.params['userModule'].primaryTableData[count].staffType,
          staffTitle: this.full.params['userModule'].primaryTableData[count].staffTitle,
          staffPosition: this.full.params['userModule'].primaryTableData[count].staffPosition,
          staffRank: this.full.params['userModule'].primaryTableData[count].staffRank,
          staffDept: this.full.params['userModule'].primaryTableData[count].staffDept,
          userAddress: this.full.params['userModule'].primaryTableData[count].userAddress,
        };
        this.full.params['userModule'].indexSelected = +count;
      }
    }
    

    checkBoxSelected是一个JSON数组,键是数据表中数组的索引,值存储的是相应的界面元素,这么做是为了能够直观地操作界面元素的背景。其他部分的代码很清楚,就不解释了。

  2. 辅助路由界面代码部分:
    因为这个部分对Angular表单的运用有一点难度,所以还是详细说明一下。

    <tab heading="修改" (select)="full.params.userModule.workTabSelected=2" [disabled]="!full.params.userModule.canClickModifyTab" [active]="full.params.userModule.workTabSelected===2">
      <div class="animated fadeIn" *ngIf="this.full.params.userModule.workTabSelected===2&&initModifyForm()" style="padding:28px 5px">
        <form [formGroup]="formModelModify">
          <div class="input-group mb-1">
            <div style="text-align:center; width:100%">
              <button type="button" class="btn btn-success1 px-3" style="margin-right:10%" [disabled]="formModelModify.invalid" (click)="modifyUser()">修改</button>
              <button type="button" class="btn btn-primary px-3" (click)="formModelModify.reset(full.params.userModule.modifyParams)">重置</button>
            </div>
          </div>
          <div class="input-group mb-2" style="margin-top:28px">
            <span class="input-group-addonnew">姓名</span>
            <input #usernameModify type="text" class="form-control col-md-9" formControlName="userName" required />
          </div>
          <label class="input-group mb-2" style="margin-left:5%" [hidden]="formModelModify.get('userName').valid||formModelModify.get('userName').pristine">姓名应为2~10位的中英文</label>
          <div class="input-group mb-2">
            <span class="input-group-addonnew">工号</span>
            <input #staffIdModify type="text" class="form-control col-md-9" formControlName="staffId" readonly />
          </div>
          <label class="input-group mb-2" style="margin-left:5%" [hidden]="formModelModify.get('staffId').valid||formModelModify.get('staffId').pristine">工号为数字形式</label>
          <div class="input-group mb-2">
            <span class="input-group-addonnew">电话号码</span>
            <input #mobileModify type="text" class="form-control col-md-9" formControlName="phoneNum" [value]=full.params.userModule.modifyParams.phoneNum required />
          </div>
          <label class="input-group mb-2" style="margin-left:5%" [hidden]="formModelModify.get('phoneNum').valid||formModelModify.get('phoneNum').pristine">手机号不正确,请输入正确号码</label>
          <div class="input-group mb-2">
            <span class="input-group-addonnew">人事科室</span>
            <input #departmentModify list="staffDepartmentModify" class="form-control col-md-9" formControlName="staffDept" required />
            <datalist id="staffDepartmentModify">
              <option class="form-control" *ngFor="let staffDepartment of this.categoryResponse.staffDeptList" [value]="staffDepartment.deptname">{{staffDepartment.deptpycode}}</option>
            </datalist>
          </div>
        </form>
      </div>
    </tab>
    

    界面代码相对容易理解,果然在于以下业务逻辑处理部分:
    在构造器中:

    this.formControlsConfig = {
      staffId: ['', numberValidator],
      userName: ['', truenameValidator],
      phoneNum: ['', mobileValidator],
      staffDept: [''],
    };
    this.formModelModify = fb.group(this.formControlsConfig);
    

    因为真实业务中有两套表单验证器,所以把表单配置信息单独写出来,然后让两个表单控制器分别读入。因为staffDept的验证器是通过网络请求过来的数组动态加载的,所以在这里初始化时候,先配置固定的validator,比如姓名、工号和身份证等,然后在网络请求回来以后,再动态将这个验证器加上,代码如下:

    this.formModelAdd.get('staffRank').setValidators(listValidator(this.staffRankList));
    

    另外,关于验证器部分,跟分页一样后面将单独写一篇来介绍。

    public modifyUser() {
      let respData = '';
      const loginUrl = '/user-manage/change-user-info';
      const params = {};
      Object.keys(this.formModelModify.value).forEach((item) => {
        params[item] = this.formModelModify.get(item).value;
      });
      this.submitService.postSubmitWithRelativeUrl(loginUrl, JSON.stringify(params))
        .then(
          responseData => respData = responseData,
          error => this.errorMessage = <any>error)
        .then(() => {
          const responseData = JSON.parse(respData).responseCode;
          if (responseData === '100') {
            alert('您尚未登录或会话过期,请重新登录!');
            this.router.navigate(['']).then();
          } else if (responseData === '1') {  // 修改成功
            this.toasterService.pop('success', '后台管理', '修改成功!');
            Object.keys(this.formModelModify.value).forEach((item) => {
              this.full.params['userModule'].modifyParams[item] = this.formModelModify.get(item).value;
            });
            const currentYear = new Date().getFullYear();
            const idNum = this.formModelModify.get('idNum').value;
            Object.keys(this.formModelModify.value).forEach((item) => {
              this.full.params['userModule'].primaryTableData[this.full.params['userModule'].indexSelected][item] = this.formModelModify.get(item).value;
            });
            this.full.params['userModule'].primaryTableData[this.full.params['userModule'].indexSelected][ 'age'] = currentYear - parseInt(idNum.substring(6, idNum.length === 18 ? 10 : 8), 0);
            this.full.params['userModule'].primaryTableData[this.full.params['userModule'].indexSelected]['userGender'] = parseInt(idNum.substr(idNum.length === 18 ? 16 : 14, 1), 0) % 2;
            this.formModelModify.reset(this.full.params['userModule'].modifyParams);
            this.full.params['userModule'].isModifyTabChanged = false;
          } else {
            this.toasterService.pop('info', '后台管理', '修改失败!');
          }
        });
    }
    public initModifyForm() {
      if (this.localIndex !== this.full.params['userModule'].indexSelected) {
        this.formModelModify.reset(this.full.params['userModule'].modifyParams);
        this.localIndex = this.full.params['userModule'].indexSelected;
      }
      this.full.params['userModule'].isModifyTabChanged = !this.formModelModify.pristine;
      return true;
    }
    

    先说initModifyForm函数,以为辅助路由中的表单数据是通过this.formModelModify.reset()函数初始化的,所以没有办法通过数据的双向绑定来让左边主路由的动作触发右边的动态加载,所以利用了Angular的监视scope域,把处理表单初始化的函数写在右边组件的*ngIf里,不断监视右边往主界面组件中传的值,然后再处理这个业务。
    再说modifyUser函数,我们通过遍历表单控制器里的所有控制域来减少生成网络请求参数的生成过程,然后通过直接设置主界面组件中相应的需要修改的数组索引处的值来直接触发主路由组件的数据表更新,从而省去了手动操作主路由数据表或是其重加载。

  3. 最后讲如何通过自定义路由策略来自由控制通过路由方式加载的组件的销毁逻辑:
    首先app.module中的providers数组中要说明:

    { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
    

    然后再看具体的CustomRouteReuseStrategy这个类:

    export class CustomRouteReuseStrategy implements RouteReuseStrategy {
      _cacheRouters: { [key: string]: any } = {};
      private storeFlag: boolean;
      private clearFlag: boolean;
      constructor() {
        this.storeFlag = true;
        this.clearFlag = false;
      }
      shouldDetach(route: ActivatedRouteSnapshot): boolean {
        // 对所有路由允许复用
        return true;
      }
      store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        // 按path作为key存储路由快照&组件当前实例对象
        // path等同RouterModule.forRoot中的配置
        if (this.clearFlag) {
          delete this._cacheRouters;
          this._cacheRouters = {};
          this.clearFlag = false;
        } else {
          if (this.storeFlag) {
            this._cacheRouters[this.interceptString(route['_routerState']['url'])] = {
              snapshot: route,
              handle: handle
            };
          } else {
            this.storeFlag = true;
          }
        }
      }
      shouldAttach(route: ActivatedRouteSnapshot): boolean {
        // 在缓存中有的都认为允许还原路由
        return !!route.routeConfig && !!this._cacheRouters[this.interceptString(route['_routerState']['url'])] && !!this._cacheRouters[this.interceptString(route['_routerState']['url'])].handle;
      }
      retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        // 从缓存中获取快照,若无则返回null
        if (!route.routeConfig || !this._cacheRouters[this.interceptString(route['_routerState']['url'])] || !this._cacheRouters[this.interceptString(route['_routerState']['url'])].handle) {
          return null;
        }
        // 当前缓存对象里的handle与当前请求的路由所对应的handle不同,以至于angular路由内部策略通不过,会抛出错误,终止当前逻辑。
        return this._cacheRouters[this.interceptString(route['_routerState']['url'])].handle;
      }
      shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        // 同一路由时复用路由
        if (curr.queryParams['paramsExtras']) {
          const paramsExtras = JSON.parse(curr.queryParams['paramsExtras']);
          if (paramsExtras['reference'] !== '') {
            this.storeFlag = false;
            delete this._cacheRouters[paramsExtras['reference']];
          }
          this.clearFlag = paramsExtras['isLast'] === 'true';
        }
        return future.routeConfig === curr.routeConfig;
      }
      private interceptString(route) {
        const index = route.indexOf('?');
        return index === -1 ? route : route.substring(0, index);
      }
    }
    

    其中,判断paramsExtras['reference'] !== ''这一句,是因为我们在做每一次路由跳转的时候都会传一个paramsExtras值,然后我们定义,当Tab页关闭的时候,会把要关闭的Tab页所应的路由名带上,在路由策略类中截获到路由跳转传递的参数时,如果其中带了reference,也就是我们正在关闭的路由名,就将缓存中相应的信息给删掉。关于怎样定义存储逻辑,我们测试到shouldReuseRoute接口会在store接口之前调用,所以在shouldReuseRoute中写好判断逻辑,然后执行到store时再判断存储与否。另外,如果传递过来的参数中的clearFlag为真,则我们确定用户执行的操作状态是所以Tab页均已关闭,所以我们将此处缓存的所有信息全部清空。

感想:

  1. 关于路由部分,我想我可能试了好多语法,通常情况下会报各种我们作为普通开发者看不懂的错,总之按照我上面的写法应该没问题。

参考资料:

  1. 路由:
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 一.课程简介 (注意:这里的AngularJS指的是2.0以下的版本) AngularJS的优点: 模板功能强大丰...
    壹点微尘阅读 858评论 0 0
  • 这是我第96篇原创日记,我是日记星球226号星宝宝。 9月10日 深圳晴 今天是生命密码最后一天的课程。...
    沐阳花开阅读 825评论 0 4
  • 北方有南音阅读 174评论 4 2