自定义Angular路由复用策略(页面前进后退时,能保持之前的状态)

1、问题提出

  • 在基于Angular的SPA应用中,应用通过路由在各个页面之间进行导航。 默认情况下,用户在离开一个页面时,这个页面(组件)会被Angular销毁,用户的输入信息也随之丢失,当用户再次进入这个页面时,看到的是一个新生成的页面(组件),之前的输入信息都没了。

  • 配置的前端项目就是基于Angular的,工作中遇到了这样的问题,部分页面需要保存用户的输入信息,用户再次进入页面时需要回到上一次离开时的状态,部分页面每次都要刷新页面,不需要保存用户信息。而页面间的导航正是通过路由实现的,Angular的默认行为不能满足我们的需求!

2、解决思路

  • 针对以上问题,通过查阅Angular的相关资料可以发现,Angular提供了RouteReuseStrategy接口,通过实现这个接口,可以让开发者自定义路由复用策略。

2.1 RouteReuseStrategy接口

我们先来看看RouteReuseStrategy的接口定义:

image.png

这个接口只定义了5个方法,每个方法的作用如下:

  • shouldDetach
    路由离开时是否需要保存页面,这是实现自定义路由复用策略最重要的一个方法。

其中:

返回值为true时,路由离开时保存页面信息,当路由再次激活时,会直接显示保存的页面。

返回值为false时,路由离开时直接销毁组件,当路由再次激活时,直接初始化为新页面。

  • store
    如果shouldDetach方法返回true,会调用这个方法来保存页面。

  • shouldAttach
    路由进入页面时是否有页面可以重用。 true: 重用页面,false:生成新的页面

  • retrieve
    路由激活时获取保存的页面,如果返回null,则生成新页面

  • shouldReuseRout
    决定跳转后是否可以使用跳转前的路由页面,即跳转前后跳转后使用相同的页面

在这个默认的路由复用策略中,只有当跳转前和跳转后的路由一致时,才会复用页面。只要跳转前和跳转后的路由不一致,页面就会被销毁。

有鉴于此,我们需要实现一个自定义的路由复用策略,实现针对不同的路由,能够有不同的行为。同时,也要能兼容现有代码,不能对现有代码做大规模的修改。

3、代码如下

3.1 首先建一个 ZwRouteReuseStrategy 类实现 RouteReuseStrategy 接口,自定义其中方法。

import {
    ActivatedRouteSnapshot,
    DetachedRouteHandle,
    RouteReuseStrategy
  } from '@angular/router';
  import { Injectable } from '@angular/core';
  
  interface IRouteConfigData {
    reuse: boolean;
  }
  
  interface ICachedRoute {
    handle: DetachedRouteHandle;
    data: IRouteConfigData;
  }
  @Injectable()
  export class ZwRouteReuseStrategy implements RouteReuseStrategy {
    private static routeCache = new Map<string, ICachedRoute>();
    private static waitDelete: string; // 当前页未进行存储时需要删除
    private static currentDelete: string; // 当前页存储过时需要删除
  
    /** 进入路由触发,判断是否是同一路由 */
    shouldReuseRoute(
      future: ActivatedRouteSnapshot,
      curr: ActivatedRouteSnapshot
    ): boolean {
      const IsReturn =
        future.routeConfig === curr.routeConfig &&
        JSON.stringify(future.params) == JSON.stringify(curr.params);
      return IsReturn;
    }
  
    /** 表示对所有路由允许复用 如果你有路由不想利用可以在这加一些业务逻辑判断,这里判断是否有data数据判断是否复用 */
    shouldDetach(route: ActivatedRouteSnapshot): boolean {
      if (this.getRouteData(route)) {
        return true;
      }
      return false;
    }
  
    /** 当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象 */
    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
      // const url = this.getFullRouteUrl(route);
      const url = this.getRouteUrl(route);
      const data = this.getRouteData(route);
  
      if (
        ZwRouteReuseStrategy.waitDelete &&
        ZwRouteReuseStrategy.waitDelete === url
      ) {
        // 如果待删除是当前路由,且未存储过则不存储快照
        ZwRouteReuseStrategy.waitDelete = null;
        return null;
      } else {
        // 如果待删除是当前路由,且存储过则不存储快照
        if (
          ZwRouteReuseStrategy.currentDelete &&
          ZwRouteReuseStrategy.currentDelete === url
        ) {
          ZwRouteReuseStrategy.currentDelete = null;
          return null;
        } else {
          if (handle) {
            ZwRouteReuseStrategy.routeCache.set(url, { handle, data });
            this.addRedirectsRecursively(route);
          }
        }
      }
    }
  
    /** 若 path 在缓存中有的都认为允许还原路由 */
    shouldAttach(route: ActivatedRouteSnapshot): boolean {
      // const url = this.getFullRouteUrl(route);
      const url = this.getRouteUrl(route);
      const handle = ZwRouteReuseStrategy.routeCache.has(url)
        ? ZwRouteReuseStrategy.routeCache.get(url).handle
        : null;
      const data = this.getRouteData(route);
      const IsReturn =
        data && ZwRouteReuseStrategy.routeCache.has(url) && handle != null;
      return IsReturn;
    }
  
    /** 从缓存中获取快照,若无则返回nul */
    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
      const url = this.getRouteUrl(route);
      const data = this.getRouteData(route);
      const IsReturn =
        data && ZwRouteReuseStrategy.routeCache.has(url)
          ? ZwRouteReuseStrategy.routeCache.get(url).handle
          : null;
  
      return IsReturn;
    }
  
    private addRedirectsRecursively(route: ActivatedRouteSnapshot): void {
      const config = route.routeConfig;
      if (config) {
        if (!config.loadChildren) {
          const routeFirstChild = route.firstChild;
          const routeFirstChildUrl = routeFirstChild
            ? this.getRouteUrlPaths(routeFirstChild).join('/')
            : '';
          const childConfigs = config.children;
          if (childConfigs) {
            const childConfigWithRedirect = childConfigs.find(
              c => c.path === '' && !!c.redirectTo
            );
            if (childConfigWithRedirect) {
              childConfigWithRedirect.redirectTo = routeFirstChildUrl;
            }
          }
        }
        route.children.forEach(childRoute =>
          this.addRedirectsRecursively(childRoute)
        );
      }
    }
    private getRouteUrl(route: ActivatedRouteSnapshot) {
      return (
        route['_routerState'].url.replace(/\//g, '_') +
        '_' +
        (route.routeConfig.loadChildren ||
          route.routeConfig.component
            .toString()
            .split('(')[0]
            .split(' ')[1])
      );
    }
  
    private getFullRouteUrl(route: ActivatedRouteSnapshot): string {
      return this.getFullRouteUrlPaths(route)
        .filter(Boolean)
        .join('/')
        .replace('/', '_');
    }
  
    private getFullRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
      const paths = this.getRouteUrlPaths(route);
      return route.parent
        ? [...this.getFullRouteUrlPaths(route.parent), ...paths]
        : paths;
    }
  
    private getRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
      return route.url.map(urlSegment => urlSegment.path);
    }
  
    private getRouteData(route: ActivatedRouteSnapshot): IRouteConfigData {
      return (
        route.routeConfig &&
        (route.routeConfig.data as IRouteConfigData) &&
        route.routeConfig.data.reuse
      );
    }
  
    /** 用于删除路由快照*/
    public deleteRouteSnapshot(url: string): void {
      if (url[0] === '/') {
        url = url.substring(1);
      }
      url = url.replace('/', '_');
      if (ZwRouteReuseStrategy.routeCache.has(url)) {
        ZwRouteReuseStrategy.routeCache.delete(url);
        ZwRouteReuseStrategy.currentDelete = url;
      } else {
        ZwRouteReuseStrategy.waitDelete = url;
      }
    }
    public clear() {
      ZwRouteReuseStrategy.routeCache.clear();
    }
    public clearExcept(list) {
      if (!list || !ZwRouteReuseStrategy.routeCache) return;
      try {
        let waitDelete = [];
        ZwRouteReuseStrategy.routeCache.forEach((value: ICachedRoute, key) => {
          let handle: any = value.handle;
          let url = handle.route.value._routerState.snapshot.url;
          if (list.indexOf(url) < 0) {
            waitDelete.push(key);
          }
        });
        waitDelete.forEach(item => {
          ZwRouteReuseStrategy.routeCache.delete(item);
        });
      } catch (error) {
        console.log('clearExcept error', error);
      }
    }
  }  

3.2 配置路由重用策略为自定义策略

为了使用自定义的路由复用策略,需要在应用的根路由模块providers中使用自定义的路由复用策略。

image.png

3.3 配置路由

在路由配置中,按需配置路由的data属性。如需要保存页面,则设置reuse值为true,如不需要保存页面,不配置该属性。例如:

image.png

此路由配置下,访问/list页面会在路由离开时会被保存,再次进入该页面都会恢复到上一次离开该页面时的状态。

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