import { Injectable, Injector, Optional } from '@angular/core'; import { ActivatedRouteSnapshot, ActivatedRoute, DetachedRouteHandle } from '@angular/router'; import { ReuseTabCached, ReuseTitle, ReuseTabNotify, ReuseTabMatchMode } from './interface'; import { BehaviorSubject } from 'rxjs'; import { NaMenuService } from '@ng-arthur-mobile/core'; import { NaUtils } from '@ng-arthur-mobile/common'; /** * 路由复用类,提供复用所需要一些基本接口 * * @export * @class NaReuseService */ @Injectable({ providedIn: 'root' }) export class NaReuseService { private _debug = true; private _max = 10; private _mode = ReuseTabMatchMode.Menu; private _cachedChange: BehaviorSubject = new BehaviorSubject(null); private _cached: ReuseTabCached[] = []; private _titleCached: { [url: string]: ReuseTitle } = {}; private _closableCached: { [url: string]: boolean } = {}; private removeUrlBuffer: string; private _excludes: RegExp[] = []; /** 允许最多复用多少个页面,取值范围 `2-100` */ 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(); } } /** 设置匹配模式 */ set mode(value: ReuseTabMatchMode) { this._mode = value; } get mode() { return this._mode; } /** 获取当前缓存的路由总数 */ get count() { return this._cached.length; } /** 设置Debug模式 */ set debug(value: boolean) { this._debug = value; } get debug() { return this._debug; } constructor(private injector: Injector, @Optional() private menuService: NaMenuService) { } /** 当前路由地址 */ get curUrl() { return NaUtils.getUrl(this.injector.get(ActivatedRoute).snapshot); } /** 自定义当前标题 */ set title(value: string | ReuseTitle) { const url = this.curUrl; if (typeof value === 'string') { value = { text: value }; } this._titleCached[url] = value; this.log('update current tag title: ', value); this._cachedChange.next({ active: 'title', title: value, list: this._cached, }); } /** * 获取标题,顺序如下: * * 1. 组件内使用 `ReuseTabService.title = 'new title'` 重新指定文本 * 2. 路由配置中 data 属性中包含 titleI18n > title * 3. 菜单数据中 text 属性 * * @param url 指定URL * @param route 指定路由快照 */ getTitle(url: string, route?: ActivatedRouteSnapshot): ReuseTitle { if (this._titleCached[url]) { return this._titleCached[url]; } if (route && route.data && (route.data.titleI18n || route.data.title)) { return { text: route.data.title, i18n: route.data.titleI18n }; } const menu = this.mode !== ReuseTabMatchMode.URL ? this.getMenu(url) : null; return menu ? { text: menu.text, i18n: menu.i18n } : { text: url }; } /** * 根据URL获取菜单详细 * * @private * @param {string} url * @returns * @memberof NaReuseService */ private getMenu(url: string) { const menus = this.menuService ? this.menuService.getPathByUrl(url) : []; if (!menus || menus.length === 0) { return null; } return menus.pop(); } private destroy(handle: any) { if (handle && handle.componentRef && handle.componentRef.destroy) { handle.componentRef.destroy(); } } private log(...args) { if (!this.debug) { return; } console.warn(...args); } /** 获取指定路径缓存所在位置,`-1` 表示无缓存 */ index(url: string): number { return this._cached.findIndex(w => w.url === url); } /** 获取指定路径缓存是否存在 */ exists(url: string): boolean { return this.index(url) !== -1; } 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; if (!item || (!includeNonCloseable && !item.closable)) { return false; } this.destroy(item.handle); this._cached.splice(idx, 1); delete this._titleCached[url]; return true; } /** * 根据URL移除标签 * * @param [includeNonCloseable=false] 是否强制包含不可关闭 */ close(url: string, includeNonCloseable = false) { this.removeUrlBuffer = url; this.remove(url, includeNonCloseable); this._cachedChange.next({ active: 'close', url, list: this._cached }); this.log('close tag', url); return true; } /** 获取指定路径缓存 */ get(url: string): ReuseTabCached { return url ? this._cached.find(w => w.url === url) || null : null; } /** * 获取 `closable` 状态,顺序如下: * * 1. 组件内使用 `ReuseTabService.closable = true` 重新指定 `closable` 状态 * 2. 路由配置中 data 属性中包含 `reuseClosable` * 3. 菜单数据中 `reuseClosable` 属性 * * @param url 指定URL * @param route 指定路由快照 */ getClosable(url: string, route?: ActivatedRouteSnapshot): boolean { if (typeof this._closableCached[url] !== 'undefined') { return this._closableCached[url]; } if (route && route.data && typeof route.data.reuseClosable === 'boolean') { return route.data.reuseClosable; } const menu = this.mode !== ReuseTabMatchMode.URL ? this.getMenu(url) : null; if (menu && typeof menu.reuseClosable === 'boolean') { return menu.reuseClosable; } return true; } private runHook(method: string, url: string, comp: any) { if (comp.instance && typeof comp.instance[method] === 'function') { comp.instance[method](); } } private hasInValidRoute(route: ActivatedRouteSnapshot) { return ( !route.routeConfig || route.routeConfig.loadChildren || route.routeConfig.children ); } /** * 检查快照是否允许被复用 */ private can(route: ActivatedRouteSnapshot): boolean { const url = NaUtils.getUrl(route); if (url === this.removeUrlBuffer) { return false; } if (route.data && typeof route.data.reuse === 'boolean') { return route.data.reuse; } if (this.mode !== ReuseTabMatchMode.URL) { const menu = this.getMenu(url); if (!menu) { return false; } if (this.mode === ReuseTabMatchMode.Menu) { if (menu.reuse === false) { return false; } } else { if (!menu.reuse || menu.reuse !== true) { return false; } } return true; } return this._excludes.findIndex(r => r.test(url)) === -1; } /** * 1 * 决定是否允许路由复用,若 `true` 会触发 `store` * 表示对所有路由允许复用 如果你有路由不想利用可以在这加一些业务逻辑判断,这里判断是否有data数据判断是否复用 * * @param {ActivatedRouteSnapshot} route * @returns {boolean} * @memberof NaReuseService */ shouldDetach(route: ActivatedRouteSnapshot): boolean { if (this.hasInValidRoute(route)) { return false; } this.log('#shouldDetach', this.can(route), NaUtils.getUrl(route)); return this.can(route); } /** * 2 * 存储: 当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象 * * @param {ActivatedRouteSnapshot} route * @param {*} handle * @memberof NaReuseService */ store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle) { if (this.count >= this._max) { this._cached.shift(); } const url = NaUtils.getUrl(route); const idx = this.index(url); const item: ReuseTabCached = { title: this.getTitle(url, route), closable: this.getClosable(url, route), url, route, handle, }; if (idx <= -1) { this._cached.push(item); } else { this._cached[idx] = item; } this.removeUrlBuffer = null; this.log('#store', idx === -1 ? '[new]' : '[override]', url); if (handle && handle['componentRef']) { this.runHook('_onReuseDestroy', url, handle['componentRef']); } this._cachedChange.next({ active: 'add', item, list: this._cached }); } /** * 3 * 决定是否允许应用缓存数据 * 若 path 在缓存中有的都认为允许还原路由 * * @param {ActivatedRouteSnapshot} route * @returns {boolean} * @memberof NaReuseService */ shouldAttach(route: ActivatedRouteSnapshot): boolean { if (this.hasInValidRoute(route)) { return false; } const url = NaUtils.getUrl(route); const data = this.get(url); const ret = !!(data && data.handle); this.log('#shouldAttach', ret, url); if (ret && data.handle['componentRef']) { this.runHook('_onReuseInit', url, data.handle['componentRef']); } return ret; } /** * 4 * 提取复用数据 * 从缓存中获取快照,若无则返回null * * @param {ActivatedRouteSnapshot} route * @returns {{}} * @memberof NaReuseService */ retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (this.hasInValidRoute(route)) { return null; } const url = NaUtils.getUrl(route); const data = this.get(url); const ret = (data && data.handle) || null; this.log('#retrieve', url, ret); return ret; } /** * 5 * 决定是否应该进行复用路由处理 * * @param {ActivatedRouteSnapshot} future * @param {ActivatedRouteSnapshot} curr * @returns {boolean} * @memberof NaReuseService */ shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { let ret = future.routeConfig === curr.routeConfig; if (!ret) { return false; } const path = ((future.routeConfig && future.routeConfig.path) || '') as string; // tslint:disable-next-line:no-bitwise if (path.length > 0 && ~path.indexOf(':')) { const futureUrl = NaUtils.getUrl(future); const currUrl = NaUtils.getUrl(curr); ret = futureUrl === currUrl; } this.log('====================='); this.log( '#shouldReuseRoute', ret, `${NaUtils.getUrl(curr)}=>${NaUtils.getUrl(future)}`, future, curr, ); return ret; } }