有採用RouteReuseStrategy,子路由切換到其他路徑再切回來跳Cannot reattach ActivatedRouteSnapshot錯誤

目前有AAA.BBB.CCC三種業務
想說在CCC的頁面底下加上可以前往C01和C02的第二層NavBar

所以
最上方的Navigation Bar是這樣

<mat-toolbar class="" >
  <div class="" >
    <button mat-button [routerLink]="['/AAA']" routerLinkActive="activebutton">
      AAA
    </button>
  </div>

  <div class="">
    <button
      mat-button
      [routerLink]="['/BBB']"  routerLinkActive="activebutton" >
      BBB
    </button>
  </div>

  <div class="">
    <button
      mat-button
      [routerLink]="['/CCC']"  routerLinkActive="activebutton" >
      CCC
    </button>
  </div>

  <button mat-button [routerLink]="['/login']">登出</button>
</mat-toolbar>

<router-outlet></router-outlet>

然後路徑規劃如下

const routes: Routes = [
  {
    path: '',
    canActivate: [AuthGuard],
    component: LayoutComponent,
    children: [
      { path: '', redirectTo: 'AAA', pathMatch: 'full' },
      { path: 'AAA', component: AAAComponent },    
      { path: 'BBB', component: BBBPageComponent },
      {
        path: 'CCC',
        component: CCCComponent,
        children: [
          {
            path: 'C01',
            component: C01Component,
          },
          {
            path: 'C02',
            component: C02Component,
          },
        ],
      },    
    ],
  },
  { path: 'login', component: LoginComponent },
  {
    path: '**',
    redirectTo: 'login',
    pathMatch: 'full',
  },
];

然後CCC的Navigation Bar是這樣設定

<mat-toolbar>
  <div class="">
    <button
      mat-button
      [routerLink]="['/CCC/C01']"
      routerLinkActive="activebutton"
    >
     C01
    </button>
  </div>

  <div class="">
    <button
      mat-button
      [routerLink]="['/CCC/C02']"
      routerLinkActive="activebutton"
    >
    C02
    </button>
  </div>
</mat-toolbar>

<router-outlet></router-outlet>

第一次進入CCC會顯示點選C01和C02的按鈕
C01和C02頁面都很正常也能切換

但是當去點BBB或AAA之後
再回來點CCC,就會跳以下錯誤

ERROR Error: Uncaught (in promise): 
Error: Cannot reattach ActivatedRouteSnapshot created from a different route

有採用RouteReuseStrategy

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

interface RouteStorageObject {
  snapshot: ActivatedRouteSnapshot;
  handle: DetachedRouteHandle;
}

export class AppRoutingCache implements RouteReuseStrategy {
  public static handlers: { [key: string]: DetachedRouteHandle } = {};

  // 判斷路由是否能重複使用
  public shouldDetach(route: ActivatedRouteSnapshot): boolean {
    if (!route.routeConfig || route.routeConfig.loadChildren) {
      return false;
    }

    // 默認所有的路由設定都可以重複使用
    // 可透過 route.data 的方式來設定重複使用的規則
    return true;
  }

  // 當路由離開時,會觸發此方法
  public store(
    route: ActivatedRouteSnapshot,
    handle: DetachedRouteHandle,
  ): void {
    // 將目前路由內容暫存起來
    AppRoutingCache.handlers[route.routeConfig.path] = handle;
  }

  // 當路由進入時,可判斷是否還原路由的暫存內容
  public shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return (
      !!route.routeConfig && !!AppRoutingCache.handlers[route.routeConfig.path]
    );
  }
  // 從 Cache 中取得對應的暫存內容
  public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    if (!route.routeConfig) {
      return null;
    }
    if (route.routeConfig.loadChildren) {
      return null;
    }

    return AppRoutingCache.handlers[route.routeConfig.path];
  }

  // 判斷是否同一路由
  public shouldReuseRoute(
    future: ActivatedRouteSnapshot,
    current: ActivatedRouteSnapshot,
  ): boolean {
    return future.routeConfig === current.routeConfig;
  }
}

有找到這幾篇
但是改了還是一樣
不知道是眼睛太大還是哪邊我概念有問題…
求幫忙診斷…感謝

https://stackoverflow.com/questions/56218581/angular-cannot-reattach-activatedroutesnapshot-with-a-different-number-of-childr

使用
https://github.com/angular/angular/issues/13869

改寫完成後問題解決

export class CustomRouteReuseStrategy extends RouteReuseStrategy {
  handlers: {[path:string]:DetachedRouteHandle} = {};

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    //Avoid second call to getter
    let config: Route = route.routeConfig;
    //Don't store lazy loaded routes
    return config && !config.loadChildren;
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    let path: string = this.getRoutePath(route);
    this.handlers[path] = handle;
    /*
      This is where we circumvent the error.
      Detached route includes nested routes, which causes error when parent route does not include the same nested routes
      To prevent this, whenever a parent route is stored, we change/add a redirect route to the current child route
    */
    let config: Route = route.routeConfig;
    if(config) {
      let childRoute: ActivatedRouteSnapshot = route.firstChild;
      let futureRedirectTo = childRoute ? childRoute.url.map(function(urlSegment) {
        return urlSegment.path;
      }).join('/') : '';
      let childRouteConfigs: Route[] = config.children;
      if(childRouteConfigs) {
        let redirectConfigIndex: number;
        let redirectConfig: Route = childRouteConfigs.find(function(childRouteConfig, index) {
          if(childRouteConfig.path === '' && !!childRouteConfig.redirectTo) {
            redirectConfigIndex = index;
            return true;
          }
          return false;
        });
        //Redirect route exists
        if(redirectConfig) {
          if(futureRedirectTo !== '') {
            //Current activated route has child routes, update redirectTo
            redirectConfig.redirectTo = futureRedirectTo;
          } else {
            //Current activated route has no child routes, remove the redirect (otherwise retrieval will always fail for this route)
            childRouteConfigs.splice(redirectConfigIndex, 1);
          }
        } else if(futureRedirectTo !== '') {
          childRouteConfigs.push({
            path: '',
            redirectTo: futureRedirectTo,
            pathMatch: 'full'
          });
        }
      }
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!this.handlers[this.getRoutePath(route)];
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    let config: Route = route.routeConfig;
    //We don't store lazy loaded routes, so don't even bother trying to retrieve them
    if(!config || config.loadChildren) {
      return false;
    }
    return this.handlers[this.getRoutePath(route)];
  }

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }

  getRoutePath(route: ActivatedRouteSnapshot): string {
    let namedOutletCount: number = 0;
    return route.pathFromRoot.reduce((path, route) => {
      let config: Route = route.routeConfig;
      if(config) {
        if(config.outlet && config.outlet !== PRIMARY_OUTLET) {
          path += `(${config.outlet}:`;
          namedOutletCount++;
        } else {
          path += '/';
        }
        return path += config.path
      }
      return path;
    }, '') + (namedOutletCount ? new Array(namedOutletCount + 1).join(')') : '');
  }
}

裡面提到可能問題為

This is router config from plunkr:

    {path: '', redirectTo: 'search', pathMatch: 'full'},
    {path: 'search', component: SearchComponent},
    {
      path: 'person/:id', component: PersonComponent,
      children:[
        { path: '', redirectTo: 'view', pathMatch: 'full' },
        { path: 'view', component: ViewPersonComponent },
        { path: 'edit', component: EditPersonComponent }
        ]
    }

Custom reuse strategy uses route.routeConfig.path as a key in local storage, but it does not work when you have child routes, in this case you can see that there are two child routes for person/:id: View and Edit. Storage entry for person/:id gets overwritten with person/:id/Edit and when person/:id is retrieved last time it actually returns person/:id/Edit route, but expected route is person/:id/View.

The question is how should we choose key for routes when implementing custom reuse strategy? route.routeConfig.path is not suitable because of the reasons stated above, we need unique route id for any given route. Another question is why we get person/:id here at all? It is in the middle of the path.