// 创建重用策略
import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from '@angular/router';
/**
* 路由重用策略
*/
export class SimpleReuseStrategy implements RouteReuseStrategy {
// 保存路由快照
// [key:string] 键为字符串类型
// DetachedRouteHandle 值为路由处理器
public static snapshots: { [key: string]: DetachedRouteHandle } = {};
/**
* 从缓存中获取快照
* @param {ActivatedRouteSnapshot} route
* @return {DetachedRouteHandle | null}
*/
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return route.routeConfig ? SimpleReuseStrategy.snapshots[route.routeConfig.path] : null;
}
/**
* 是否允许还原
* @param {ActivatedRouteSnapshot} route
* @return {boolean} true-允许还原
*/
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return route.routeConfig && SimpleReuseStrategy.snapshots[route.routeConfig.path];
}
/**
* 确定是否应该分离此路由(及其子树)以便以后重用
* @param {ActivatedRouteSnapshot} route
* @return {boolean}
*/
shouldDetach(route: ActivatedRouteSnapshot): boolean {
// useCache 为自定义数据
return route.routeConfig && route.routeConfig.data && route.routeConfig.data.useCache;
}
/**
* 进入路由触发, 判断是否为同一路由
* @param {ActivatedRouteSnapshot} future
* @param {ActivatedRouteSnapshot} curr
* @return {boolean}
*/
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
// future - 未来的(下一个)路由快照
return future.routeConfig === curr.routeConfig;
}
/**
* 保存路由
* @param {ActivatedRouteSnapshot} route
* @param {DetachedRouteHandle | null} handle
*/
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
// 通过 Route.path 映射路由快照, 一定要确保它的唯一性
// 也可以通过 route.routeConfig.data.uid 或其他可以确定唯一性的数据作为映射key
// 作者这里能够确保 path 的唯一性
SimpleReuseStrategy.snapshots[route.routeConfig.path] = handle;
}
}
在app.module.ts
中注册路由重影提供商
@NgModule({
declarations: [
AppComponent,
// ...
],
imports: [
// ...
// 导入路由模块
AppRoutingModule,
// ...
],
providers: [
// ...
// 注册路由重用服务提供商
{provide: RouteReuseStrategy, useClass: SimpleReuseStrategy},
// ...
],
bootstrap: [AppComponent]
})
export class AppModule {
}
在路由定义中需要用到静态数据定义 Route.data
const routes: Routes = [
{
path: 'm', component: DashboardComponent, children: <Routes>[
{path: 'gr', component: GameRecordComponent, data: {uid: 101, useCache: true}},
{path: 'mgr/um', component: UserManageComponent, data: {uid: 10401, useCache: true}},
// 玩家信息不做路由重用(不缓存页面数据), useCache=false
{path: 'pi', component: PlayerInfoComponent, data: {uid: 105, useCache: false}},
// 这里不必指定默认默认路由,
// 如果做rbac控制, 很可能当前用户没有那个权限
// {path: '', redirectTo: 'gr', pathMatch: 'full'}
]
},
{path: '', redirectTo: '/m', pathMatch: 'full'}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
在目标组件如: dashboard-component.ts
添加路由监听
<nz-layout>
<nz-sider class="bg-eee" [(nzCollapsed)]="isCollapsed" nzTheme='light'>
<ul nz-menu [nzMode]="'inline'" nzTheme='light' [nzInlineCollapsed]="isCollapsed">
<ng-container *ngFor="let menu of menus">
<ng-container *ngIf="!menu.child">
<li nz-menu-item nz-tooltip nzPlacement="right"
[nzTitle]="isCollapsed ? menu.title : ''" (click)="actionTab(menu)">
<span title>
<i nz-icon type="tag">i>
<span>{{menu.title}}span>
<a *ngIf="menu.newPage" target="_blank" (click)="waitForAction(menu)"
[routerLink]="menu.path" class="float-right">
<i nz-icon nzType="select" nzTheme="outline" nzTitle="新窗口打开">i>
a>
span>
li>
ng-container>
<ng-container *ngIf="menu.child">
<li nz-submenu>
<span title><i nz-icon type="appstore">i><span>{{menu.title}}span>span>
<ul>
<li *ngFor="let cm of menu.child" nz-menu-item [routerLink]="cm.path"
(click)="actionTab(cm)">
<i nz-icon type="tag">i> {{cm.title}}
li>
ul>
li>
ng-container>
ng-container>
ul>
nz-sider>
<nz-content class="main-right bg-white flex-v">
<nz-tabset [nzType]="'card'" [(nzSelectedIndex)]="selectedIndex">
<nz-tab *ngFor="let tab of tabs;let i=index;" [nzTitle]="titleTemplate"
(nzSelect)="tabSelect(tab, i)">
<ng-template #titleTemplate>
<div>
{{ tab.name }}<i nz-icon type="close" class="ant-tabs-close-x" (click)="closeTab(tab)">i>
div>
ng-template>
nz-tab>
nz-tabset>
<div class="flex-1 overflow-auto content-box">
<router-outlet class="main-content">router-outlet>
div>
nz-content>
nz-layout>
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
// 定义左侧菜单以及相关路由链接
menus: Menus = [
{path: '/m/gr', title: '游戏记录', uid: 101},
// ...
// newPage指示是否需要添加 [新窗口打开] 功能
{path: '/m/pi', title: '玩家信息', uid: 105, newPage: true},
{
title: '权限配置', uid: 104, child: [
{path: '/m/mgr/um', title: '用户列表', uid: 10401},
// ...
]
}
];
// uid映射
// 用于快速查找, 而不用每次都去 forEach(menus)
menusMap: { [uid: string]: Menu } = {};
// ...
constructor(
// 路由器控制器
private router: Router,
// 页面标题服务
private titleService: Title,
// 当前组件相关的路由器
private activatedRoute: ActivatedRoute
) {
// 监听路由事件
// 只订阅 ActivationEnd 事件
this.router.events.pipe(filter(e => e instanceof ActivationEnd))
.subscribe((e: ActivationEnd) => {
const snapshot = e.snapshot;
const isSkip = !(snapshot['_routerState'].url
&& snapshot.routeConfig.data
&& snapshot.routeConfig.data.useCache);
if (isSkip) return;
// 获取路由配置中自定义的唯一标记
// uid: Unique Identity
const uid = snapshot.routeConfig.data.uid;
// 从当前激活的已存在的 tab 缓存中直接激活
let exist = false;
eachA(this.tabs, (tab, i) => {
if (uid === tab.uid) {
// mvvm 模式直接激活指定 tab
this.selectedIndex = i;
exist = true;
return false;
}
});
// 指定路由没有在 tab 缓存中找到(或已经从ui中关闭/删除)
// 首次进入没找到 tab, 从menu中获取
if (!exist)
this.actionTab(this.menusMap[uid]);
});
}
ngOnInit(){
this.initMenusMap(this.menus);
}
// 初始化uid菜单映射
initMenusMap(ms: Array<Menu>) {
if (Array.isArray(ms))
eachA(ms, m => {
this.menusMap[m.uid] = m;
// 递归子孙菜单
this.initMenusMap(m.child);
});
}
// 点击菜单激活指定路由并保存tab页签
// 这里命名不准确, 应修改为 actionMenu
actionTab(menu) {
if (!menu) return;
// 如果已经存在该tab页签, 直接将它选中
// 如果没有打开就添加该tab页签, 并将它选中
// pushUniqueA 为自定义方法, 后面有说明
this.selectedIndex = pushUniqueA(
this.tabs,
{name: menu.title, path: menu.path, uid: menu.uid},
'uid');
// 将当前页面标题切换为菜单名字
let tab = this.tabs[this.selectedIndex];
this.titleService.setTitle(tab.name);
// 激活这个tab对应的路由视图
this.activeRoute(tab);
}
// 关闭tab页签
closeTab(tab) {
// 最后一个不允许关闭
if (1 === this.tabs.length) return;
// 删除tab
let optionIndex = indexA(this.tabs, tab, 'uid');
removeA(this.tabs, tab, 'uid');
// 正在关闭激活tab
if (this.selectedIndex === optionIndex) {
// 激活上一个tab
let nextIndex = this.selectedIndex - 1;
this.selectedIndex = nextIndex > 0 ? nextIndex : 0;
// 激活上一个路由
this.activeRoute(this.tabs[nextIndex]);
}
// 正在关闭激活tab的左侧tab
else if (this.selectedIndex > optionIndex) {
// 关闭前面的tab, 索引下标前移一位
this.selectedIndex -= 1;
}
// 正在关闭激活tab的右侧tab
else {
// 关闭后面的tab, 不作任何处理
}
}
// 切换tab选项卡
tabSelect(tab) {
// 激活选项卡对应的路由
this.activeRoute(tab);
}
// 激活tab所关联的路由
activeRoute(tab) {
this.router.navigateByUrl(tab.path).finally();
this.titleService.setTitle(this.tabs[this.selectedIndex].name);
}
// ...
}
一些常用的工具函数
//////////////////////////////////////////// other
/**
* 对象克隆
* @param o
* @return {any}
*/
export function clone(o: any) {
if (isObject(o))
return JSON.parse(JSON.stringify(o));
return o;
}
/**
* 展开对象属性为ognl表达式
* @param {array|object} o 数组或对象
* @param {boolean} skipNullOrUndef=true 跳过值为null或undefined的属性
* @return {?} 将多级属性展开为一级属性后的对象
*/
export function extJson(o: any, skipNullOrUndef = true): { [prop: string]: any } {
skipNullOrUndef = true || (undefined === skipNullOrUndef);
const ret = {};
ext(clone(o), null, ret);
return ret;
function ext(json, prefix, ret) {
// const isArr = Array.isArray(json);
eachO(json, (v, k) => {
if (skipNullOrUndef && (isNullOrUndefined(v) || '' === v))
return true;
// 数组或数字属性名都是用中括号'[]'
let nk;
if (!isNaN(k)) nk = prefix ? (prefix + "[" + k + "]") : k;
else nk = prefix ? (prefix + "." + k) : k;
// 赋值
if (!isObject(v)) ret[nk] = v;
else ext(v, nk, ret);
});
return ret;
}
}
/**
* 增量步骤执行
* @param {number} star 起始值
* @param {number} end 结束边界值
* @param {number} step 步长
* @param {(v) => (boolean | void)} f 步长处理回调函数
*/
export function incrStep(star: number, end: number, step: number, f: (v) => boolean | void) {
while (star <= end) {
if (false === f(star)) break;
star += step;
}
}
//////////////////////////////////////////// object
/**
* 对象属性遍历
* @param {object} o 目标对象
* @param {(v: T, k: (string | any)) => (boolean | any)} f 属性处理函数
*/
export function eachO<T>(o: object, f: (v: T, k: string | any) => boolean | any) {
for (const k in o)
if (false === f(o[k], k))
break;
}
/**
* 清空对象属性
* @param {object} o 目标对象
* @param {(v: any, k: string) => (boolean | void)} f true-跳过当前属性,
* false-跳过后续所有,
* undefined-删除当前属性
*/
export function clearO(o: object, f?: (v: any, k: string) => boolean | void) {
f = !isFunction(f) ? f : () => undefined;
eachO(o, (v, k) => {
let r = f(v, k);
if (true === r) return;
if (false === r) return false;
delete o[k];
});
}
//////////////////////////////////////////// Array
/**
* 数组遍历
* @param {Array} a
* @param {((e: T, i?: number) => boolean) | void | any} f
*/
export function eachA<T>(a: Array<T>, f?: (((e: T, i?: number) => boolean) | void | any)) {
if (!isFunction(f)) f = _ => true;
for (let i = 0, len = a.length; i < len; i++)
if (false === f(a[i], i))
break;
}
/**
* 追加唯一目标值, 如果校验存在则跳过
* @param {Array} a 数组
* @param {T} e 新元素
* @param {string | ((el: T, i: number) => boolean)} c 唯一值属性名或比较器函数(返回true表示存在)
* @return {number} 与e匹配的元素索引
*/
export function pushUniqueA<T>(a: Array<T>, e: T, c?: string | ((el: T, i: number) => boolean)): number {
let foundIndex = indexA(a, e, c);
if (-1 !== foundIndex)
return foundIndex;
return a.push(e) - 1;
}
/**
* 查找索引
* @param {Array} a 数组
* @param {T} e 查找条件
* @param {string | ((el: T, i: number) => boolean)} k 唯一值属性名或比较器函数(返回true表示找到)
* @return {number} 索引, -1表示未找到
*/
export function indexA<T>(a: Array<T>, e: T, k?: string | ((el: T, i: number) => boolean)): number {
let fn: (el: T, i: number) => boolean;
if (!(k instanceof Function)) {
if (isNullOrUndefined(k)) fn = el => el === e;
else if (isString(k)) fn = el => el[k + ''] === e[k + ''];
}
let foundIdx = -1;
eachA(a, (el, i) => {
if (true === fn(el, i)) {
foundIdx = i;
return false;
}
});
return foundIdx;
}
/**
* 查找目标值
* @param {Array} a 数组
* @param {T} e 查找条件
* @param {string | ((el: T, i: number) => boolean)} k 唯一值属性名或比较器函数(返回true表示找到)
* @return {T | null} 查找成功返回目标值, 否则返回null
*/
export function findA<T>(a: Array<T>, e: T, k?: string | ((el: T, i: number) => boolean)): T | null {
const i = indexA(a, e, k);
return -1 !== i ? a[i] : null;
}
/**
* 删除
* @param {Array} a 数组
* @param {T} e 查找条件
* @param {string | ((el: T, i: number) => boolean)} k 唯一值属性名或比较器函数(返回true表示找到)
* @return {T | null} 删除成功返回被删除目标值, 否则返回null
*/
export function removeA<T>(a: Array<T>, e: T, k?: string | ((el: T, i: number) => boolean)): T | null {
const i = indexA(a, e, k);
if (-1 === i) return null;
return a.splice(i, 1)[0];
}
/**
* 合并
* @param {Array} t 目标数组
* @param {Array} s 元素组
*/
export function concatA<T>(t: Array<T>, s: Array<T>) {
if (!isArray(t) || !isArray(s)) throw '无效数组参数';
Array.prototype.push.apply(t, s);
}
//////////////////////////////////////////// NzForm
/**
* angular表单校验
* @param {FormGroup} fm 表单组
* @return {boolean} true-校验通过, false-校验失败
*/
export function validNgForm(fm: FormGroup): boolean {
eachO<FormControl>(fm.controls, c => {
c.markAsDirty();
c.updateValueAndValidity();
});
return fm.valid;
}
由于angular
的前端路由特性导致页面控制从SpringBoot2
的Controller
全面交由Router
控制, 导致服务器无法识别浏览器新窗口的路径. 未解决该问题各种百度/Google都没有找到合适的解决方案, 以下是针对作者实际项目整理的解决思路, 仅供参考:
(click)
事件缓存路由信息【注意】:开启新窗口sessionStorage中的数据会得到新的拷贝
前端关键代码
<a *ngIf="menu.newPage" target="_blank"
(click)="waitForAction(menu)"
[routerLink]="menu.path"
class="float-right">
<i nz-icon nzType="select" nzTheme="outline" nzTitle="新窗口打开">i>
a>
点击事件处理
waitForAction(menu: Menu) {
// 将路由信息缓存在 sessionStorage 中
// 由于是通过 a 标签跳转, 两个页面都在同一域名下因此可以共享sessionStorage数据
this.cached.session.set('menu', menu, true);
// 在作者当前项目中, SpringBoot转发的页面总是在当前函数所在的组件
// 因此该缓存信息使用一次后将不再有效
this.cached.session.lazyDel('menu');
}
[新窗口]从缓存中获取路由信息并跳转
ngOnInit() {
this.userService.checkLogin().subscribe(r => {
if (!r.flag) {
this.notificationService.warning('警告', '用户未登录或登录信息已过期');
this.router.navigateByUrl('/login').finally();
return;
}
this.menuManageService.listBy().subscribe(
handleResult(this.notificationService, ({data}) => {
this.menus = data;
this.initMenusMap(this.menus);
// 激活指定路由
// 该路由信息使用一次即可
// 注意: 由于在新窗口中, 这里的session.del将无法删除其他窗口中的sessionStorage数据
const menu = this.cached.session.del<Menu>('menu');
if (null != menu) this.actionTab(menu);
else this.actionTab(this.menus[0]);
})
);
});
}