很多接觸過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應用會形成一棵應用的路由樹近刘,像下面這樣
應用會從根開始逐級去匹配每一級的路由節(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é)點弧腥。
還是以上面的路由樹為例厦取,它的檢測層級是這樣的:
對比圖示,方法的每一次調用時比較的都是同一層級的路由配置節(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)
下面是典型的調用順序鏈截圖:
三、使用問題
這個路由復用策略的使用限制比較大 窖贤,一般需要路由組織層級標準化广辰,且無法緩存多級路由出口嵌套的場景。
常用配置
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)常遇到下面這種錯誤:
這種錯誤可以通過修改緩存的匹配邏輯來避免几睛,我們也可以根據(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 {
// 組件恢復時
}
}
});