最近正在使用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 举报,一经查实,本站将立刻删除。