实现angular路由复用策略

功能场景描述

用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
}
image.png

看到官方提供的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);
    }
}

通过打印的顺序,可以总结如下执行顺序,

  1. shouldReuseRoute()
  2. shouldDetach()
  3. store()
  4. shouldAttach()
  5. retrieve()
image.png

之前有定义集合cacheRouters,是用来记录路由快照的,路由离开时,执行完成的shouldDetach()
方法后,返回true时,才会执行store这个存储方法 ,把当前的路由快照存储到cacheRouters集合中。等进入到新页面时,执行到shouldAttach方法时,如果返回为true,这时候会调用retrieve方法,然后通过retrieve方法,在存储的cacheRouters集合中找有无存过当前的路由,有则返回对应的路由快照,也就是复用路由,否则就返回null,也就是重载路由了。此创建的的类还需引入到相应的app.module中路由配置才可生效

 providers: [{ provide: RouteReuseStrategy, useClass: SimpleReuseStrategy }],
image.png

以上的路由复用策略是配置好了,这里还有个问题,就是我如果有的页面需要遵循这个复用策略,有的路由不需要,这个时候可以通过预先配置好相应的路由配置,加相应的的参数,如下找到当前页面的路由配置表,需要遵循复用策略的,我们只需要在配置中加入相应的data,这个data很熟悉吧,一般给页面添加title也是在这个data里添加

image.png

要复用页面在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方式跳转的,就提前触发一下这个订阅

image.png

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class GlobalSubscriptionService {
  /**路由是否复用*/
  public $routeIsReuse = new BehaviorSubject<any>(null);
  constructor() { }
}
image.png

在SimpleReuseStrategy类中加些判断


image.png

完整代码贴出来

 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路由复用策略设计是有一定缺陷的,本文中讲的需求最后有一点遗憾,不够优雅,但是能满足要求,各位看官有啥好的建议,本人荣幸接收。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容