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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容