功能场景描述
用angular开发项目中,特别是移动端,要求返回上一页面,保持上一页面在离开时的样子不变,由于我们在angular项目中正常情况下切换路由时,当前页面的组件是重新加载的,也就是经过了销毁和重建的过程,所以再次进到页面就都是新的内容。类似于做缓存页面的tab切换这种功能,在vue中我们可以想到的是缓存组件keep-alive实现,比较方便。但是在angular中并没有缓存组件的概念,只能从路由复用策略上想办法
RouteReuseStrategy类
官方对于路由策略提供了一个RouteReuseStrategy类,
abstract class RouteReuseStrategy{
//确定是否应分离此路由(及其子树)以便以后复用,返回true时执行store方法,存储当前路由快照,返回false,直接跳过
abstract shouldDetach(route:ActivatedRouteSnapshot):boolean
//存储分离的路由。
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void
//确定是否应重新连接此路由(及其子树),返回true执行retrieve方法,返回false,结束,路由重载
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean
//检索以前存储的路由
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null
//确定是否应复用路由 , 返回true,就直接执行shouldAttach方法,返回false执行shouldDetach方法
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean
}
看到官方提供的api也是晦涩难懂,本人也是花了好长时间才摸清楚具体方法的含义,以及工作原理
先照着官方提供的api来新建类继承
新建一个SimpleReuseStrategy类去继承RouteReuseStrategy,如下
import { ActivatedRouteSnapshot, DetachedRouteHandle, Route, RouteReuseStrategy } from "@angular/router";
import * as _ from "lodash";
import { Directive } from "@angular/core";
@Directive()
export class SimpleReuseStrategy implements RouteReuseStrategy {
private cacheRouters: any = new Map<string, DetachedRouteHandle>();
// 相同路由是否复用(路由进入触发)
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
console.log('shouldReuseRoute',this.getFullRouteURL(future),this.getFullRouteURL(curr), future, curr,future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params));
return future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params);
}
// 是否允许复用路由
shouldDetach(route: ActivatedRouteSnapshot): boolean {
console.log('shouldDetach', this.getFullRouteURL(route), route);
return true;
}
// 存入路由(路由离开出发)
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
console.log('store', this.getFullRouteURL(route), route, handle);
const url = this.getFullRouteURL(route);
this.cacheRouters.set(url, handle);
}
// 是否允许还原路由
shouldAttach(route: ActivatedRouteSnapshot): boolean {
const url = this.getFullRouteURL(route);
console.log('shouldAttach', this.getFullRouteURL(route), route,this.cacheRouters.has(url));
return this.cacheRouters.has(url);
}
// 获取存储路由
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | any {
const url = this.getFullRouteURL(route);
console.log('retrieve', this.getFullRouteURL(route), route,this.cacheRouters.get(url));
if (this.cacheRouters.has(url)) {
return this.cacheRouters.get(url);
} else {
return null;
}
}
//获取完整路由路径
private getFullRouteURL(route: ActivatedRouteSnapshot): string {
const { pathFromRoot } = route;
let fullRouteUrlPath: string[] = [];
pathFromRoot.forEach((item: ActivatedRouteSnapshot) => {
fullRouteUrlPath = fullRouteUrlPath.concat(this.getRouteUrlPath(item));
});
return `/${fullRouteUrlPath.join('/')}`;
}
private getRouteUrlPath(route: ActivatedRouteSnapshot) {
return route.url.map(urlSegment => urlSegment.path);
}
}
通过打印的顺序,可以总结如下执行顺序,
- shouldReuseRoute()
- shouldDetach()
- store()
- shouldAttach()
- retrieve()
之前有定义集合cacheRouters,是用来记录路由快照的,路由离开时,执行完成的shouldDetach()
方法后,返回true时,才会执行store这个存储方法 ,把当前的路由快照存储到cacheRouters集合中。等进入到新页面时,执行到shouldAttach方法时,如果返回为true,这时候会调用retrieve方法,然后通过retrieve方法,在存储的cacheRouters集合中找有无存过当前的路由,有则返回对应的路由快照,也就是复用路由,否则就返回null,也就是重载路由了。此创建的的类还需引入到相应的app.module中路由配置才可生效
providers: [{ provide: RouteReuseStrategy, useClass: SimpleReuseStrategy }],
以上的路由复用策略是配置好了,这里还有个问题,就是我如果有的页面需要遵循这个复用策略,有的路由不需要,这个时候可以通过预先配置好相应的路由配置,加相应的的参数,如下找到当前页面的路由配置表,需要遵循复用策略的,我们只需要在配置中加入相应的data,这个data很熟悉吧,一般给页面添加title也是在这个data里添加
要复用页面在data中加入自定义参数keepalive: true后,相当于是在路由系统中添加了一个这样的标记,在切换的时候,我们很容易得到这个值,然后回到我们自定义的SimpleReuseStrategy类,加一些判断
import { ActivatedRouteSnapshot, DetachedRouteHandle, Route, RouteReuseStrategy } from "@angular/router";
import * as _ from "lodash";
import { Directive } from "@angular/core";
@Directive()
export class SimpleReuseStrategy implements RouteReuseStrategy {
private cacheRouters: any = new Map<string, DetachedRouteHandle>();
// 相同路由是否复用(路由进入触发)
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
console.log('shouldReuseRoute',this.getFullRouteURL(future),this.getFullRouteURL(curr), future, curr,future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params));
return future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params);
}
// 是否允许复用路由
shouldDetach(route: ActivatedRouteSnapshot): boolean {
console.log('shouldDetach', this.getFullRouteURL(route), route);
return Boolean(route.data["keepalive"]);
}
// 存入路由(路由离开出发)
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
console.log('store', this.getFullRouteURL(route), route, handle);
const url = this.getFullRouteURL(route);
this.cacheRouters.set(url, handle);
}
// 是否允许还原路由
shouldAttach(route: ActivatedRouteSnapshot): boolean {
const url = this.getFullRouteURL(route);
console.log('shouldAttach', this.getFullRouteURL(route), route,this.cacheRouters.has(url));
return this.cacheRouters.has(url);
}
// 获取存储路由
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | any {
const url = this.getFullRouteURL(route);
console.log('retrieve', this.getFullRouteURL(route), route,this.cacheRouters.get(url));
if (Boolean(route.data["keepalive"]) && this.cacheRouters.has(url)) {
return this.cacheRouters.get(url);
} else {
return null;
}
}
//获取完整路由路径
private getFullRouteURL(route: ActivatedRouteSnapshot): string {
const { pathFromRoot } = route;
let fullRouteUrlPath: string[] = [];
pathFromRoot.forEach((item: ActivatedRouteSnapshot) => {
fullRouteUrlPath = fullRouteUrlPath.concat(this.getRouteUrlPath(item));
});
return `/${fullRouteUrlPath.join('/')}`;
}
private getRouteUrlPath(route: ActivatedRouteSnapshot) {
return route.url.map(urlSegment => urlSegment.path);
}
}
此时就可以达到控制只对部分页面进行路由复用,以上做到这里,又会发现新的问题,这个配置相当于是不管导航怎么跳转进入页面的,只要进过就会缓存,我返回的时候才需要调用缓存页面,那我不是通过返回的方式而是通过Router.navigate方式第二次进入页面的,这个时候我根本就不需要页面缓存,是要让路由重载的,想到这里,这个问题就很棘手了,我翻阅了大量的文献,没有找到官方的解决方案
如何区分页面是返回的路由跳转和正常的页面间通过Router.navigate的跳转
新的问题,解决思路是要分清楚这两个场景的区别,
返回的场景有 : 1.通过点击 触发项目的history.back() ,这个复用策略还是有缺陷的,
2.直接通过浏览器的返回按钮行为返回上一页面
不能全部满足期望,尝试了很多方法都不是很理想,这里的难点是怎么在页面给路由系统里添加对应的标志信息,和前面讲的路由配置data中添加 keepalive: true那样添加标记,尝试很多办法无果。又想了不是很优美的方法,用Router.navigate方式跳转,这是不调用缓存路由的,那么可以在跳转的时候在url上添加查询参数,路由策略那里通过获取路由参数进行判断,这个方式个人觉得很low,因此url地址栏上多了些没用的信息,强迫症的人觉得很丑。
然后我想到了一个退而求其次的方法,用订阅的方式来定义一个全局的变量,通过Router.navigate方式跳转的,就提前触发一下这个订阅
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class GlobalSubscriptionService {
/**路由是否复用*/
public $routeIsReuse = new BehaviorSubject<any>(null);
constructor() { }
}
在SimpleReuseStrategy类中加些判断
完整代码贴出来
import { GlobalSubscriptionService } from './../../services/global-subscription.service';
import { ActivatedRouteSnapshot, DetachedRouteHandle, Route, RouteReuseStrategy } from "@angular/router";
import * as _ from "lodash";
import { Directive } from "@angular/core";
@Directive()
export class SimpleReuseStrategy implements RouteReuseStrategy {
private cacheRouters: any = new Map<string, DetachedRouteHandle>();
constructor(
private $globalSub: GlobalSubscriptionService
) {
}
// 相同路由是否复用(路由进入触发)
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
console.log('shouldReuseRoute', this.getFullRouteURL(future), this.getFullRouteURL(curr), future, curr, future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params));
return future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params);
}
// 是否允许复用路由
shouldDetach(route: ActivatedRouteSnapshot): boolean {
console.log('shouldDetach', this.getFullRouteURL(route), route);
return Boolean(route.data["keepalive"]);
}
// 存入路由(路由离开出发)
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
console.log('store', this.getFullRouteURL(route), route, handle);
const url = this.getFullRouteURL(route);
this.cacheRouters.set(url, handle);
}
// 是否允许还原路由
shouldAttach(route: ActivatedRouteSnapshot): boolean {
const url = this.getFullRouteURL(route);
let routeIsReuse = _.cloneDeep(this.$globalSub.$routeIsReuse.value);
setTimeout(() => {
this.$globalSub.$routeIsReuse.next(null);
});
return this.cacheRouters.has(url) && this.cacheRouters.has(url) && url !== routeIsReuse?.field && !routeIsReuse?.value;
}
// 获取存储路由
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | any {
const url = this.getFullRouteURL(route);
console.log('retrieve', this.getFullRouteURL(route), route, this.cacheRouters.get(url));
if (Boolean(route.data["keepalive"])) {
return this.cacheRouters.get(url);
} else {
return false;
}
}
//获取完整路由路径
private getFullRouteURL(route: ActivatedRouteSnapshot): string {
const { pathFromRoot } = route;
let fullRouteUrlPath: string[] = [];
pathFromRoot.forEach((item: ActivatedRouteSnapshot) => {
fullRouteUrlPath = fullRouteUrlPath.concat(this.getRouteUrlPath(item));
});
return `/${fullRouteUrlPath.join('/')}`;
}
private getRouteUrlPath(route: ActivatedRouteSnapshot) {
return route.url.map(urlSegment => urlSegment.path);
}
}
总结
这个路由复用策略,有点晦涩难懂,一共提供了5个方法,有执行顺序,通过函数返回的布尔值来控制对应页面是否调用缓存,花点时间研究搞懂这5个方法的执行顺序,还是可以解决大部分场景需求,angular路由复用策略设计是有一定缺陷的,本文中讲的需求最后有一点遗憾,不够优雅,但是能满足要求,各位看官有啥好的建议,本人荣幸接收。