解析Angular路由組件緩存復(fù)用和實(shí)現(xiàn)問題

很多接觸過vuejs的同學(xué)對(duì)keep-alive這個(gè)指令是印象比較深刻的,它可以指定vue組件緩存之前的狀態(tài),不論是路由組件還是動(dòng)態(tài)切換組件,不像正常的組件一樣進(jìn)行銷毀和重新實(shí)例化,這在有些功能的實(shí)現(xiàn)顯得十分重要。而且,這在其他兩個(gè)框架并沒有官方提供指令,從這點(diǎn)可以看出vue設(shè)計(jì)的巧妙,以及對(duì)需求的考慮細(xì)致。

在angular中,雖然不能像vue一樣自由緩存組件示例狀態(tài),卻提供了一個(gè)路由復(fù)用策略來實(shí)現(xiàn)對(duì)路由組件實(shí)例的緩存和復(fù)用,我們平時(shí)使用的多級(jí)嵌套路由在切換時(shí)上層路由出口的實(shí)例不會(huì)重現(xiàn)實(shí)例化,就是angular內(nèi)部使用默認(rèn)的路由復(fù)用策略實(shí)現(xiàn)的,這點(diǎn)在看完下面的流程分析就明白了。

一、概念

路由樹

我們知道,在配置了路由導(dǎo)航的angular應(yīng)用會(huì)形成一棵應(yīng)用的路由樹,像下面這樣


route-reuse-tree.png

應(yīng)用會(huì)從根開始逐級(jí)去匹配每一級(jí)的路由節(jié)點(diǎn)和routeConfig,并檢測(cè)實(shí)例化路由組件,其中routeConfig涵蓋樹里的每一個(gè)節(jié)點(diǎn),包括懶加載路由

路由復(fù)用策略

RouteReuseStrategy是angular提供的一個(gè)路由復(fù)用策略,暴露了簡(jiǎn)單的接口

abstract  class  RouteReuseStrategy {
  // 判斷是否復(fù)用路由
  abstract  shouldReuseRoute(future:  ActivatedRouteSnapshot, curr:  ActivatedRouteSnapshot): boolean
  // 存儲(chǔ)路由快照&組件當(dāng)前實(shí)例對(duì)象
  abstract  store(route:  ActivatedRouteSnapshot, handle:  DetachedRouteHandle):  void
  // 判斷是否允許還原路由對(duì)象及其子對(duì)象
  abstract  shouldAttach(route:  ActivatedRouteSnapshot): boolean
  // 獲取實(shí)例對(duì)象,決定是否實(shí)例化還是使用緩存
  abstract  retrieve(route:  ActivatedRouteSnapshot):  DetachedRouteHandle  |  null
  // 判斷路由是否允許復(fù)用
  abstract  shouldDetach(route:  ActivatedRouteSnapshot): boolean
}

二、方法解析

1) shouldReuseRoute

檢測(cè)是否復(fù)用路由,該方法根據(jù)返回值來決定是否繼續(xù)調(diào)用,如果返回值為true則表示當(dāng)前節(jié)點(diǎn)層級(jí)路由復(fù)用,將繼續(xù)下一路由節(jié)點(diǎn)調(diào)用,入?yún)榈膄uture和curr不確定,每次都交叉?zhèn)魅耄环駝t,則停止調(diào)用,表示從這個(gè)節(jié)點(diǎn)開始將不再復(fù)用。
兩個(gè)路由路徑切換的時(shí)候是從“路由樹”的根開始從上往下層級(jí)依次比較和調(diào)用的,并且兩邊每次比較的都是同一層級(jí)的路由節(jié)點(diǎn)配置。root路由節(jié)點(diǎn)調(diào)用一次,非root路由節(jié)點(diǎn)調(diào)用兩次這個(gè)方法,第一次比較父級(jí)節(jié)點(diǎn),第二次比較當(dāng)前節(jié)點(diǎn)。
還是以上面的路由樹為例,它的檢測(cè)層級(jí)是這樣的:


route-reuse-tree-2.png

對(duì)比圖示,方法的每一次調(diào)用時(shí)比較的都是同一層級(jí)的路由配置節(jié)點(diǎn),就是像圖中被橫線穿在一起的那些一樣,即入?yún)⒌膄uture和curr是同級(jí)的。
舉個(gè)例子,shouldReuseRoute方法的常見實(shí)現(xiàn)為:

shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
  return  future.routeConfig  === curr.routeConfig;
}

這時(shí)當(dāng)路由從“main/cop/web/pc”切換到“main/cop/fan/list/group”的調(diào)用順序是這樣的:

 root  -->  main  -->  web  / fan (返回false)

即到第3層的時(shí)候routeConfig不一樣,返回false,調(diào)用結(jié)束,得到不復(fù)用的“分叉路由點(diǎn)”

這個(gè)方法得到的結(jié)果很重要,將作為其他好幾個(gè)方法的基礎(chǔ)

2) retrieve

緊接著shouldReuseRoute方法返回false的節(jié)點(diǎn)調(diào)用,入?yún)oute即是當(dāng)前層級(jí)路由不需要復(fù)用。以上個(gè)例子說明,此時(shí)的route是main/cop/fan/的路由節(jié)點(diǎn)。
retrieve調(diào)用根據(jù)返回結(jié)果來決定是否繼續(xù)調(diào)用:如果返回的是null,當(dāng)前路由對(duì)應(yīng)的組件會(huì)實(shí)例化,并繼續(xù)對(duì)其子級(jí)路由調(diào)用retrieve方法,直到遇到緩存路由或到末級(jí)路由。

在本次路由還原時(shí)也會(huì)調(diào)用,用來獲取緩存示例

3) shouldDetach

用來判斷剛剛離開的上一個(gè)路由是否復(fù)用,其調(diào)用的時(shí)機(jī)也是當(dāng)前層級(jí)路由不需要復(fù)用,shouldReuseRoute方法返回false的時(shí)候。以上個(gè)例子說明,首次調(diào)用的入?yún)oute是main/cop/web/的路由節(jié)點(diǎn)。
shouldDetach方法根據(jù)返回結(jié)果來決定是否繼續(xù)調(diào)用:如果返回的是false,則繼續(xù)下一層級(jí)調(diào)用該方法,當(dāng)前路由對(duì)應(yīng)的組件會(huì)實(shí)例化,并繼續(xù)對(duì)其子級(jí)路由調(diào)用retrieve方法,直到返回true或者是最末級(jí)路由后才結(jié)束。

4) store

緊接著shouldDetach方法返回true的時(shí)候調(diào)用,存儲(chǔ)需要被緩存的那一級(jí)路由的DetachedRouteHandle;若沒有返回true的則不調(diào)用。
以上個(gè)例子說明,若我們?cè)O(shè)置了main/cop/web/pc的keep=true,此時(shí)的入?yún)oute是main/cop/web/pc節(jié)點(diǎn),存儲(chǔ)的是它的實(shí)例對(duì)象。
注意:

  • 無論路徑上有幾個(gè)可以被緩存的路由節(jié)點(diǎn),被存儲(chǔ)的只有有一個(gè),就是Detach第一次返回true的那次
  • 在本次路由還原后也會(huì)調(diào)用一次此方法存儲(chǔ)實(shí)例

5) shouldAttach

判斷是否允許還原路由對(duì)象及其子對(duì)象,調(diào)用時(shí)機(jī)是當(dāng)前層級(jí)路由不需要復(fù)用的時(shí)候,即shouldReuseRoute()返回false的時(shí)候,而且,并不是所有的路由層級(jí)都是有組件實(shí)例的,只有包含component的route才會(huì)觸發(fā)shouldAttach。
如果反回false,將繼續(xù)到當(dāng)前路由的下一帶有component的路由層級(jí)調(diào)用shouldAttach,直到返回true或者是最末級(jí)路由后才結(jié)束。
當(dāng)shouldAttach返回true時(shí)就調(diào)用一次retrieve方法和store方法

6)調(diào)用順序

shouldReuseRoute -> retrieve -> shouldDetach -> store -> shouldAttach -
-> retrieve(若shouldAttach返回true) -> store(若shouldAttach返回true) 

下面是典型的調(diào)用順序鏈截圖:


屏幕快照 2019-10-20 下午12.16.57.png

三、使用問題

這個(gè)路由復(fù)用策略的使用限制比較大 ,一般需要路由組織層級(jí)標(biāo)準(zhǔn)化,且無法緩存多級(jí)路由出口嵌套的場(chǎng)景。

常用配置

import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';

export class AppReuseStrategy implements RouteReuseStrategy {
    public static handlers: { [key: string]: DetachedRouteHandle } = {};

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        // 若是全緩存可去掉此分支
        if (!route.data.keep) {
            return false;
        }
        return true;
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        AppReuseStrategy.handlers[this.getRouteUrl(route)] = handle;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        return !!AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!AppReuseStrategy.handlers[this.getRouteUrl(route)]) {
            return null;
        }
        return AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params);
    }

    /** 使用route的path作為快照的key */
    getRouteUrl(route: ActivatedRouteSnapshot) {
        const path = route['_routerState'].url.replace(/\//g, '_');
        return path;
    }
}

這個(gè)配置的使用限制很大,通常需要路由有嚴(yán)格的層級(jí)配置,一般在同一module下的同級(jí)路由組件之間的緩存和切換時(shí)很好用的,但是在不同module之間切換或者時(shí)緩存路由不同級(jí)時(shí)就會(huì)出現(xiàn)恢復(fù)的不是你想要的組件實(shí)例,或者經(jīng)常遇到下面這種錯(cuò)誤:


屏幕快照 2019-10-20 下午12.22.55.png

這種錯(cuò)誤可以通過修改緩存的匹配邏輯來避免,我們也可以根據(jù)我們的使用業(yè)務(wù)來修改各個(gè)方法的邏輯條件來滿足使用場(chǎng)景。
下面是時(shí)間總結(jié)的幾種使用和避免錯(cuò)誤的方法:

1、清除緩存實(shí)例

由于策略的使用限制,我們可以提供兩個(gè)清除緩存的接口

   // 清除單個(gè)路由緩存
    public static deleteRouteSnapshot(path: string): void {
        const name = path.replace(/\//g, '_');
        if (AppReuseStrategy.handlers[name]) {
            delete AppReuseStrategy.handlers[name];
        }
    }
    // 清除全部路由緩存
    public static clear(): void {
        for (let key in AppReuseStrategy.handlers) {
            delete AppReuseStrategy.handlers[key];
        }
    }

根據(jù)需要可以在其他組件的初始化調(diào)用這個(gè)接口做清除工作,更好的方法是利用路由守衛(wèi),在模塊的共同父路由守衛(wèi)里調(diào)用clear接口

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad  {
  ...
  canActivate(next: ActivatedRouteSnapshot,
              state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    ...
    AppReuseStrategy.clear();
    ...
}

這樣可以避免不同模塊之間切換的錯(cuò)誤,在同一模塊內(nèi)的緩存和切換依然生效。

2、重組url

上面的方式雖然可行,但把策略的修改波及到其他地方,不內(nèi)聚,可以通過修改緩存匹配URL的方式讓策略自己實(shí)現(xiàn)而不上報(bào)reattach不匹配的錯(cuò)誤:有緩存實(shí)例,復(fù)用;否則,實(shí)例化。修改上面的方案:

export class AppReuseStrategy implements RouteReuseStrategy {

    public static handlers: { [key: string]: DetachedRouteHandle } = {};
    public static currRouteConfig: any;
    ...
    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        const diffUrl = this.getDiffRouteUrl(this.getRouteUrl(route));
        return !!AppReuseStrategy.handlers[diffUrl];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        const diffUrl = this.getDiffRouteUrl(this.getRouteUrl(route));
        if (!AppReuseStrategy.handlers[diffUrl]) {
            return null;
        }
        return AppReuseStrategy.handlers[diffUrl];
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        if (future.routeConfig === curr.routeConfig &&
            JSON.stringify(future.params) === JSON.stringify(curr.params)) {
            return true;
        } else {
            AppReuseStrategy.currRouteConfig =curr.routeConfig;
            return false;
        }
    }

    getRouteUrl(route: ActivatedRouteSnapshot) {
        const path = route['_routerState'].url.replace(/\//g, '_');
        return path;
    }

    getDiffRouteUrl(path: any) {
        if (AppReuseStrategy.currRouteConfig && AppReuseStrategy.currRouteConfig.children) {
            for (let child of AppReuseStrategy.currRouteConfig.children) {
                if (path.lastIndexOf(child.path) !== -1) {
                    return path.slice(0, path.lastIndexOf(`_${child.path}`));
                }
            }
            return path;
        } else {
            return path;
        }
    }
}

3、只緩存葉子組件

事實(shí)上在我們路由樹里,通常是有葉子路由節(jié)點(diǎn)需要被緩存和復(fù)用,依賴整個(gè)“樹枝”一起存儲(chǔ)占內(nèi)存也沒有必要,二來由于策略局限性也容易出現(xiàn)問題。存儲(chǔ)葉子即可緩存指定的葉子節(jié)點(diǎn),也可以在不同模塊間自由切換,還是修改上面的例子:

export class AppReuseStrategy implements RouteReuseStrategy {
    public static handlers: { [key: string]: DetachedRouteHandle } = {};

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.debug('shouldDetach======>', route);
        if (!route.data.keep) {
            return false;
        }
        return true;
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        console.debug('store======>', route, handle);
        AppReuseStrategy.handlers[this.getRouteUrl(route)] = handle;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        console.debug('shouldAttach======>', route);
        return !route.routeConfig.children && !route.routeConfig.loadChildren && 
            !!AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        console.debug('retrieve======>', route);
        if (route.routeConfig.children || route.routeConfig.loadChildren || !AppReuseStrategy.handlers[this.getRouteUrl(route)]) {
            return null;
        }
        return AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        console.debug('shouldReuseRoute======>');
        return future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params);
    }

    getRouteUrl(route: ActivatedRouteSnapshot) {
        const path = route['_routerState'].url.replace(/\//g, '_');
        return path;
    }
}

若要支持非葉子節(jié)點(diǎn)的緩存,可以增加次標(biāo)志符,比如perantKeep,如下:

    ...
    path: 'cop-project',
    canActivate: [AuthGuard],
    data: {perantKeep: true},
    children: [
      ...
    ]

修改策略方法:

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.debug('shouldDetach======>', route);
        if (!route.data.keep && !route.data.perantKeep) {
            return false;
        }
        return true;
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        AppReuseStrategy.handlers[this.getRouteUrl(route)] = handle;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        return (route.data.keepParent || !route.routeConfig.children && !route.routeConfig.loadChildren) && 
          !!AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if ((!route.data.keepParent && (route.routeConfig.children || route.routeConfig.loadChildren)) ||
          !AppReuseStrategy.handlers[this.getRouteUrl(route)]) {
            return null;
        }
        return AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return (!curr.data.keepParent || !future.data.keepParent) && 
              (future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params));
    }

有時(shí)候我們想在緩存頁面的切出和切入時(shí)干點(diǎn)事情,因?yàn)榇藭r(shí)組件不再重新初始化,以前放在Init和Destroy鉤子里做的事情可能需要考慮找個(gè)時(shí)機(jī)來做,可以使rxjs訂閱來做,修改策略代碼,增加subject,

import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
import { Observable, Subject } from 'rxjs';

export class RouteMsg {
    url: string = '';
    type: string = '';
    constructor(type: string, url: string) {
        this.type = type;
        this.url = url;
    }
}

export class AppReuseStrategy implements RouteReuseStrategy {
    public static handlers: { [key: string]: DetachedRouteHandle } = {};
    public static routeText$ = new Subject<RouteMsg>();

    public static getRouteText(): Observable<RouteMsg> {
        return AppReuseStrategy.routeText$.asObservable();
    }

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        if (!route.data.keep) {
            return false;
        }
        AppReuseStrategy.routeText$.next(new RouteMsg('detach', route['_routerState'].url));
        return true;
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if ((!route.data.keepParent && (route.routeConfig.children || route.routeConfig.loadChildren)) || !AppReuseStrategy.handlers[this.getRouteUrl(route)]) {
            return null;
        }
        AppReuseStrategy.routeText$.next(new RouteMsg('attach', route['_routerState'].url));
        return AppReuseStrategy.handlers[this.getRouteUrl(route)];
    }
    ...
}

在對(duì)應(yīng)組件訂閱該對(duì)象

AppReuseStrategy. getRouteText().subscrib(res => {
  if(res.res === this.url) {
    if(res.type === 'detach') {
      // 組件切換出
    } else {
      // 組件恢復(fù)時(shí)
    }
  }
});
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一、對(duì)于 MVVM 的理解?# MVVM是 Model-View-Viewmodel的縮寫,Model代表數(shù)據(jù)模型...
    一朵er閱讀 284評(píng)論 0 0
  • 本文首發(fā)于TalkingCoder,一個(gè)有逼格的程序員社區(qū)。轉(zhuǎn)載請(qǐng)注明出處和作者。 寫在前面 本文為系列文章,總共...
    Aresn閱讀 9,568評(píng)論 0 42
  • 主要還是自己看的,所有內(nèi)容來自官方文檔。 介紹 Vue.js 是什么 Vue (讀音 /vju?/,類似于 vie...
    Leonzai閱讀 3,375評(píng)論 0 25
  • VUE介紹 Vue的特點(diǎn)構(gòu)建用戶界面,只關(guān)注View層簡(jiǎn)單易學(xué),簡(jiǎn)潔、輕量、快速漸進(jìn)式框架 框架VS庫庫,是一封裝...
    多多醬_DuoDuo_閱讀 2,742評(píng)論 1 17
  • 什么是組件? 組件 (Component) 是 Vue.js 最強(qiáng)大的功能之一。組件可以擴(kuò)展 HTML 元素,封裝...
    youins閱讀 9,528評(píng)論 0 13