angular13 + ng-zorro通过路由重用实现Tab页签

最近正在使用Angular开发Ai-Admin快速开发平台,UI框架选择了Ant Design的ng-zorro。

在UI设计过程中,我们想要实现动态Tab标签页,即点击菜单可以添加一个新的标签页(或打开已打开的),同时满足路由、菜单、Tab页签三者联动。通过调研,最后决定使用路由懒加载的方式实现。

在这里插入图片描述

路由重用
在基于Angular的SPA应用中,各个页面之间的切换是通过路由导航控制的。路由对组件的操作是无状态的,即路由退出时组件状态也一并删除。换句话说,当用户离开一个页面时,之前在该页面的所有操作(例如各种输入框输入的信息)也会被销毁,用户再次返回该页面时,看到的将是一个全新的页面。如果想要保留用户的操作,则需要用到Angular的路由重用。

我们可以这样理解路由重用,路由跳转时记录路由当前的快照并将快照保存起来,重新进入该路由时再取出快照。

Angular如何实现路由重用?

一、定义路由重用策略
RouteReuseStrategy提供了自定义路由重用的方法。

图片

这个接口共定义了5种方法:

shouldDetach
路由离开时是否需要保存页面,也是自定义路由重用策略最重要的一个方法。返回true时,路由离开时保存页面信息,当路由再次激活时,会直接显示保存的页面;返回false时,路由离开时直接销毁组件。

store
如果shouldDetach返回true,调用该方法保存页面。

shouldAttach
路由进入时是否有页面可以重用。 true:重用页面,false:生成新的页面。

retrieve
路由激活时获取保存的页面,如果返回null,则生成新页面

shouldReuseRoute
决定跳转后是否可以使用跳转前的路由页面,即跳转前跳转后使用相同的页面

自定义路由重用策略

import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from ‘@angular/router’;export class AiRouteReuseStrategy implements RouteReuseStrategy {
/**
* 用于保存路由快照
**/
public static routeSnapshots: { [key: string]: DetachedRouteHandle } = {};

/** 
 * 允许所有路由重用
 * 如果你有路由不想被重用,可以在这个方法中加业务逻辑判断 
 **/
shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return true;
}

/** 
 * 以url为key保存路由,key也可以使用其他属性,能确保唯一即可
 **/
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    const url = this.getFullRouteUrl(route);
    AiRouteReuseStrategy.routeSnapshots[url] = handle;
}

 /** 
  * 缓存中存在则允许还原路由
 **/
shouldAttach(route: ActivatedRouteSnapshot): boolean {
    const url = this.getFullRouteUrl(route);
    return !!AiRouteReuseStrategy.routeSnapshots[url];
}

/** 
 * 从缓存中获取快照,没有返回null
**/
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    const url = this.getFullRouteUrl(route);
    return route.routeConfig ? AiRouteReuseStrategy.routeSnapshots[url] : null;
}

/** 
 * 进入路由触发,判断是否同一路由 
**/
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig &&
        JSON.stringify(future.params) === JSON.stringify(curr.params);
}

private getFullRouteUrl(route: ActivatedRouteSnapshot): string {
    return this.getFullRouteUrlPaths(route).filter(Boolean).join('/').replace(/\//g, '_');
}

private getFullRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
    const paths = this.getRouteUrlPaths(route);
    return route.parent
        ? [...this.getFullRouteUrlPaths(route.parent), ...paths]
        : paths;
}

private getRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
    return route.url.map(urlSegment => urlSegment.path);
}}

二、路由重用注入到app.module中

@NgModule({
declarations: [
AppComponent,
// …
],
imports: [
// …
// 导入路由模块
AppRoutingModule,
// …
],
providers: [
// …
// 注册路由重用服务提供商
{provide: RouteReuseStrategy, useClass: SimpleReuseStrategy},
// …
],
bootstrap: [AppComponent]})export class AppModule {}
结合ng-zorro实现多tab页签
一、菜单栏即Tab页签组件实现
可参考ng-zorro官网

<ul nz-menu nzMode="inline" [nzInlineCollapsed]="isCollapsed">
  <ng-container *ngTemplateOutlet="menuTpl; context: { $implicit: menus }"></ng-container>
  <ng-template #menuTpl let-menus>
    <ng-container *ngFor="let menu of menus">
      <li
        *ngIf="!menu.children"
        nz-menu-item
        nzMatchRouter
        [nzPaddingLeft]="menu.level * 24"
      >
        <i nz-icon [nzType]="menu.icon" *ngIf="menu.icon"></i>
        <a class="menu-btn" [routerLink]="['/', menu.path]">{{ menu.title }}</a>
      </li>
      <li
        *ngIf="menu.children"
        nz-submenu
        [nzPaddingLeft]="menu.level * 24"
        [nzTitle]="menu.title"
        [nzIcon]="menu.icon"
        [nzDisabled]="menu.disabled"
      >
        <ul>
          <ng-container *ngTemplateOutlet="menuTpl; context: { $implicit: menu.children }"></ng-container>
        </ul>
      </li>
    </ng-container>
  </ng-template>
</ul>

export class AppComponent {
// 当前打开的Tab页
activatedMenuIndex = -1;
// 存放所有菜单信息
menus: SystemMenu[] = [];
// 存放已打开的Tab页信息
tabs: { path: string, title: string }[] = [];

constructor(
private themeService: ThemeService,
private sysMenuService: SystemMenuService,
private router: Router
) {
// 监听路由事件,只订阅 ActivationEnd 事件
this.router.events.pipe(filter(e => e instanceof ActivationEnd))
.subscribe((e) => {
// 这里不强转VS Code编译通不过,有没有大佬有解决方法
const thisEvt = e;
// 当前激活的路由
const activatedRoutePath = thisEvt.snapshot.routeConfig?.path;
const routeData = thisEvt.snapshot.routeConfig?.data;
let menuTitle = ‘新标签页’;
if(routeData) {
menuTitle = routeData[‘title’];
}

    // 该路由是否已激活,激活过则直接打开
    let isExist = false;
    this.tabs.every((t, i) => {
      if(activatedRoutePath === t.path) {
        this.activatedMenuIndex = i;
        isExist = true;
        return false;
      }
      return true;
    });

    // 指定路由不在tabs中存在(未激活或激活后关闭)
    if(!isExist) {
      this.activeMenu(activatedRoutePath, menuTitle);
    }
    });

}

// 点击菜单激活指定路由并保存tab页签
activeMenu(menuPath: string | undefined, menuTitle: string): void {
if(!menuPath) return;
let menuIndex = -1;
this.tabs.every((t, i) => {
if(menuPath === t.path) {
menuIndex = i;
return false;
}
return true;
});

if(menuIndex === -1) {
  this.tabs.push({path: menuPath, title: menuTitle});
  menuIndex = this.tabs.length - 1;
  this.activatedMenuIndex = menuIndex;
}

}

// 激活路由
activeRoute(path: string): void {
this.router.navigateByUrl(path).finally();
}

// 切换tab,激活对应路由
toggleTab(path: string): void {
this.activeRoute(path);
}

// tab页签关闭,从缓存中删除对应信息
closeTab(path: string): void {
if (1 === this.tabs.length) return;

let selectedIndex = -1;
this.tabs.every((t, i) => {
  if(t.path === path) {
    selectedIndex = i;
    return false;
  }

  return true;
});
this.tabs.splice(selectedIndex, 1);

if(selectedIndex === this.activatedMenuIndex)  {
  let prevIndex = this.activatedMenuIndex - 1;
  this.activatedMenuIndex = prevIndex > 0 ? prevIndex : 0;
  this.activeRoute(this.tabs[this.activatedMenuIndex].path);
}else if (this.activatedMenuIndex > selectedIndex) {
    this.activatedMenuIndex -= 1;
  }

}}
附:路由配置信息

const routes: Routes = [
{path: ‘’, redirectTo: ‘/home’, pathMatch: ‘full’},
{ path: ‘home’, component: HomeComponent, data: {title: ‘首页’} },
{ path: ‘user’, component: UserComponent, data: {title: ‘用户管理’} },
{ path: ‘department’, component: DepartmentComponent, data: {title: ‘部门管理’} }];
菜单是保存在数据库中的,菜单与路由一一对应。下一步我们要实现“动态添加路由”,以更好完善Ai-Admin。

项目地址(全代码下载):
https://github.com/Frank-Z20/ai-admin
https://gitee.com/chou-xf/ai-admin

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340