# Angular 路由快照 使用RouteReuseStrategy路由复用策略暂存路由状态

> Angular(angular2 ) RouteReuseStrategy使用情景，后台数据信息管理类系统往往会用到页面临时状态存储，通常表现为Tab切换形式；临时性离开页面存储当前页面状态，以便重新返回该页面恢复其状态；也就是进入路由时，是否初始化组件。

Author: DeathGhost
Published: 2018-07-21 00:36:44
Category: angular
Canonical: https://www.deathghost.cn/article/angular/28
Keywords: Angular, RouteReuseStrategy, 路由快照, 路由缓存, 路由状态暂存, 路由复用策略

去年开始接触Angular（angular2 ）框架，实例性了一个后台管理系统，看的是云里雾里，到现在也是稀里糊涂乱七八糟的，暂时顶多也只是能够正常使用。

在项目后台系统操作过程中，往往会用到页面（路由）状态临时性缓存下来（也可以称其为快照），在不销毁的前提下，再次进入依然返回离开时的状态；

好比在这个路由页面中是个表单，我们在处理中途跳转到别的页面，再次返回，当初数据依然存在。这么一个功能性操作。

![Angular RouteReuseStrategy](https://www.deathghost.cn/public/upload/article/2018/08/1534340536831.jpg)

Angular 标签式路由缓存示例图

以下原出处于网络文档，实例测试中有些地方做了修改调整。

## 首先了解下Angular中RouteReuseStrategy有哪几种方法可操作：

- shouldDetach 是否允许复用路由；

- store 当路由离开时会触发，存储路由

- shouldAttach 是否允许还原路由

- retrieve 获取存储路由

- shouldReuseRoute 进入路由触发，是否同一路由时复用路由

## 使用方法（实例环境：Angular5.2，工具vscode）

### 1、在\src\app目录下创建app-route-reuse-strategy.ts文件，内容如下：

import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';

export class AppRouteReuseStrategy implements RouteReuseStrategy {

    public static handlers: { [key: string]: DetachedRouteHandle } = {};
    private static waitDelete: string;
    // 删除
    public static deleteRouteSnapshot(name: string): void {
        if (AppRouteReuseStrategy.handlers[name]) {
            delete AppRouteReuseStrategy.handlers[name];
        } else {
            AppRouteReuseStrategy.waitDelete = name;
        }
    }
    /** 表示对所有路由允许复用 如果你有路由不想利用可以在这加一些业务逻辑判断 */
    public shouldDetach(route: ActivatedRouteSnapshot): boolean {
        // 添加控制器过滤 false则不对其缓存，反之...
        if (!route.data.keep) {
            return false;
        }
        if (!route.routeConfig || route.routeConfig.loadChildren) {
            return false;
        }
        return true;
    }

    /** 当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象 */
    public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        if (AppRouteReuseStrategy.waitDelete && AppRouteReuseStrategy.waitDelete === route.routeConfig.path) {
            // 如果待删除是当前路由则不存储快照
            AppRouteReuseStrategy.waitDelete = null;
            return;
        }
        AppRouteReuseStrategy.handlers[route.routeConfig.path] = handle;
    }

    /** 若 path 在缓存中有的都认为允许还原路由 */
    public shouldAttach(route: ActivatedRouteSnapshot): boolean {
        return !!route.routeConfig && !!AppRouteReuseStrategy.handlers[route.routeConfig.path];
    }

    /** 从缓存中获取快照，若无则返回null */
    public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig) { return null; }
        return AppRouteReuseStrategy.handlers[route.routeConfig.path];
    }

    /** 进入路由触发，判断是否同一路由 */
    public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return future.routeConfig === curr.routeConfig;
    }
}

### 2、在app.module.ts中导入AppRouteReuseStrategy模块

import { RouteReuseStrategy } from '@angular/router';
import { AppRouteReuseStrategy } from './app-route-reuse-strategy';
// ...
providers: [
  {provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy}
]

### 3、目标组件TS文件中导入及定义（注意路径）

import { AppRouteReuseStrategy } from '../../app-route-reuse-strategy';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
import { Title } from '@angular/platform-browser';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css'],
  providers: [AppRouteReuseStrategy]
})
// 路由列表
menuList: Array<{ title: string, module: string, power: string, isSelect: boolean }> = [];
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private titleService: Title
) {
  // 路由事件
  this.router.events.filter(event => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .map(route => {
    while (route.firstChild) { route = route.firstChild; }
    return route;
  })
  .filter(route => route.outlet === 'primary')
  .mergeMap(route => route.data)
  .subscribe((event) => {
    // 路由data的标题
    const title = event['title'];
    this.menuList.forEach(p => p.isSelect = false);
    const menu = { title: title, module: event['module'], power: event['power'], isSelect: true};
    this.titleService.setTitle(title);
    const exitMenu = this.menuList.find(info => info.title === title);
    if (exitMenu) {
      // 如果存在不添加，当前表示选中
      this.menuList.forEach(p => p.isSelect = p.title === title);
      return ;
    }
    this.menuList.push(menu);
  });
}
// 关闭选项标签
  closeUrl(module: string, isSelect: boolean) {
    // 当前关闭的是第几个路由
    const index = this.menuList.findIndex(p => p.module === module);
    // 如果只有一个不可以关闭
    if (this.menuList.length === 1) { return; }
    this.menuList = this.menuList.filter(p => p.module !== module);
    // 删除复用
    AppRouteReuseStrategy.deleteRouteSnapshot(module);
    if (!isSelect) { return; }
    // 显示上一个选中
    let menu = this.menuList[index - 1];
    if (!menu) {
      // 如果上一个没有,下一个选中
      menu = this.menuList[index];
    }
    this.menuList.forEach(p => p.isSelect = p.module === menu.module );
    // 显示当前路由信息
    this.router.navigate(['admin/'   menu.module]);
  }

### 4、路由设置，路由中添加参数

data: { title: '用户列表', module: 'user-list', keep: true, power: ''}

其中module值与path一致，keep值是个boolean（也就是AppRouteReuseStrategy中shouldDetach一个判断，是否对其路由状态进行暂存；如否，则每次进入路由都会进行初始化请求，反之...）

### 4、组件布局结构样式，参考标示性段落，里面部分有实例中的元素可忽略。

临时性Tab溢出我用scrollWidth 与 offsetWidth判断显示与否及按钮滚动距离控制。

<div class="interim-nav">
  <div class="scroll-tabs" #scrollNav>
    <span *ngFor="let menu of menuList" class="dg-tabs">
      <span *ngIf="menu.isSelect">
        <span class="dg-tab" routerLink="{{ menu.module }}" routerLinkActive='active'>{{menu.title}}</span>
        <a (click)="closeUrl(menu.module,menu.isSelect)" title="删除"
          *ngIf="menuList.length === 1 ? false : true">
          <i class="anticon anticon-close"></i>
        </a>
      </span>
      <span *ngIf="!menu.isSelect">
        <span class="dg-tab" routerLink="{{ menu.module }}" routerLinkActive='active'>{{menu.title}}</span>
        <a (click)="closeUrl(menu.module,menu.isSelect)" title="删除"
          *ngIf="menuList.length === 1 ? false : true">
          <i class="anticon anticon-close"></i>
        </a>
      </span>
    </span>
  </div>
  <div class="scroll-btn" *ngIf="scrollNav.scrollWidth > scrollNav.offsetWidth ? true : false">
    <a title="左移" (click)="move(scrollNav, 'lt')" class="text999"><i class="anticon anticon-fast-backward"></i></a>
    <i class="anticon anticon-pause"></i>
    <a title="右移" (click)="move(scrollNav, 'rt')" class="text999"><i class="anticon anticon-fast-forward"></i></a>
  </div>
</div>
样式仅供参考，具体根据设计而定

.interim-nav{
    display:flex;
    position: relative;
    border-bottom:1px #ececec solid;
    border-top:1px #ececec solid;
    background-color:white;
    height:42px;
    line-height:42px;
    z-index:1;
}
.interim-nav::before,.interim-nav::after{content:"";position: absolute;top:0;width:25px;height:100%;z-index:2;}
.interim-nav::before{
    left:0;
    background-image:linear-gradient(to left,white,rgba(255,255,255,.5));
}
.interim-nav::after{
    right:145px;
    background-image:linear-gradient(to right,rgba(255,255,255,.5),white);
}
.interim-nav .scroll-tabs{position:relative;flex:1;padding:0 10px;margin:0 12px;white-space:nowrap;overflow:hidden;}
.interim-nav .dg-tabs{display:inline-block;cursor: pointer;margin-right:32px;}
最后，我们在登出后台管理系统时，需要移除Tabs列表，否则在不刷新浏览器的情况下，再次登入会报错，具体的忘做记录，这里就不对其表示了；我对其上述menuList做了循环删除操作，以便清空所有快照，再次登录方可进行。

this.menuList.forEach(item => {
  AppRouteReuseStrategy.deleteRouteSnapshot(item.module);
});
最后就是打包发布，尽量使用ng build --prod 这样会小很多，提升初始化打开速度，注意的一点是未定义的直接删除掉或在ts中对其定义下，否则打包过程中会报错。

## Related

- Next: [Angular自定义预加载策略](https://www.deathghost.cn/article/angular/88)
