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

很多接觸過vuejs的同學對keep-alive這個指令是印象比較深刻的舌镶,它可以指定vue組件緩存之前的狀態(tài)块饺,不論是路由組件還是動態(tài)切換組件邑遏,不像正常的組件一樣進行銷毀和重新實例化穆端,這在有些功能的實現(xiàn)顯得十分重要。而且传惠,這在其他兩個框架并沒有官方提供指令迄沫,從這點可以看出vue設計的巧妙,以及對需求的考慮細致卦方。

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

一睬捶、概念

路由樹

我們知道,在配置了路由導航的angular應用會形成一棵應用的路由樹近刘,像下面這樣


route-reuse-tree.png

應用會從根開始逐級去匹配每一級的路由節(jié)點和routeConfig擒贸,并檢測實例化路由組件,其中routeConfig涵蓋樹里的每一個節(jié)點觉渴,包括懶加載路由

路由復用策略

RouteReuseStrategy是angular提供的一個路由復用策略介劫,暴露了簡單的接口

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

二案淋、方法解析

1) shouldReuseRoute

檢測是否復用路由座韵,該方法根據(jù)返回值來決定是否繼續(xù)調用,如果返回值為true則表示當前節(jié)點層級路由復用踢京,將繼續(xù)下一路由節(jié)點調用誉碴,入?yún)榈膄uture和curr不確定,每次都交叉?zhèn)魅氚昃啵环駝t黔帕,則停止調用,表示從這個節(jié)點開始將不再復用旨涝。
兩個路由路徑切換的時候是從“路由樹”的根開始從上往下層級依次比較和調用的蹬屹,并且兩邊每次比較的都是同一層級的路由節(jié)點配置。root路由節(jié)點調用一次白华,非root路由節(jié)點調用兩次這個方法慨默,第一次比較父級節(jié)點,第二次比較當前節(jié)點弧腥。
還是以上面的路由樹為例厦取,它的檢測層級是這樣的:


route-reuse-tree-2.png

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

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

這時當路由從“main/cop/web/pc”切換到“main/cop/fan/list/group”的調用順序是這樣的:

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

即到第3層的時候routeConfig不一樣霎箍,返回false奇钞,調用結束,得到不復用的“分叉路由點”

這個方法得到的結果很重要漂坏,將作為其他好幾個方法的基礎

2) retrieve

緊接著shouldReuseRoute方法返回false的節(jié)點調用景埃,入?yún)oute即是當前層級路由不需要復用。以上個例子說明顶别,此時的route是main/cop/fan/的路由節(jié)點谷徙。
retrieve調用根據(jù)返回結果來決定是否繼續(xù)調用:如果返回的是null,當前路由對應的組件會實例化驯绎,并繼續(xù)對其子級路由調用retrieve方法完慧,直到遇到緩存路由或到末級路由。

在本次路由還原時也會調用剩失,用來獲取緩存示例

3) shouldDetach

用來判斷剛剛離開的上一個路由是否復用屈尼,其調用的時機也是當前層級路由不需要復用,shouldReuseRoute方法返回false的時候赴叹。以上個例子說明鸿染,首次調用的入?yún)oute是main/cop/web/的路由節(jié)點。
shouldDetach方法根據(jù)返回結果來決定是否繼續(xù)調用:如果返回的是false乞巧,則繼續(xù)下一層級調用該方法,當前路由對應的組件會實例化摊鸡,并繼續(xù)對其子級路由調用retrieve方法绽媒,直到返回true或者是最末級路由后才結束。

4) store

緊接著shouldDetach方法返回true的時候調用免猾,存儲需要被緩存的那一級路由的DetachedRouteHandle是辕;若沒有返回true的則不調用。
以上個例子說明猎提,若我們設置了main/cop/web/pc的keep=true获三,此時的入?yún)oute是main/cop/web/pc節(jié)點,存儲的是它的實例對象锨苏。
注意:

  • 無論路徑上有幾個可以被緩存的路由節(jié)點疙教,被存儲的只有有一個,就是Detach第一次返回true的那次
  • 在本次路由還原后也會調用一次此方法存儲實例

5) shouldAttach

判斷是否允許還原路由對象及其子對象伞租,調用時機是當前層級路由不需要復用的時候贞谓,即shouldReuseRoute()返回false的時候,而且葵诈,并不是所有的路由層級都是有組件實例的裸弦,只有包含component的route才會觸發(fā)shouldAttach祟同。
如果反回false,將繼續(xù)到當前路由的下一帶有component的路由層級調用shouldAttach理疙,直到返回true或者是最末級路由后才結束晕城。
當shouldAttach返回true時就調用一次retrieve方法和store方法

6)調用順序

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

下面是典型的調用順序鏈截圖:


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

三、使用問題

這個路由復用策略的使用限制比較大 窖贤,一般需要路由組織層級標準化广辰,且無法緩存多級路由出口嵌套的場景。

常用配置

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;
    }
}

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


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

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

1粤攒、清除緩存實例

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

   // 清除單個路由緩存
    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ù)需要可以在其他組件的初始化調用這個接口做清除工作,更好的方法是利用路由守衛(wèi)夯接,在模塊的共同父路由守衛(wèi)里調用clear接口

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

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

2盔几、重組url

上面的方式雖然可行晴弃,但把策略的修改波及到其他地方,不內(nèi)聚逊拍,可以通過修改緩存匹配URL的方式讓策略自己實現(xiàn)而不上報reattach不匹配的錯誤:有緩存實例上鞠,復用;否則芯丧,實例化芍阎。修改上面的方案:

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、只緩存葉子組件

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

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é)點的緩存椒袍,可以增加次標志符驼唱,比如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));
    }

有時候我們想在緩存頁面的切出和切入時干點事情驹暑,因為此時組件不再重新初始化玫恳,以前放在Init和Destroy鉤子里做的事情可能需要考慮找個時機來做辨赐,可以使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)];
    }
    ...
}

在對應組件訂閱該對象

AppReuseStrategy. getRouteText().subscrib(res => {
  if(res.res === this.url) {
    if(res.type === 'detach') {
      // 組件切換出
    } else {
      // 組件恢復時
    }
  }
});
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惭婿,隨后出現(xiàn)的幾起案子不恭,更是在濱河造成了極大的恐慌,老刑警劉巖财饥,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件换吧,死亡現(xiàn)場離奇詭異,居然都是意外死亡钥星,警方通過查閱死者的電腦和手機沾瓦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谦炒,“玉大人贯莺,你說我怎么就攤上這事∧模” “怎么了缕探?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長还蹲。 經(jīng)常有香客問我爹耗,道長,這世上最難降的妖魔是什么秽誊? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任鲸沮,我火速辦了婚禮,結果婚禮上锅论,老公的妹妹穿的比我還像新娘。我一直安慰自己楣号,他們只是感情好最易,可當我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著炫狱,像睡著了一般藻懒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上视译,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天嬉荆,我揣著相機與錄音,去河邊找鬼酷含。 笑死鄙早,一個胖子當著我的面吹牛汪茧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播限番,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼舱污,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弥虐?” 一聲冷哼從身側響起扩灯,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霜瘪,沒想到半個月后珠插,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡颖对,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年捻撑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惜互。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡布讹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出训堆,到底是詐尸還是另有隱情描验,我是刑警寧澤,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布坑鱼,位于F島的核電站膘流,受9級特大地震影響,放射性物質發(fā)生泄漏鲁沥。R本人自食惡果不足惜呼股,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望画恰。 院中可真熱鬧彭谁,春花似錦、人聲如沸允扇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽考润。三九已至狭园,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間糊治,已是汗流浹背唱矛。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绎谦。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓管闷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親燥滑。 傳聞我的和親對象是個殘疾皇子渐北,可洞房花燭夜當晚...
    茶點故事閱讀 45,876評論 2 361

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

  • 一、對于 MVVM 的理解铭拧?# MVVM是 Model-View-Viewmodel的縮寫赃蛛,Model代表數(shù)據(jù)模型...
    一朵er閱讀 281評論 0 0
  • 本文首發(fā)于TalkingCoder,一個有逼格的程序員社區(qū)搀菩。轉載請注明出處和作者呕臂。 寫在前面 本文為系列文章,總共...
    Aresn閱讀 9,536評論 0 42
  • 主要還是自己看的肪跋,所有內(nèi)容來自官方文檔歧蒋。 介紹 Vue.js 是什么 Vue (讀音 /vju?/,類似于 vie...
    Leonzai閱讀 3,358評論 0 25
  • VUE介紹 Vue的特點構建用戶界面州既,只關注View層簡單易學谜洽,簡潔、輕量吴叶、快速漸進式框架 框架VS庫庫阐虚,是一封裝...
    多多醬_DuoDuo_閱讀 2,719評論 1 17
  • 什么是組件? 組件 (Component) 是 Vue.js 最強大的功能之一蚌卤。組件可以擴展 HTML 元素实束,封裝...
    youins閱讀 9,487評論 0 13