很多接觸過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)用的路由樹,像下面這樣
應(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í)是這樣的:
對(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)用順序鏈截圖:
三、使用問題
這個(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ò)誤:
這種錯(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í)
}
}
});