import { Injectable , Injector , OnDestroy } from '@angular/core' ; import { ActivatedRoute , ActivatedRouteSnapshot , ExtraOptions , NavigationEnd , NavigationStart , Router , ROUTER_CONFIGURATION } from '@angular/router' ; import { BehaviorSubject , Observable , timer , Unsubscribable } from 'rxjs' ; import { Menu , MenuService } from '@delon/theme' ; import { ScrollService } from '@delon/util/browser' ; import type { NzSafeAny } from 'ng-zorro-antd/core/types' ; import { ReuseComponentRef , ReuseHookOnReuseInitType , ReuseHookTypes , ReuseTabCached , ReuseTabMatchMode , ReuseTabNotify , ReuseTabRouteParamMatchMode , ReuseTitle } from './reuse-tab.interfaces' ; @ Injectable ( { providedIn : 'root' } ) export class ReuseTabService implements OnDestroy { private _inited = false ; private _max = 10 ; private _keepingScroll = false ; private _cachedChange = new BehaviorSubject < ReuseTabNotify | null > ( null ) ; private _cached : ReuseTabCached [ ] = [ ] ; private _titleCached : { [ url ] : ReuseTitle ; } = { } ; private _closableCached : { [ url ] : boolean ; } = { } ; private _router$ ? : Unsubscribable ; private removeUrlBuffer : string | null = null ; private positionBuffer : { [ url ] : [ number , number ] ; } = { } ; componentRef ? : ReuseComponentRef ; debug = false ; routeParamMatchMode : ReuseTabRouteParamMatchMode = 'strict' ; mode = ReuseTabMatchMode . Menu ; /** 排除规则，限 `mode=URL` */ excludes : RegExp [ ] = [ ] ; private get snapshot ( ) : ActivatedRouteSnapshot { return this . injector . get ( ActivatedRoute ) . snapshot ; } /**
     * Get init status
     *
     * 是否已经初始化完成
     */ get inited ( ) : boolean { return this . _inited ; } /**
     * Current routing address
     *
     * 当前路由地址
     */ get curUrl ( ) : string { return this . getUrl ( this . snapshot ) ; } /**
     * 允许最多复用多少个页面，取值范围 `2-100`，值发生变更时会强制关闭且忽略可关闭条件
     */ set max ( value ) { this . _max = Math . min ( Math . max ( value , 2 ) , 100 ) ; for ( let i = this . _cached . length ; i > this . _max ; i -- ) { this . _cached . pop ( ) ; } } set keepingScroll ( value ) { this . _keepingScroll = value ; this . initScroll ( ) ; } get keepingScroll ( ) : boolean { return this . _keepingScroll ; } keepingScrollContainer ? : Element ; /** 获取已缓存的路由 */ get items ( ) : ReuseTabCached [ ] { return this . _cached ; } /** 获取当前缓存的路由总数 */ get count ( ) : number { return this . _cached . length ; } /** 订阅缓存变更通知 */ get change ( ) : Observable < ReuseTabNotify | null > { return this . _cachedChange . asObservable ( ) ; } /** 自定义当前标题 */ set title ( value ) { const url = this . curUrl ; if ( typeof value === 'string' ) value = { text : value } ; this . _titleCached [ url ] = value ; this . di ( 'update current tag title: ' , value ) ; this . _cachedChange . next ( { active : 'title' , url , title : value , list : this . _cached } ) ; } /** 获取指定路径缓存所在位置，`-1` 表示无缓存 */ index ( url ) { return this . _cached . findIndex ( w => w . url === url ) ; } /** 获取指定路径缓存是否存在 */ exists ( url ) { return this . index ( url ) !== - 1 ; } /** 获取指定路径缓存 */ get ( url ? ) { return url ? this . _cached . find ( w => w . url === url ) || null : null ; } private remove ( url , includeNonCloseable ) { 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 ; } param includeNonCloseable close ( url , includeNonCloseable = false ) { this . removeUrlBuffer = url ; this . remove ( url , includeNonCloseable ) ; this . _cachedChange . next ( { active : 'close' , url , list : this . _cached } ) ; this . di ( 'close tag' , url ) ; return true ; } param includeNonCloseable closeRight ( url , includeNonCloseable = false ) { const start = this . index ( url ) ; for ( let i = this . count - 1 ; i > start ; i -- ) { this . remove ( i , includeNonCloseable ) ; } this . removeUrlBuffer = null ; this . _cachedChange . next ( { active : 'closeRight' , url , list : this . _cached } ) ; this . di ( 'close right tages' , url ) ; return true ; } param includeNonCloseable clear ( includeNonCloseable = false ) { this . _cached . forEach ( w => { if ( ! includeNonCloseable && w . closable ) this . destroy ( w . _handle ) ; } ) ; this . _cached = this . _cached . filter ( w => ! includeNonCloseable && ! w . closable ) ; this . removeUrlBuffer = null ; this . _cachedChange . next ( { active : 'clear' , list : this . _cached } ) ; this . di ( 'clear all catch' ) ; } param url param position example move ( url , position ) { const start = this . _cached . findIndex ( w => w . url === url ) ; if ( start === - 1 ) return ; const data = this . _cached . slice ( ) ; data . splice ( position < 0 ? data . length + position : position , 0 , data . splice ( start , 1 ) [ 0 ] ) ; this . _cached = data ; this . _cachedChange . next ( { active : 'move' , url , position , list : this . _cached } ) ; } /**
     * 强制关闭当前路由（包含不可关闭状态），并重新导航至 `newUrl` 路由
     */ replace ( newUrl ) { const url = this . curUrl ; if ( this . exists ( url ) ) { this . close ( url , true ) ; } else { this . removeUrlBuffer = url ; } this . injector . get < Router > ( Router ) . navigateByUrl ( newUrl ) ; } param url param route getTitle ( url , route ? ) { 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 } as ReuseTitle ; } const menu = this . getMenu ( url ) ; return menu ? { text : menu . text , i18n : menu . i18n } : { text : url } ; } /**
     * 清除标题缓存
     */ clearTitleCached ( ) { this . _titleCached = { } ; } /** 自定义当前 `closable` 状态 */ set closable ( value ) { const url = this . curUrl ; this . _closableCached [ url ] = value ; this . di ( 'update current tag closable: ' , value ) ; this . _cachedChange . next ( { active : 'closable' , closable : value , list : this . _cached } ) ; } param url param route getClosable ( url , route ? ) { 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 ; } /**
     * 清空 `closable` 缓存
     */ clearClosableCached ( ) { this . _closableCached = { } ; } getTruthRoute ( route ) { let next = route ; while ( next . firstChild ) next = next . firstChild ; return next ; } /**
     * 根据快照获取URL地址
     */ getUrl ( route ) { let next = this . getTruthRoute ( route ) ; const segments = [ ] ; while ( next ) { segments . push ( next . url . join ( '/' ) ) ; next = next . parent ! ; } const url = `/${ segments . filter ( i => i ) . reverse ( ) . join ( '/' ) }` ; return url ; } /**
     * 检查快照是否允许被复用
     */ can ( route ) { const url = this . 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 . isExclude ( url ) ; } isExclude ( url ) { return this . excludes . findIndex ( r => r . test ( url ) ) !== - 1 ; } /**
     * 刷新，触发一个 refresh 类型事件
     */ refresh ( data ? ) { this . _cachedChange . next ( { active : 'refresh' , data } ) ; } private destroy ( _handle ) { if ( _handle && _handle . componentRef && _handle . componentRef . destroy ) _handle . componentRef . destroy ( ) ; } private di ( ... args ) { if ( typeof ngDevMode === 'undefined' || ngDevMode ) { if ( ! this . debug ) return ; console . warn ( ... args ) ; } } constructor ( private injector , private menuService ) { } init ( ) { this . initScroll ( ) ; this . _inited = true ; } private getMenu ( url ) { const menus = this . menuService . getPathByUrl ( url ) ; if ( ! menus || menus . length === 0 ) return null ; return menus . pop ( ) ; } runHook ( method , comp , type = 'init' ) { if ( typeof comp === 'number' ) { const item = this . _cached [ comp ] ; comp = item . _handle . componentRef ; } if ( comp == null || ! comp . instance ) { return ; } const compThis = comp . instance ; const fn = compThis [ method ] ; if ( typeof fn !== 'function' ) { return ; } if ( method === '_onReuseInit' ) { fn . call ( compThis , type ) ; } else { ( fn as ( ) => void ) . call ( compThis ) ; } } private hasInValidRoute ( route ) { return ! route . routeConfig || ! ! route . routeConfig . loadChildren || ! ! route . routeConfig . children ; } /**
     * 决定是否允许路由复用，若 `true` 会触发 `store`
     */ shouldDetach ( route ) { if ( this . hasInValidRoute ( route ) ) return false ; this . di ( '#shouldDetach' , this . can ( route ) , this . getUrl ( route ) ) ; return this . can ( route ) ; } /**
     * 存储
     */ store ( _snapshot , _handle ) { const url = this . getUrl ( _snapshot ) ; const idx = this . index ( url ) ; const isAdd = idx === - 1 ; const item = { title : this . getTitle ( url , _snapshot ) , closable : this . getClosable ( url , _snapshot ) , position : this . getKeepingScroll ( url , _snapshot ) ? this . positionBuffer [ url ] : null , url , _snapshot , _handle } ; if ( isAdd ) { if ( this . count >= this . _max ) { const closeIdx = this . _cached . findIndex ( w => w . closable ! ) ; if ( closeIdx !== - 1 ) this . remove ( closeIdx , false ) ; } this . _cached . push ( item ) ; } else { const cahcedComponentRef = this . _cached [ idx ] . _handle ?. componentRef ; if ( _handle == null && cahcedComponentRef != null ) { timer ( 100 ) . subscribe ( ( ) => this . runHook ( '_onReuseInit' , cahcedComponentRef ) ) ; } this . _cached [ idx ] = item ; } this . removeUrlBuffer = null ; this . di ( '#store' , isAdd ? '[new]' : '[override]' , url ) ; if ( _handle && _handle . componentRef ) { this . runHook ( '_onReuseDestroy' , _handle . componentRef ) ; } if ( ! isAdd ) { this . _cachedChange . next ( { active : 'override' , item , list : this . _cached } ) ; } } /**
     * 决定是否允许应用缓存数据
     */ shouldAttach ( route ) { if ( this . hasInValidRoute ( route ) ) return false ; const url = this . getUrl ( route ) ; const data = this . get ( url ) ; const ret = ! ! ( data && data . _handle ) ; this . di ( '#shouldAttach' , ret , url ) ; if ( ! ret ) { this . _cachedChange . next ( { active : 'add' , url , list : this . _cached } ) ; } return ret ; } /**
     * 提取复用数据
     */ retrieve ( route ) { if ( this . hasInValidRoute ( route ) ) return null ; const url = this . getUrl ( route ) ; const data = this . get ( url ) ; const ret = ( data && data . _handle ) || null ; this . di ( '#retrieve' , url , ret ) ; return ret ; } /**
     * 决定是否应该进行复用路由处理
     */ shouldReuseRoute ( future , curr ) { let ret = future . routeConfig === curr . routeConfig ; if ( ! ret ) return false ; const path = ( ( future . routeConfig && future . routeConfig . path ) || '' ) as string ; if ( path . length > 0 && ~ path . indexOf ( ':' ) ) { if ( this . routeParamMatchMode === 'strict' ) { ret = this . getUrl ( future ) === this . getUrl ( curr ) ; } else { ret = path === ( ( curr . routeConfig && curr . routeConfig . path ) || '' ) ; } } this . di ( '=====================' ) ; this . di ( '#shouldReuseRoute' , ret , `${ this . getUrl ( curr ) }=>${ this . getUrl ( future ) }` , future , curr ) ; return ret ; } /**
     * 获取 `keepingScroll` 状态，顺序如下：
     *
     * 1. 路由配置中 data 属性中包含 `keepingScroll`
     * 2. 菜单数据中 `keepingScroll` 属性
     * 3. 组件 `keepingScroll` 值
     */ getKeepingScroll ( url , route ? ) { if ( route && route . data && typeof route . data . keepingScroll === 'boolean' ) return route . data . keepingScroll ; const menu = this . mode !== ReuseTabMatchMode . URL ? this . getMenu ( url ) : null ; if ( menu && typeof menu . keepingScroll === 'boolean' ) return menu . keepingScroll ; return this . keepingScroll ; } private get isDisabledInRouter ( ) : boolean { const routerConfig = this . injector . get < ExtraOptions > ( ROUTER_CONFIGURATION , { } as NzSafeAny ) ; return routerConfig . scrollPositionRestoration === 'disabled' ; } private get ss ( ) : ScrollService { return this . injector . get ( ScrollService ) ; } private initScroll ( ) { if ( this . _router$ ) { this . _router$ . unsubscribe ( ) ; } this . _router$ = this . injector . get < Router > ( Router ) . events . subscribe ( e => { if ( e instanceof NavigationStart ) { const url = this . curUrl ; if ( this . getKeepingScroll ( url , this . getTruthRoute ( this . snapshot ) ) ) { this . positionBuffer [ url ] = this . ss . getScrollPosition ( this . keepingScrollContainer ) ; } else { delete this . positionBuffer [ url ] ; } } else if ( e instanceof NavigationEnd ) { const url = this . curUrl ; const item = this . get ( url ) ; if ( item && item . position && this . getKeepingScroll ( url , this . getTruthRoute ( this . snapshot ) ) ) { if ( this . isDisabledInRouter ) { this . ss . scrollToPosition ( this . keepingScrollContainer , item . position ) ; } else { setTimeout ( ( ) => this . ss . scrollToPosition ( this . keepingScrollContainer , item . position ! ) , 1 ) ; } } } } ) ; } ngOnDestroy ( ) { const { _cachedChange , _router$ } = this ; this . clear ( ) ; this . _cached = [ ] ; _cachedChange . complete ( ) ; if ( _router$ ) { _router$ . unsubscribe ( ) ; } } }