/

angular 路由组件缓存复用

类似于vue的keep-alive指令一样,在路由切换的时候,不用销毁和重新实例化组件。
angular提供了路由策略来实现对组件的缓存和复用。我们使用的多级嵌套路由在切换的时,父级路由出口的实例不会重新实例化。就是angular内部使用默认的路由复用策略实现的,这点在看完下面的流程分析就明白了。

一、概念

1、路由树

我们知道,在配置了路由导航的angular应用会形成一棵应用的路由树

路由树图片

应用会从根开始逐级去匹配每一级的路由节点和routeConfig,并检测实例化路由组件,其中routeConfig涵盖树里的每一个节点,包括懒加载路由

2、路由复用策略

RouteReuseStrategy是angular提供的一个路由复用策略,暴露了简单的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class RouteReuseStrategy { // 确定是否应重用路由 abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean // 存储分离的路由 存储“null”值应删除以前存储的值 abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void // 确定是否应重用路由 abstract shouldAttach(route: ActivatedRouteSnapshot): boolean // 检索以前存储的路由 abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null // 确定是否应分离此路由(及其子树)以供以后重用。若 `true` 会触发 `store // 离开的路由,是否储存 abstract shouldDetach(route: ActivatedRouteSnapshot): boolean }

二、方法解析

1、shouldReuseRoute

检测是否复用路由,该方法根据返回值来决定是否继续调用,如果返回值为true则表示当前节点层级路由复用,将继续下一路由节点调用,入参为的future和curr不确定,每次都交叉传入;否则,则停止调用,表示从这个节点开始将不再复用。
两个路由切换的时候是从“路由树”的根开始从上往下层级依次比较和调用的,并且两边每次比较的都是同一层级的路由节点配置。root路由节点调用一次,非root路由节点调用两次这个方法,第一次比较父级节点,第二次比较当前节点。

还是以上面的路由树为例,它的检测层级是这样的:

路由树图片

对比图示,方法的每一次调用时比较的都是同一层级的路由配置节点,就是像图中被横线穿在一起的那些一样,即入参的future和curr是同级的。

举个例子,shouldReuseRoute方法的常见实现为:

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

这时当路由从“main/cop/web/pc”切换到“main/cop/fan/list/group”的调用顺序是这样的:

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

即到第3层的时候routeConfig不一样,返回false,调用结束,得到不复用的“分叉路由点”

这个方法得到的结果很重要,将作为其他好几个方法的基础

2、retrieve

紧接着shouldReuseRoute方法返回false的节点调用,入参route即是当前层级路由不需要复用。以上个例子说明,此时的route是main/cop/fan/的路由节点。 retrieve调用根据返回结果来决定是否继续调用:如果返回的是null,当前路由对应的组件会实例化,并继续对其子级路由调用retrieve方法,直到遇到缓存路由或到末级路由

在本次路由还原时也会调用,用来获取缓存示例

3、shouldDetach

用来判断刚刚离开的上一个路由是否复用,其调用的时机也是当前层级路由不需要复用,shouldReuseRoute方法返回false的时候。以上个例子说明,首次调用的入参route是main/cop/web/的路由节点。 shouldDetach方法根据返回结果来决定是否继续调用:如果返回的是false,则继续下一层级调用该方法,当前路由对应的组件会实例化,并继续对其子级路由调用retrieve方法,直到返回true或者是最末级路由后才结束。

4、store

紧接着shouldDetach方法返回true的时候调用,存储需要被缓存的那一级路由的DetachedRouteHandle;若没有返回true的则不调用。 以上个例子说明,若我们设置了main/cop/web/pc的keep=true,此时的入参route是main/cop/web/pc节点,存储的是它的实例对象。

无论路径上有几个可以被缓存的路由节点,被存储的只有有一个,就是Detach第一次返回true的那次 在本次路由还原后也会调用一次此方法存储实例

5、shouldAttach

判断是否允许还原路由对象及其子对象,调用时机是当前层级路由不需要复用的时候,即shouldReuseRoute()返回false的时候,而且,并不是所有的路由层级都是有组件实例的,只有包含component的route才会触发shouldAttach。 如果反回false,将继续到当前路由的下一带有component的路由层级调用shouldAttach,直到返回true或者是最末级路由后才结束。 当shouldAttach返回true时就调用一次retrieve方法和store方法

三、调用顺序

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

null

四、应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
export class RouteMsg { constructor(public type: string, public url: string, public route: ActivatedRouteSnapshot) { } } export class AppReuseStrategy implements RouteReuseStrategy { private static routeText$ = new Subject<RouteMsg>() private static handlers: Map<string, DetachedRouteHandle> = new Map() public static routeReuseEvent = AppReuseStrategy.routeText$.asObservable() /** * 确定是否应分离此路由(及其子树)以供以后重用。若 `true` 会触发 `store * @param route * @returns */ shouldDetach(route: ActivatedRouteSnapshot): boolean { if (this.hasInValidRoute(route)) { return false } AppReuseStrategy.routeText$.next(new RouteMsg('detach', this.getUrl(route), route)) return Boolean(route.data.keep) } /** * 存储分离的路线 存储“null”值应删除以前存储的值 * @param route * @param handle */ store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { AppReuseStrategy.handlers.set(this.getUrl(route), handle) } /** * 确定是否应重新附着此路由(及其子树) * @param route * @returns */ shouldAttach(route: ActivatedRouteSnapshot): boolean { if (this.hasInValidRoute(route)) { return false } AppReuseStrategy.routeText$.next(new RouteMsg('attach', this.getUrl(route), route)) return AppReuseStrategy.handlers.has(this.getUrl(route)) } /** * 检索以前存储的路由 * @param route * @returns */ retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { if (this.hasInValidRoute(route)) return null return AppReuseStrategy.handlers.get(this.getUrl(route))||null } /** * 确定是否应重用路由 * @param future * @param curr * @returns */ shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 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(':')) { ret = this.getUrl(future) === this.getUrl(curr) } return ret } hasInValidRoute(route: ActivatedRouteSnapshot): boolean { return !route.routeConfig || !!route.routeConfig.loadChildren || !!route.routeConfig.children; } getTruthRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot { let next = route; while (next.firstChild) next = next.firstChild; return next; } /** * 根据快照获取URL地址 */ getUrl(route: ActivatedRouteSnapshot): string { let next = this.getTruthRoute(route); const segments: string[] = []; while (next) { segments.push(next.url.join('/')); next = next.parent!; } const url = `/${segments .filter(i => i) .reverse() .join('/')}`; return url; } } //在对应组件订阅该对象 //此时组件不再重新初始化,以前放在Init和Destroy钩子里做的事情可能需要考虑找个时机来做,可以使rxjs订阅来做,修改策略代码,增加subject, AppReuseStrategy.routeReuseEvent.pipe( takeUntil(this.unsubscribe$) ).subscribe(res => { if (res.type == 'detach') { } else if (this.router.url.includes(res.url)) { if (res.type == 'attach') { } } }) }
作者:liuk123标签:angular分类:angular

本文是原创文章,采用 CC BY-NC-ND 4.0 协议, 完整转载请注明来自 liuk123