【问题标题】:Error: Cannot reattach ActivatedRouteSnapshot created from a different route错误:无法重新附加从不同路由创建的 ActivatedRouteSnapshot
【发布时间】:2017-05-25 21:03:14
【问题描述】:

我正在尝试实现RouteReuseStrategy 类。当我导航到顶级路径时它工作正常。

一旦路径具有子路径并且我导航到子路径,然后导航回顶级路径,我就会收到以下错误:

错误:未捕获(承诺中):错误:无法重新附加从不同路由创建的 ActivatedRouteSnapshot

我创建了一个plunker 来演示错误。我看到 plunker 在 IE 11 中不起作用,请在最新版本的 Chrome

中查看

重现错误的步骤:

第一步:

第二步

第三步

第四步

可以在控制台查看错误:

我已尝试在此 article 上找到的实现

export class CustomReuseStrategy implements RouteReuseStrategy {

    handlers: {[key: string]: DetachedRouteHandle} = {};

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.debug('CustomReuseStrategy:shouldDetach', route);
        return true;
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        console.debug('CustomReuseStrategy:store', route, handle);
        this.handlers[route.routeConfig.path] = handle;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        console.debug('CustomReuseStrategy:shouldAttach', route);
        return !!route.routeConfig && !!this.handlers[route.routeConfig.path];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        console.debug('CustomReuseStrategy:retrieve', route);
        if (!route.routeConfig) return null;
        return this.handlers[route.routeConfig.path];
    }

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

}

还有这个stackoverflow的实现answer

/**
 * reuse-strategy.ts
 * by corbfon 1/6/17
 */

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

/** Interface for object which can store both: 
 * An ActivatedRouteSnapshot, which is useful for determining whether or not you should attach a route (see this.shouldAttach)
 * A DetachedRouteHandle, which is offered up by this.retrieve, in the case that you do want to attach the stored route
 */
interface RouteStorageObject {
    snapshot: ActivatedRouteSnapshot;
    handle: DetachedRouteHandle;
}

export class CustomReuseStrategy implements RouteReuseStrategy {

    /** 
     * Object which will store RouteStorageObjects indexed by keys
     * The keys will all be a path (as in route.routeConfig.path)
     * This allows us to see if we've got a route stored for the requested path
     */
    storedRoutes: { [key: string]: RouteStorageObject } = {};

    /** 
     * Decides when the route should be stored
     * If the route should be stored, I believe the boolean is indicating to a controller whether or not to fire this.store
     * _When_ it is called though does not particularly matter, just know that this determines whether or not we store the route
     * An idea of what to do here: check the route.routeConfig.path to see if it is a path you would like to store
     * @param route This is, at least as I understand it, the route that the user is currently on, and we would like to know if we want to store it
     * @returns boolean indicating that we want to (true) or do not want to (false) store that route
     */
    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        let detach: boolean = true;
        console.log("detaching", route, "return: ", detach);
        return detach;
    }

    /**
     * Constructs object of type `RouteStorageObject` to store, and then stores it for later attachment
     * @param route This is stored for later comparison to requested routes, see `this.shouldAttach`
     * @param handle Later to be retrieved by this.retrieve, and offered up to whatever controller is using this class
     */
    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        let storedRoute: RouteStorageObject = {
            snapshot: route,
            handle: handle
        };

        console.log("store:", storedRoute, "into: ", this.storedRoutes);
        // routes are stored by path - the key is the path name, and the handle is stored under it so that you can only ever have one object stored for a single path
        this.storedRoutes[route.routeConfig.path] = storedRoute;
    }

    /**
     * Determines whether or not there is a stored route and, if there is, whether or not it should be rendered in place of requested route
     * @param route The route the user requested
     * @returns boolean indicating whether or not to render the stored route
     */
    shouldAttach(route: ActivatedRouteSnapshot): boolean {

        // this will be true if the route has been stored before
        let canAttach: boolean = !!route.routeConfig && !!this.storedRoutes[route.routeConfig.path];

        // this decides whether the route already stored should be rendered in place of the requested route, and is the return value
        // at this point we already know that the paths match because the storedResults key is the route.routeConfig.path
        // so, if the route.params and route.queryParams also match, then we should reuse the component
        if (canAttach) {
            let willAttach: boolean = true;
            console.log("param comparison:");
            console.log(this.compareObjects(route.params, this.storedRoutes[route.routeConfig.path].snapshot.params));
            console.log("query param comparison");
            console.log(this.compareObjects(route.queryParams, this.storedRoutes[route.routeConfig.path].snapshot.queryParams));

            let paramsMatch: boolean = this.compareObjects(route.params, this.storedRoutes[route.routeConfig.path].snapshot.params);
            let queryParamsMatch: boolean = this.compareObjects(route.queryParams, this.storedRoutes[route.routeConfig.path].snapshot.queryParams);

            console.log("deciding to attach...", route, "does it match?", this.storedRoutes[route.routeConfig.path].snapshot, "return: ", paramsMatch && queryParamsMatch);
            return paramsMatch && queryParamsMatch;
        } else {
            return false;
        }
    }

    /** 
     * Finds the locally stored instance of the requested route, if it exists, and returns it
     * @param route New route the user has requested
     * @returns DetachedRouteHandle object which can be used to render the component
     */
    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {

        // return null if the path does not have a routerConfig OR if there is no stored route for that routerConfig
        if (!route.routeConfig || !this.storedRoutes[route.routeConfig.path]) return null;
        console.log("retrieving", "return: ", this.storedRoutes[route.routeConfig.path]);

        /** returns handle when the route.routeConfig.path is already stored */
        return this.storedRoutes[route.routeConfig.path].handle;
    }

    /** 
     * Determines whether or not the current route should be reused
     * @param future The route the user is going to, as triggered by the router
     * @param curr The route the user is currently on
     * @returns boolean basically indicating true if the user intends to leave the current route
     */
    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        console.log("deciding to reuse", "future", future.routeConfig, "current", curr.routeConfig, "return: ", future.routeConfig === curr.routeConfig);
        return future.routeConfig === curr.routeConfig;
    }

    /** 
     * This nasty bugger finds out whether the objects are _traditionally_ equal to each other, like you might assume someone else would have put this function in vanilla JS already
     * One thing to note is that it uses coercive comparison (==) on properties which both objects have, not strict comparison (===)
     * @param base The base object which you would like to compare another object to
     * @param compare The object to compare to base
     * @returns boolean indicating whether or not the objects have all the same properties and those properties are ==
     */
    private compareObjects(base: any, compare: any): boolean {

        // loop through all properties in base object
        for (let baseProperty in base) {

            // determine if comparrison object has that property, if not: return false
            if (compare.hasOwnProperty(baseProperty)) {
                switch (typeof base[baseProperty]) {
                    // if one is object and other is not: return false
                    // if they are both objects, recursively call this comparison function
                    case 'object':
                        if (typeof compare[baseProperty] !== 'object' || !this.compareObjects(base[baseProperty], compare[baseProperty])) { return false; } break;
                    // if one is function and other is not: return false
                    // if both are functions, compare function.toString() results
                    case 'function':
                        if (typeof compare[baseProperty] !== 'function' || base[baseProperty].toString() !== compare[baseProperty].toString()) { return false; } break;
                    // otherwise, see if they are equal using coercive comparison
                    default:
                        if (base[baseProperty] != compare[baseProperty]) { return false; }
                }
            } else {
                return false;
            }
        }

        // returns true only after false HAS NOT BEEN returned through all loops
        return true;
    }
}

RouteReuseStrategy 准备好迎接孩子paths 了吗?或者还有其他方法可以让RouteReuseStrategy 使用包含子paths 的路径

【问题讨论】:

    标签: angular typescript angular2-routing


    【解决方案1】:

    我添加了一个解决方法,通过在自定义 RouteReuseStrategy 中修改我的检索函数,在使用 loadChildren 的路线上时从不检索分离的路线。

        retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
           if (!route.routeConfig) return null;
           if(route.routeConfig.loadChildren) return null;
           return this.handlers[route.routeConfig.path];
        }
    

    我不确定它是否是所有场景的完美解决方案,但在我的情况下它可以工作。

    【讨论】:

      【解决方案2】:

      Angular 路由器过于复杂,自定义策略延续了这一趋势。

      您的自定义策略使用route.routerConfig.path 作为存储路由的键。

      它为同一路径存储(覆盖)两条不同的路由person/:id

      1. /person/%23123456789%23/edit
      2. /person/%23123456789%23/view

      第一次查看路线已存储,第二次编辑,当您再次打开查看时,最后存储的路线已编辑,但需要查看。

      根据路由器意见,这条路由不兼容,它递归检查节点,发现ViewPersonComponentrouterConfigEditPersonComponentrouterConfig不一样,嘭!

      所以要么routerConfig.path 不能用作密钥,要么是路由器设计问题/限制。

      【讨论】:

      • 你能创建一个 plunker 来显示你在回答中解释的内容吗?
      • 其实我用了你的 plunkr。此信息仅在调试器中可用,只需添加断点进行存储和检索。您必须在 chrome 调试器中启用暂停异常选项才能查看路由比较失败的原因。
      • 我确实调试了代码,请求的路由就是存储的路由。
      • 是的,但正如我上面写的,存储的路线(视图)在检索之前被编辑路线覆盖,这两条路线将具有相同的路径,但并不意味着它们是相同的,试试在第一次存储和最后一次检索路径 person/:id 期间在调试器中扩展 routeConfig 属性。
      • @kemsky 您能否建议使用更好的密钥来避免子路由出现此问题?
      【解决方案3】:

      这是一种为策略类中的路由生成唯一键的方法。 我有类似的问题,但是一旦我开始生成唯一键,问题就消失了:

      private takeFullUrl(route: ActivatedRouteSnapshot) {
        let next = route;
        // Since navigation is usually relative
        // we go down to find out the child to be shown.
        while (next.firstChild) {
          next = next.firstChild;
        }
        const segments = [];
        // Then build a unique key-path by going to the root.
        while (next) {
          segments.push(next.url.join('/'));
          next = next.parent;
        }
        return compact(segments.reverse()).join('/');
      }
      

      更多关于https://github.com/angular/angular/issues/13869#issuecomment-344403045

      【讨论】:

      • 似乎是一种非常奇怪的方法来遍历层次结构以找到最深的孩子,然后将其从另一个方向遍历,然后反转线段数组。您可以在第一遍时生成它。
      • compact()函数显然不是canon。
      • @Thor84no 传递的路由可能不是导航树的根。你看出区别了吗?
      • 在这种情况下,while (next.parent) { next = next.parent; } 而不是 firstChild 会节省您反转段的时间,不是吗?
      • @Thor84no firstChild 用于获取叶子然后返回根以构建整个路径,如您所说的 next = next.parent。
      【解决方案4】:

      我遇到了类似的问题,修改我的唯一密钥方法解决了它。

      private routeToUrl(route: ActivatedRouteSnapshot): string {
          if (route.url) {
              if (route.url.length) {
                  return route.url.join('/');
              } else {
                  if (typeof route.component === 'function') {
                      return `[${route.component.name}]`;
                  } else if (typeof route.component === 'string') {
                      return `[${route.component}]`;
                  } else {
                      return `[null]`;
                  }
              }
          } else {
              return '(null)';
          }
      }
      
      
      private getChildRouteKeys(route:ActivatedRouteSnapshot): string {
          let  url = this.routeToUrl(route);
          return route.children.reduce((fin, cr) => fin += this.getChildRouteKeys(cr), url);
      }
      
      private getRouteKey(route: ActivatedRouteSnapshot) {
          let url = route.pathFromRoot.map(it => this.routeToUrl(it)).join('/') + '*';
          url += route.children.map(cr => this.getChildRouteKeys(cr));
          return url;
      }
      

      以前我只建立了第一个孩子,现在我只是递归地建立我的所有孩子的钥匙。 routeToUrl 函数不是我写的,是从前段时间读到的一篇关于自定义重用策略的文章中得到的,它没有被修改。

      【讨论】:

      • 这对我有用。包含组件名称似乎是我缺少的,以确保防止错误覆盖的唯一键。感谢分享!
      【解决方案5】:

      在我的情况下,我还需要在检索方法中检查 route.routeConfig.children:

      retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
         if (!route.routeConfig) return null;
         if (route.routeConfig.loadChildren || route.routeConfig.children ) return null;
         return this.handlers[route.routeConfig.path];
      }
      

      【讨论】:

        【解决方案6】:

        我刚刚在包含延迟加载模块的应用程序中解决了这个问题。最终,我不得不采用路由重用策略来解决,该策略保留路由组件一个模块中,而不是模块之间。

        import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
        
        export class CustomReuseStrategy implements RouteReuseStrategy {
        
          handlers: { [key: string]: DetachedRouteHandle } = {};
        
          calcKey(route: ActivatedRouteSnapshot) {
            return route.pathFromRoot
              .map(v => v.url.map(segment => segment.toString()).join('/'))
              .filter(url => !!url)
              .join('/');
          }
        
          shouldDetach(route: ActivatedRouteSnapshot): boolean {
            return true;
          }
        
          store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
            this.handlers[this.calcKey(route)] = handle;
          }
        
          shouldAttach(route: ActivatedRouteSnapshot): boolean {
            return !!route.routeConfig && !!this.handlers[this.calcKey(route)];
          }
        
          retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
            if (!route.routeConfig) { return null as any; }
            if (route.routeConfig.loadChildren) {
              Object.keys(this.handlers).forEach(key => delete this.handlers[key]);
              return null as any;
            }
            return this.handlers[this.calcKey(route)];
          }
        
          shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
            return this.calcKey(curr) === this.calcKey(future);
          }
        
        }
        

        【讨论】:

          【解决方案7】:

          如果同样的问题尝试了不同的解决方案,这对我有用:

          import { RouteReuseStrategy} from "@angular/router/";
          import { ActivatedRouteSnapshot, DetachedRouteHandle } from "@angular/router";
          
          interface RouteStorageObject {
            snapshot: ActivatedRouteSnapshot;
            handle: DetachedRouteHandle;
          }
          
          export class CacheRouteReuseStrategy implements RouteReuseStrategy {
            storedRouteHandles = new Map<string, DetachedRouteHandle>();
            allowRetriveCache = {};
            storedRoutes: { [key: string]: RouteStorageObject } = {};
          
            shouldReuseRoute( before: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot):boolean {
             return before.routeConfig === curr.routeConfig;
            }
          
            retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
              if (!route.routeConfig || !this.storedRoutes[this.getPath(route)] ) return null as any;
              if (route.routeConfig.loadChildren) {
                 Object.keys(this.storedRoutes).forEach(key => delete this.storedRoutes[key]);
                 return null as any;
              }
              return this.storedRoutes[this.getPath(route)].handle;
            }
          
            shouldAttach(route: ActivatedRouteSnapshot): boolean {
              let canAttach: boolean = !!route.routeConfig && !!this.storedRoutes[this.getPath(route)];
              if (canAttach) {
                 let paramsMatch: boolean = this.compareObjects(route.params, this.storedRoutes[this.getPath(route)].snapshot.params);
                 let queryParamsMatch: boolean = this.compareObjects(route.queryParams, this.storedRoutes[this.getPath(route)].snapshot.queryParams);
          
                 return paramsMatch && queryParamsMatch;
              } else {
                 return false;
              }
            }
          
            shouldDetach(route: ActivatedRouteSnapshot): boolean {
              return true
            }
          
            store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {
               let storedRoute: RouteStorageObject = {
                  snapshot: route,
                  handle: detachedTree
               };
               if ( detachedTree != null ){
                this.storedRoutes[this.getPath(route)] = storedRoute;
               }
            }
          
            private getPath(route: ActivatedRouteSnapshot): string {
                return route.pathFromRoot
                 .map(v => v.url.map(segment => segment.toString()).join('/'))
                 .filter(url => !!url)
                 .join('/');
            }
          
             private compareObjects(base: any, compare: any): boolean {
                // loop through all properties in base object
                for (let baseProperty in base) {
          
                  // determine if comparrison object has that property, if not: return false
                  if (compare.hasOwnProperty(baseProperty)) {
                  switch(typeof base[baseProperty]) {
                      // if one is object and other is not: return false
                      // if they are both objects, recursively call this comparison function
                      case 'object':
                          if ( typeof compare[baseProperty] !== 'object' || !this.compareObjects(base[baseProperty], compare[baseProperty]) ) { return false; } break;
                      // if one is function and other is not: return false
                      // if both are functions, compare function.toString() results
                      case 'function':
                          if ( typeof compare[baseProperty] !== 'function' || base[baseProperty].toString() !== compare[baseProperty].toString() ) { return false; } break;
                      // otherwise, see if they are equal using coercive comparison
                      default:
                          if ( base[baseProperty] != compare[baseProperty] ) { return false; }
                    }
                  } else {
                  return false;
                  }
              }
          
             // returns true only after false HAS NOT BEEN returned through all loops
             return true;
             }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-11-27
            • 1970-01-01
            • 1970-01-01
            • 2020-01-27
            • 2022-01-20
            • 2020-11-27
            相关资源
            最近更新 更多