Skip to content

Commit

Permalink
feat(abc:reuse-tab): support custom cache data (#1609)
Browse files Browse the repository at this point in the history
  • Loading branch information
cipchk authored Jul 16, 2023
1 parent 3cd73f7 commit 11599d9
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 39 deletions.
4 changes: 4 additions & 0 deletions packages/abc/reuse-tab/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,7 @@ Limiting the maximum number of reuse can reduce memory growth. There are several
### Not supported QueryString parameters

Route reuse preserves uses URLs to distinguish whether the same page, and QueryString query parameters will be repeatedly misused, so not supported, and the QueryString part is forced to be ignored.

### Multi-application cache processing

Allows overriding `REUSE_TAB_CACHED_MANAGER` to change the cache storage, for example when using a micro-frontend (similar to [ngx-planet](https://github.com/worktile/ngx-planet)) can rewrite cached data to `window` guaranteed data sharing.
4 changes: 4 additions & 0 deletions packages/abc/reuse-tab/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,7 @@ export class DemoComponent implements OnReuseInit, OnReuseDestroy {
### 不支持 QueryString 查询参数

复用采用URL来区分是否同一个页面,而 QueryString 查询参数很容易产生重复性误用,因此不支持查询参数,且在复用过程中会强制忽略掉 QueryString 部分。

### 多应用缓存处理

允许覆盖 `REUSE_TAB_CACHED_MANAGER` 改变缓存存储,例如在使用微前端(类似[ngx-planet](https://github.com/worktile/ngx-planet))可以重写缓存数据到 `window` 下来实现数据共享。
20 changes: 20 additions & 0 deletions packages/abc/reuse-tab/reuse-tab.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { InjectionToken } from '@angular/core';

import { ReuseTabCached, ReuseTitle } from './reuse-tab.interfaces';

/**
* Storage manager that can change rules by implementing `get`, `set` accessors
*/
export const REUSE_TAB_CACHED_MANAGER = new InjectionToken<ReuseTabCachedManager>('REUSE_TAB_CACHED_MANAGER');

export interface ReuseTabCachedManager {
list: ReuseTabCached[];
title: { [url: string]: ReuseTitle };
closable: { [url: string]: boolean };
}

export class ReuseTabCachedManagerFactory implements ReuseTabCachedManager {
list: ReuseTabCached[] = [];
title: { [url: string]: ReuseTitle } = {};
closable: { [url: string]: boolean } = {};
}
5 changes: 5 additions & 0 deletions packages/abc/reuse-tab/reuse-tab.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs';
import { ReuseTabContextMenuComponent } from './reuse-tab-context-menu.component';
import { ReuseTabContextComponent } from './reuse-tab-context.component';
import { ReuseTabContextDirective } from './reuse-tab-context.directive';
import { REUSE_TAB_CACHED_MANAGER, ReuseTabCachedManagerFactory } from './reuse-tab.cache';
import { ReuseTabComponent } from './reuse-tab.component';
import { ReuseTabLocalStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';

Expand All @@ -28,6 +29,10 @@ const NOEXPORTS = [ReuseTabContextMenuComponent, ReuseTabContextComponent, Reuse
{
provide: REUSE_TAB_STORAGE_STATE,
useFactory: () => new ReuseTabLocalStorageState()
},
{
provide: REUSE_TAB_CACHED_MANAGER,
useFactory: () => new ReuseTabCachedManagerFactory()
}
],
exports: COMPONENTS
Expand Down
5 changes: 5 additions & 0 deletions packages/abc/reuse-tab/reuse-tab.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { filter } from 'rxjs';
import { MenuService } from '@delon/theme';
import { NzSafeAny } from 'ng-zorro-antd/core/types';

import { REUSE_TAB_CACHED_MANAGER, ReuseTabCachedManagerFactory } from './reuse-tab.cache';
import { ReuseItem, ReuseTabMatchMode, ReuseTitle } from './reuse-tab.interfaces';
import { ReuseTabService } from './reuse-tab.service';
import { ReuseTabLocalStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE } from './reuse-tab.state';
Expand Down Expand Up @@ -50,6 +51,10 @@ describe('abc: reuse-tab(service)', () => {
provide: REUSE_TAB_STORAGE_STATE,
useFactory: () => new ReuseTabLocalStorageState()
},
{
provide: REUSE_TAB_CACHED_MANAGER,
useFactory: () => new ReuseTabCachedManagerFactory()
},
{ provide: ActivatedRoute, useValue: { snapshot: { url: [] } } },
{ provide: Router, useFactory: () => new MockRouter() }
].concat(providers)
Expand Down
79 changes: 40 additions & 39 deletions packages/abc/reuse-tab/reuse-tab.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Menu, MenuService } from '@delon/theme';
import { ScrollService } from '@delon/util/browser';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';

import { REUSE_TAB_CACHED_MANAGER, ReuseTabCachedManager } from './reuse-tab.cache';
import {
ReuseComponentRef,
ReuseHookOnReuseInitType,
Expand All @@ -32,9 +33,6 @@ export class ReuseTabService implements OnDestroy {
private _max = 10;
private _keepingScroll = false;
private _cachedChange = new BehaviorSubject<ReuseTabNotify | null>(null);
private _cached: ReuseTabCached[] = [];
private _titleCached: { [url: string]: ReuseTitle } = {};
private _closableCached: { [url: string]: boolean } = {};
private _router$?: Unsubscribable;
private removeUrlBuffer: string | null = null;
private positionBuffer: { [url: string]: [number, number] } = {};
Expand Down Expand Up @@ -75,8 +73,8 @@ export class ReuseTabService implements OnDestroy {
*/
set max(value: number) {
this._max = Math.min(Math.max(value, 2), 100);
for (let i = this._cached.length; i > this._max; i--) {
this._cached.pop();
for (let i = this.cached.list.length; i > this._max; i--) {
this.cached.list.pop();
}
}
set keepingScroll(value: boolean) {
Expand All @@ -89,50 +87,52 @@ export class ReuseTabService implements OnDestroy {
keepingScrollContainer?: Element;
/** 获取已缓存的路由 */
get items(): ReuseTabCached[] {
return this._cached;
return this.cached.list;
}
/** 获取当前缓存的路由总数 */
get count(): number {
return this._cached.length;
return this.cached.list.length;
}
/** 订阅缓存变更通知 */
get change(): Observable<ReuseTabNotify | null> {
return this._cachedChange.asObservable(); // .pipe(filter(w => w !== null));
}
/** 自定义当前标题 */
set title(value: string | ReuseTitle) {
if (this.cached == null) return;

const url = this.curUrl;
if (typeof value === 'string') value = { text: value };
this._titleCached[url] = value;
this.cached.title[url] = value;
this.di('update current tag title: ', value);
this._cachedChange.next({
active: 'title',
url,
title: value,
list: this._cached
list: this.cached.list
});
}
/** 获取指定路径缓存所在位置,`-1` 表示无缓存 */
index(url: string): number {
return this._cached.findIndex(w => w.url === url);
return this.cached.list.findIndex(w => w.url === url);
}
/** 获取指定路径缓存是否存在 */
exists(url: string): boolean {
return this.index(url) !== -1;
}
/** 获取指定路径缓存 */
get(url?: string): ReuseTabCached | null {
return url ? this._cached.find(w => w.url === url) || null : null;
return url ? this.cached.list.find(w => w.url === url) || null : null;
}
private remove(url: string | number, includeNonCloseable: boolean): boolean {
const idx = typeof url === 'string' ? this.index(url) : url;
const item = idx !== -1 ? this._cached[idx] : null;
const item = idx !== -1 ? this.cached.list[idx] : null;
if (!item || (!includeNonCloseable && !item.closable)) return false;

this.destroy(item._handle);

this._cached.splice(idx, 1);
delete this._titleCached[url];
this.cached.list.splice(idx, 1);
delete this.cached.title[url];
return true;
}
/**
Expand All @@ -145,7 +145,7 @@ export class ReuseTabService implements OnDestroy {

this.remove(url, includeNonCloseable);

this._cachedChange.next({ active: 'close', url, list: this._cached });
this._cachedChange.next({ active: 'close', url, list: this.cached.list });

this.di('close tag', url);
return true;
Expand All @@ -163,7 +163,7 @@ export class ReuseTabService implements OnDestroy {

this.removeUrlBuffer = null;

this._cachedChange.next({ active: 'closeRight', url, list: this._cached });
this._cachedChange.next({ active: 'closeRight', url, list: this.cached.list });

this.di('close right tages', url);
return true;
Expand All @@ -174,14 +174,14 @@ export class ReuseTabService implements OnDestroy {
* @param [includeNonCloseable=false] 是否强制包含不可关闭
*/
clear(includeNonCloseable: boolean = false): void {
this._cached.forEach(w => {
this.cached.list.forEach(w => {
if (!includeNonCloseable && w.closable) this.destroy(w._handle);
});
this._cached = this._cached.filter(w => !includeNonCloseable && !w.closable);
this.cached.list = this.cached.list.filter(w => !includeNonCloseable && !w.closable);

this.removeUrlBuffer = null;

this._cachedChange.next({ active: 'clear', list: this._cached });
this._cachedChange.next({ active: 'clear', list: this.cached.list });

this.di('clear all catch');
}
Expand All @@ -204,16 +204,16 @@ export class ReuseTabService implements OnDestroy {
* ```
*/
move(url: string, position: number): void {
const start = this._cached.findIndex(w => w.url === url);
const start = this.cached.list.findIndex(w => w.url === url);
if (start === -1) return;
const data = this._cached.slice();
const data = this.cached.list.slice();
data.splice(position < 0 ? data.length + position : position, 0, data.splice(start, 1)[0]);
this._cached = data;
this.cached.list = data;
this._cachedChange.next({
active: 'move',
url,
position,
list: this._cached
list: this.cached.list
});
}
/**
Expand All @@ -239,8 +239,8 @@ export class ReuseTabService implements OnDestroy {
* @param route 指定路由快照
*/
getTitle(url: string, route?: ActivatedRouteSnapshot): ReuseTitle {
if (this._titleCached[url]) {
return this._titleCached[url];
if (this.cached.title[url]) {
return this.cached.title[url];
}

if (route && route.data && (route.data.titleI18n || route.data.title)) {
Expand All @@ -258,17 +258,17 @@ export class ReuseTabService implements OnDestroy {
* 清除标题缓存
*/
clearTitleCached(): void {
this._titleCached = {};
this.cached.title = {};
}
/** 自定义当前 `closable` 状态 */
set closable(value: boolean) {
const url = this.curUrl;
this._closableCached[url] = value;
this.cached.closable[url] = value;
this.di('update current tag closable: ', value);
this._cachedChange.next({
active: 'closable',
closable: value,
list: this._cached
list: this.cached.list
});
}
/**
Expand All @@ -282,7 +282,7 @@ export class ReuseTabService implements OnDestroy {
* @param route 指定路由快照
*/
getClosable(url: string, route?: ActivatedRouteSnapshot): boolean {
if (typeof this._closableCached[url] !== 'undefined') return this._closableCached[url];
if (typeof this.cached.closable[url] !== 'undefined') return this.cached.closable[url];

if (route && route.data && typeof route.data.reuseClosable === 'boolean') return route.data.reuseClosable;

Expand All @@ -295,7 +295,7 @@ export class ReuseTabService implements OnDestroy {
* 清空 `closable` 缓存
*/
clearClosableCached(): void {
this._closableCached = {};
this.cached.closable = {};
}
getTruthRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
let next = route;
Expand Down Expand Up @@ -370,6 +370,7 @@ export class ReuseTabService implements OnDestroy {
constructor(
private injector: Injector,
private menuService: MenuService,
@Optional() @Inject(REUSE_TAB_CACHED_MANAGER) private cached: ReuseTabCachedManager,
@Optional() @Inject(REUSE_TAB_STORAGE_KEY) private stateKey: string,
@Optional() @Inject(REUSE_TAB_STORAGE_STATE) private stateSrv: ReuseTabStorageState
) {}
Expand All @@ -383,7 +384,7 @@ export class ReuseTabService implements OnDestroy {
private loadState(): void {
if (!this.storageState) return;

this._cached = this.stateSrv.get(this.stateKey).map(v => ({
this.cached.list = this.stateSrv.get(this.stateKey).map(v => ({
title: { text: v.title },
url: v.url,
position: v.position
Expand All @@ -403,7 +404,7 @@ export class ReuseTabService implements OnDestroy {
type: ReuseHookOnReuseInitType = 'init'
): void {
if (typeof comp === 'number') {
const item = this._cached[comp];
const item = this.cached.list[comp];
comp = item._handle?.componentRef;
}
if (comp == null || !comp.instance) {
Expand Down Expand Up @@ -453,18 +454,18 @@ export class ReuseTabService implements OnDestroy {
if (isAdd) {
if (this.count >= this._max) {
// Get the oldest closable location
const closeIdx = this._cached.findIndex(w => w.closable!);
const closeIdx = this.cached.list.findIndex(w => w.closable!);
if (closeIdx !== -1) this.remove(closeIdx, false);
}
this._cached.push(item);
this.cached.list.push(item);
} else {
// Current handler is null when activate routes
// For better reliability, we need to wait for the component to be attached before call _onReuseInit
const cahcedComponentRef = this._cached[idx]._handle?.componentRef;
const cahcedComponentRef = this.cached.list[idx]._handle?.componentRef;
if (_handle == null && cahcedComponentRef != null) {
timer(100).subscribe(() => this.runHook('_onReuseInit', cahcedComponentRef));
}
this._cached[idx] = item;
this.cached.list[idx] = item;
}
this.removeUrlBuffer = null;

Expand All @@ -475,7 +476,7 @@ export class ReuseTabService implements OnDestroy {
}

if (!isAdd) {
this._cachedChange.next({ active: 'override', item, list: this._cached });
this._cachedChange.next({ active: 'override', item, list: this.cached.list });
}
}

Expand All @@ -489,7 +490,7 @@ export class ReuseTabService implements OnDestroy {
const ret = !!(data && data._handle);
this.di('#shouldAttach', ret, url);
if (!ret) {
this._cachedChange.next({ active: 'add', url, list: this._cached });
this._cachedChange.next({ active: 'add', url, list: this.cached.list });
}
return ret;
}
Expand Down Expand Up @@ -585,7 +586,7 @@ export class ReuseTabService implements OnDestroy {
ngOnDestroy(): void {
const { _cachedChange, _router$ } = this;
this.clear();
this._cached = [];
this.cached.list = [];
_cachedChange.complete();

if (_router$) {
Expand Down

0 comments on commit 11599d9

Please sign in to comment.