如何在Angular 2中针对特定路由实现RouteReuseStrategy shouldDetach?

170
我有一个Angular 2模块,在其中实现了路由,并希望在导航时保存状态。用户应该能够:
  1. 使用“搜索公式”搜索文档
  2. 导航到其中一个结果
  3. 返回“searchresult”-无需与服务器通信
这可以通过使用RouteReuseStrategy来实现。 问题是: 如何实现文档不被存储? 因此,路由路径“documents”的状态应该被存储而路由路径“documents/:id”的状态不应该被存储?
10个回答

298

嘿,安德斯,好问题!

我的用例与您几乎相同,并且想要做同样的事情!用户搜索 > 获取结果 > 用户导航到结果 > 用户导航回来 > BOOM 快速返回结果,但您不想存储用户导航到的特定结果。

tl;dr

您需要拥有一个实现RouteReuseStrategy的类,并在ngModule中提供您的策略。如果您想修改路由存储的时间,请修改shouldDetach函数。当它返回true时,Angular会存储该路由。如果您想修改路由何时被附加,请修改shouldAttach函数。当shouldAttach返回true时,Angular将使用存储的路由代替请求的路由。这是一个Plunker,让您随意尝试。

关于RouteReuseStrategy

通过提出这个问题,您已经了解到RouteReuseStrategy允许您告诉Angular 不要 销毁组件,而是在以后保存它以进行重新渲染。这很酷,因为它允许:
  • 减少服务器调用
  • 增加速度
  • 并且组件默认情况下以相同的状态呈现

如果您想暂时离开页面,即使用户已经输入了大量文本,最后一个功能非常重要。企业应用程序将喜欢这个功能,因为有太多的表格!

这是我想出来解决问题的方法。正如您所说,您需要利用@angular/router在3.4.1及更高版本中提供的RouteReuseStrategy

待办事项

首先确保您的项目具有@angular/router 3.4.1或更高版本。

接下来,创建一个文件,用于存放实现RouteReuseStrategy的类。我将其命名为reuse-strategy.ts并将其放置在/app文件夹中以备不时之需。目前,这个类应该长这样:
import { RouteReuseStrategy } from '@angular/router';

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(don't worry about your TypeScript errors, we're about to solve everything)
通过将类提供给您的app.module来完成基础工作。请注意,您尚未编写CustomReuseStrategy,但应该继续从reuse-strategy.ts导入它。还要导入{ RouteReuseStrategy } from '@angular/router';
@NgModule({
    [...],
    providers: [
        {provide: RouteReuseStrategy, useClass: CustomReuseStrategy}
    ]
)}
export class AppModule {
}

最后一步是编写控制路由是否被分离、存储、检索和重新连接的类。在进行旧的复制/粘贴之前,我将在此对机制进行简短的解释,因为我了解它们。请参考下面的代码以获取我描述的方法,并且当然,代码中有大量的文档

当你导航时,shouldReuseRoute 会被触发。这个方法对我来说有点奇怪,但如果它返回 true,那么实际上会重用你当前所在的路由,并且不会触发其他任何方法。如果用户正在离开,则只需返回 false。
如果 shouldReuseRoute 返回 false,则会触发 shouldDetachshouldDetach 确定是否要存储路由,并返回一个指示变量。在这里,您应该决定要存储/不存储路径,我会通过检查要存储的路径数组中是否存在 route.routeConfig.path 来进行决策,并且如果 path 在数组中不存在,则返回 false。
如果 shouldDetach 返回 true,则会触发 store,这是您存储有关路由的任何信息的机会。无论您做什么,都需要存储 DetachedRouteHandle,因为这是 Angular 用于稍后标识您存储的组件的东西。下面,我将 DetachedRouteHandleActivatedRouteSnapshot 都存储在类的本地变量中。
所以,我们已经看到了存储逻辑,但是如何导航到组件呢?Angular如何决定拦截您的导航并将存储的内容放在其位置上?
  1. 同样,在shouldReuseRoute返回false之后,运行shouldAttach,这是您确定是要重新生成还是使用内存中组件的机会。如果要重用存储的组件,则返回true,然后您就可以继续操作了!
  2. 现在Angular会问您:“您想使用哪个组件?”,您将通过从retrieve返回该组件的DetachedRouteHandle来指示。
这就是您需要的所有逻辑!在下面的reuse-strategy.ts代码中,我还为您留下了一个聪明的函数,可用于比较两个对象。我使用它来比较未来路由的route.paramsroute.queryParams与存储的路由的值是否匹配。如果这些都匹配,我希望使用存储的组件而不是生成新组件。但是如何实现取决于您!

reuse-strategy.ts

/**
 * 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 (===)
     * Another important note is that the method only tells you if `compare` has all equal parameters to `base`, not the other way around
     * @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;
    }
}

行为

该实现在路由器上存储用户访问的每个唯一路由,仅存储一次。在用户在网站上的会话期间,这将不断增加存储在内存中的组件。如果您想限制存储的路由,则应在shouldDetach方法中控制。它控制哪些路由应该保存。

示��

假设用户从主页搜索某物,导航到路径search/:term,其可能看起来像www.yourwebsite.com/search/thingsearchedfor。搜索页面包含一堆搜索结果。您希望存储此路由,以防他们想要回到它!现在,他们单击搜索结果,跳转到view/:resultId,您不希望存储该页面,因为他们可能只去那里一次。有了上述实现,我只需要更改shouldDetach方法!以下是它可能的样子:

首先让我们创建一个要存储的路径数组。

private acceptedRoutes: string[] = ["search/:term"];

现在,在shouldDetach中,我们可以检查route.routeConfig.path与我们的数组是否匹配。
shouldDetach(route: ActivatedRouteSnapshot): boolean {
    // check to see if the route's path is in our acceptedRoutes array
    if (this.acceptedRoutes.indexOf(route.routeConfig.path) > -1) {
        console.log("detaching", route);
        return true;
    } else {
        return false; // will be "view/:resultId" when user navigates to result
    }
}

因为Angular只会存储一个路由实例,所以这种存储方式将非常轻量级,我们只会存储位于“search/:term”的组件,而不是所有其他组件!
额外链接
虽然目前还没有太多的文档,但以下是一些现有的链接:
Angular文档:https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html 介绍文章:https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx

nativescript-angular的默认实现RouteReuseStrategyhttps://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts


2
@shaahin 我已经添加了一个示例,这是我当前实现中包含的确切代码! - Corbfon
1
@Corbfon 我也在官方的 Github 页面上开了一个问题:https://github.com/angular/angular/issues/13869 - Tjaart van der Walt
2
有没有办法在重新激活存储的路线时重新运行输入动画? - Jinder Sidhu
2
ReuseRouteStrategy会将您的组件交还给路由器,因此它将处于任何状态。如果您希望组件对附件做出反应,则可以使用提供“Observable”的服务。组件应在“ngOnInit”生命周期钩子期间订阅“Observable”。然后,您将能够从“ReuseRouteStrategy”告诉组件它刚刚被附加,并且组件可以根据需要修改其状态。 - Corbfon
1
@AndersGramMygind 如果我的回答解决了你提出的问题,你能否将其标记为答案? - Corbfon
显示剩余27条评论

60

不要被已接受的答案吓到,这很简单。这里是你需要的快速答案。我建议至少阅读已接受的答案,因为它充满了细节。

这个解决方案不像已接受的答案那样进行任何参数比较,但对于存储一组路由来说可以正常工作。

app.module.ts导入:

import { RouteReuseStrategy } from '@angular/router';
import { CustomReuseStrategy, Routing } from './shared/routing';

@NgModule({
//...
providers: [
    { provide: RouteReuseStrategy, useClass: CustomReuseStrategy },
  ]})

shared/routing.ts:

export class CustomReuseStrategy implements RouteReuseStrategy {
 routesToCache: string[] = ["dashboard"];
 storedRouteHandles = new Map<string, DetachedRouteHandle>();

 // Decides if the route should be stored
 shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return this.routesToCache.indexOf(route.routeConfig.path) > -1;
 }

 //Store the information for the route we're destructing
 store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    this.storedRouteHandles.set(route.routeConfig.path, handle);
 }

//Return true if we have a stored route object for the next route
 shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return this.storedRouteHandles.has(route.routeConfig.path);
 }

 //If we returned true in shouldAttach(), now return the actual route data for restoration
 retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    return this.storedRouteHandles.get(route.routeConfig.path);
 }

 //Reuse the route if we're going to and from the same route
 shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
 }
}

2
这对于懒加载的路由也适用吗? - bluePearl
4
routeConfig 为 null,因此对于不同的路由,shouldReuseRoute 将始终返回 true,这不是期望的行为。 - Gil Epshtain

42

除了 Corbfon 给出的被接受的答案和 Chris Fremgen 更简洁、更直接的解释之外,我想添加一种更灵活的处理需要使用重用策略的路由的方法。

这两个答案都将我们想要缓存的路由存储在一个数组中,并检查当前路由路径是否在数组中。这个检查是在 shouldDetach 方法中完成的。

我认为这种方法不够灵活,因为如果我们想要更改路由的名称,就需要记住在我们的 CustomReuseStrategy 类中同时更改路由名称。我们可能会忘记更改它,或者团队中的其他开发人员可能会决定更改路由名称,甚至不知道 RouteReuseStrategy 的存在。

与其将我们想要缓存的路由存储在数组中,我们可以直接在 RouterModule 中使用 data 对象标记它们。这样即使我们更改了路由名称,重用策略仍然会被应用。

{
  path: 'route-name-i-can-change',
  component: TestComponent,
  data: {
    reuseRoute: true
  }
}

然后在shouldDetach方法中,我们利用了它。

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}

2
好的解决方案。这应该真正被融入标准的Angular路由复用策略中,只需要像你所应用的那样加一个简单的标记即可。 - MIP1983
非常好的答案。非常感谢! - claudiomatiasrg
1
最终我找到了一个答案,可以在两分钟内实现,而不必深入研究Angular的原因和原理。哈哈。 - Zain Ul Abidin

28

另一个更有效、完整和可重用的实现。这个支持惰性加载模块,如 @Uğur Dinç,并集成 @Davor 路由数据标记。最好的改进是基于页面绝对路径自动生成(几乎)唯一标识符。这样你就不必在每个页面上自己定义它。

将要缓存的页面标记为 reuseRoute: true。它会在 shouldDetach 方法中使用。

{
  path: '',
  component: MyPageComponent,
  data: { reuseRoute: true },
}

这是最简单的策略实现方式,不需要比较查询参数。

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

export class CustomReuseStrategy implements RouteReuseStrategy {

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

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.data.reuseRoute || false;
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    const id = this.createIdentifier(route);
    if (route.data.reuseRoute) {
      this.storedHandles[id] = handle;
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    const id = this.createIdentifier(route);
    const handle = this.storedHandles[id];
    const canAttach = !!route.routeConfig && !!handle;
    return canAttach;
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    const id = this.createIdentifier(route);
    if (!route.routeConfig || !this.storedHandles[id]) return null;
    return this.storedHandles[id];
  }

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

  private createIdentifier(route: ActivatedRouteSnapshot) {
    // Build the complete path from the root to the input route
    const segments: UrlSegment[][] = route.pathFromRoot.map(r => r.url);
    const subpaths = ([] as UrlSegment[]).concat(...segments).map(segment => segment.path);
    // Result: ${route_depth}-${path}
    return segments.length + '-' + subpaths.join('/');
  }
}

这个也会比较查询参数。 compareObjects 相对于@Corbfon版本有一点改进:循环遍历基础对象和比较对象的属性。请记住,您可以使用外部更可靠的实现,例如 lodash 的 isEqual 方法。

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

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

export class CustomReuseStrategy implements RouteReuseStrategy {

  storedRoutes: { [key: string]: RouteStorageObject } = {};

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.data.reuseRoute || false;
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    const id = this.createIdentifier(route);
    if (route.data.reuseRoute && id.length > 0) {
      this.storedRoutes[id] = { handle, snapshot: route };
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    const id = this.createIdentifier(route);
    const storedObject = this.storedRoutes[id];
    const canAttach = !!route.routeConfig && !!storedObject;
    if (!canAttach) return false;

    const paramsMatch = this.compareObjects(route.params, storedObject.snapshot.params);
    const queryParamsMatch = this.compareObjects(route.queryParams, storedObject.snapshot.queryParams);

    console.log('deciding to attach...', route, 'does it match?');
    console.log('param comparison:', paramsMatch);
    console.log('query param comparison', queryParamsMatch);
    console.log(storedObject.snapshot, 'return: ', paramsMatch && queryParamsMatch);

    return paramsMatch && queryParamsMatch;
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    const id = this.createIdentifier(route);
    if (!route.routeConfig || !this.storedRoutes[id]) return null;
    return this.storedRoutes[id].handle;
  }

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

  private createIdentifier(route: ActivatedRouteSnapshot) {
    // Build the complete path from the root to the input route
    const segments: UrlSegment[][] = route.pathFromRoot.map(r => r.url);
    const subpaths = ([] as UrlSegment[]).concat(...segments).map(segment => segment.path);
    // Result: ${route_depth}-${path}
    return segments.length + '-' + subpaths.join('/');
  }

  private compareObjects(base: any, compare: any): boolean {

    // loop through all properties
    for (const baseProperty in { ...base, ...compare }) {

      // 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:
            // tslint:disable-next-line triple-equals
            if (base[baseProperty] != compare[baseProperty]) {
              return false;
            }
        }
      } else {
        return false;
      }
    }

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

如果您有更好的方法来生成唯一键,请在我的回答中进行评论,我将更新代码。

感谢所有分享解决方案的人。


4
这应该是被接受的答案。上面提供的许多解决方案都不能支持具有相同子URL的多个页面。因为它们正在比较activatedRoute URL,而这并不是完整路径。 - zhuhang.jasper
1
很棒的解决方案!只是提一下,如果你想在用户退出时删除存储的组件,你可以在 shouldAttach 钩子中做类似这样的事情:if (route.component === AuthComponent ){ this.storedHandles = {}; return false; } - juan_carlos_yl
如果我有以下配置:{ path: '', component: UsersComponent, children: [ { path: ':id', component: UserComponent, data: { reuseRoute: true } } ] } 在这种情况下,UserComponent 在场景 /user/1 -> /users -> /user/2 中被缓存,这是预期的,但它何时会被销毁?即使我试图从父级 UsersComponent 移动到 /users -> /something-else,该路由仍然处于活动状态。所以我想知道它何时会被销毁? - Aakash Goplani

18

要使用Chris Fremgen的策略与延迟加载模块,将CustomReuseStrategy类修改为以下内容:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
  routesToCache: string[] = ["company"];
  storedRouteHandles = new Map<string, DetachedRouteHandle>();

  // Decides if the route should be stored
  shouldDetach(route: ActivatedRouteSnapshot): boolean {
     return this.routesToCache.indexOf(route.data["key"]) > -1;
  }

  //Store the information for the route we're destructing
  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
     this.storedRouteHandles.set(route.data["key"], handle);
  }

  //Return true if we have a stored route object for the next route
  shouldAttach(route: ActivatedRouteSnapshot): boolean {
     return this.storedRouteHandles.has(route.data["key"]);
  }

  //If we returned true in shouldAttach(), now return the actual route data for restoration
  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
     return this.storedRouteHandles.get(route.data["key"]);
  }

  //Reuse the route if we're going to and from the same route
  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
     return future.routeConfig === curr.routeConfig;
  }
}

最后,在您的功能模块路由文件中定义您的键:

{ path: '', component: CompanyComponent, children: [
    {path: '', component: CompanyListComponent, data: {key: "company"}},
    {path: ':companyID', component: CompanyDetailComponent},
]}

更多信息请点击这里.


1
谢谢你添加这个功能!我得试一下。它甚至可能修复我的解决方案遇到的一些子路由处理问题。 - Corbfon
我不得不使用 route.data["key"] 才能避免错误。但问题是,我有一个路由+组件在两个不同的地方使用:1. sample/list/item2. product/id/sample/list/item。当我首次加载其中任何一个路径时,它都可以正常加载,但另一个会抛出重新附加错误,因为我是基于 list/item 进行存储的。所以我的解决方法是复制了该路由并对 URL 路径进行了一些更改,但显示相同的组件。不确定是否还有其他解决方法。 - bluePearl
这有点让我困惑,上面的方法行不通,一旦我进入我的缓存路由之一,它就会崩溃(它将不再导航,并且控制台中会出现错误)。Chris Fremgen的解决方案似乎在我的懒加载模块中可以正常工作,至少目前看来是这样的... - MIP1983

18

ANGULAR 13 (28/02/2022 版本)

经过阅读很多指南和建议,我可以解释一下:

首先,你必须理解什么是 future 和 curr。

例如:当你从 localhost/a 导航到 localhost/b 并且现在你在 b 页面。

情况 1:你想从 /a -> /b

  • shouldReuseRoute:false,因为future !== current
  • shouldDetach:true,因为我们将保存(detach)未来的任何内容到 store 并等待重用(attach)。
  • shouldRetrieve:true || false 检查 handler 如果是,则将保存的 future 组件附加以重用。如果不是,则什么都不做。(在这种情况下是 no)

情况 2:你想从 /b?id=1 -> /b?id=2

  • shouldReuseRoute:true,因为 future === current
  • shouldDetach:跳过
  • shouldRetrieve:跳过

情况 3:你想从 /b -> /a 返回

  • shouldReuseRoute:false,因为 future !== current
  • shouldDetach:true,因为我们将保存(detach)未来的任何内容到 store 并等待重用(attach)。
  • shouldRetrieve:true || false 检查 handler 如果是,则将保存的 future 组件附加以重用。如果不是,则什么都不做。(在这种情况下是 yes)

更简单的可视化说明请参见https://dev59.com/z6Hia4cB1Zd3GeqPMwXX#45788698

navigate to a
shouldReuseRoute->return true->do nothing

a->b
shouldReuseRoute()->return false->shouldDetach()->return true->store a

then b->a
shouldReuseRoute()->return false->shouldDetach()->return true->store b->retrieve() return a ->attach() a.

然后更加直观的来源于https://dev59.com/VcHqa4cB1Zd3GeqP896Y#69004775在这里输入图片描述

最后是来自Angular团队的正确代码:https://github.com/angular/angular/issues/44383

export class CustomRouteReuseStrategy implements RouteReuseStrategy { 
    private handlers: Map<Route, DetachedRouteHandle> = new Map();

    constructor() {}

    public shouldDetach(_route: ActivatedRouteSnapshot): boolean {
        return true;
    }

    public store(
        route: ActivatedRouteSnapshot,
        handle: DetachedRouteHandle
    ): void {
        if (!route.routeConfig) return;
        this.handlers.set(route.routeConfig, handle);
    }

    public shouldAttach(route: ActivatedRouteSnapshot): boolean {
        return !!route.routeConfig && !!this.handlers.get(route.routeConfig);
    }

    public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null {
        if (!route.routeConfig || !this.handlers.has(route.routeConfig)) return null;
        return this.handlers.get(route.routeConfig)!;
    }

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

谢谢。这是我迄今为止读过的关于RouterReuseStrategy的最清晰的解释。 - foresightyj

8

在我们的情况下,之前提到的所有解决方案都不太适用。我们的业务应用程序较小,包括以下内容:

  1. 介绍页面
  2. 登录页面
  3. 应用程序(登录后)

我们的要求:

  1. 惰性加载模块
  2. 多级路由
  3. 将所有路由器/组件状态存储在应用程序部分的内存中
  4. 在特定路线上使用默认的Angular重用策略的选项
  5. 在注销时销毁存储在内存中的所有组件

以下是我们路由的简化示例:

const routes: Routes = [{
    path: '',
    children: [
        {
            path: '',
            canActivate: [CanActivate],
            loadChildren: () => import('./modules/dashboard/dashboard.module').then(module => module.DashboardModule)
        },
        {
            path: 'companies',
            canActivate: [CanActivate],
            loadChildren: () => import('./modules/company/company.module').then(module => module.CompanyModule)
        }
    ]
},
{
    path: 'login',
    loadChildren: () => import('./modules/login/login.module').then(module => module.LoginModule),
    data: {
        defaultReuseStrategy: true, // Ignore our custom route strategy
        resetReuseStrategy: true // Logout redirect user to login and all data are destroyed
    }
}];

重用策略:

export class AppReuseStrategy implements RouteReuseStrategy {

private handles: Map<string, DetachedRouteHandle> = new Map();

// Asks if a snapshot from the current routing can be used for the future routing.
public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
}

// Asks if a snapshot for the current route already has been stored.
// Return true, if handles map contains the right snapshot and the router should re-attach this snapshot to the routing.
public shouldAttach(route: ActivatedRouteSnapshot): boolean {
    if (this.shouldResetReuseStrategy(route)) {
        this.deactivateAllHandles();
        return false;
    }

    if (this.shouldIgnoreReuseStrategy(route)) {
        return false;
    }

    return this.handles.has(this.getKey(route));
}

// Load the snapshot from storage. It's only called, if the shouldAttach-method returned true.
public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    return this.handles.get(this.getKey(route)) || null;
}

// Asks if the snapshot should be detached from the router.
// That means that the router will no longer handle this snapshot after it has been stored by calling the store-method.
public shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return !this.shouldIgnoreReuseStrategy(route);
}

// After the router has asked by using the shouldDetach-method and it returned true, the store-method is called (not immediately but some time later).
public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
    if (!handle) {
        return;
    }

    this.handles.set(this.getKey(route), handle);
}

private shouldResetReuseStrategy(route: ActivatedRouteSnapshot): boolean {
    let snapshot: ActivatedRouteSnapshot = route;

    while (snapshot.children && snapshot.children.length) {
        snapshot = snapshot.children[0];
    }

    return snapshot.data && snapshot.data.resetReuseStrategy;
}

private shouldIgnoreReuseStrategy(route: ActivatedRouteSnapshot): boolean {
    return route.data && route.data.defaultReuseStrategy;
}

private deactivateAllHandles(): void {
    this.handles.forEach((handle: DetachedRouteHandle) => this.destroyComponent(handle));
    this.handles.clear();
}

private destroyComponent(handle: DetachedRouteHandle): void {
    const componentRef: ComponentRef<any> = handle['componentRef'];

    if (componentRef) {
        componentRef.destroy();
    }
}

private getKey(route: ActivatedRouteSnapshot): string {
    return route.pathFromRoot
        .map((snapshot: ActivatedRouteSnapshot) => snapshot.routeConfig ? snapshot.routeConfig.path : '')
        .filter((path: string) => path.length > 0)
        .join('');
    }
}

3
以下是工作内容!参考:https://www.cnblogs.com/lovesangel/p/7853364.html

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

export class CustomReuseStrategy implements RouteReuseStrategy {

    public static handlers: { [key: string]: DetachedRouteHandle } = {}

    private static waitDelete: string

    public static deleteRouteSnapshot(name: string): void {
        if (CustomReuseStrategy.handlers[name]) {
            delete CustomReuseStrategy.handlers[name];
        } else {
            CustomReuseStrategy.waitDelete = name;
        }
    }
   
    public shouldDetach(route: ActivatedRouteSnapshot): boolean {
        return true;
    }

   
    public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        if (CustomReuseStrategy.waitDelete && CustomReuseStrategy.waitDelete == this.getRouteUrl(route)) {
            // 如果待删除是当前路由则不存储快照
            CustomReuseStrategy.waitDelete = null
            return;
        }
        CustomReuseStrategy.handlers[this.getRouteUrl(route)] = handle
    }

    
    public shouldAttach(route: ActivatedRouteSnapshot): boolean {
        return !!CustomReuseStrategy.handlers[this.getRouteUrl(route)]
    }

    /** 从缓存中获取快照,若无则返回nul */
    public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig) {
            return null
        }

        return CustomReuseStrategy.handlers[this.getRouteUrl(route)]
    }

   
    public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return future.routeConfig === curr.routeConfig &&
            JSON.stringify(future.params) === JSON.stringify(curr.params);
    }

    private getRouteUrl(route: ActivatedRouteSnapshot) {
        return route['_routerState'].url.replace(/\//g, '_')
    }
}


1
小心,这里使用了一个内部变量 _routerState - DarkNeuron
@DarkNeuron 的“_routerState”是否会造成任何危害? - k11k2
2
不,但是谷歌没有义务保留那个变量,因为它在内部使用,而不是在API中公开。 - DarkNeuron
1
我们什么时候调用 deleteRouteSnapshot 函数? - k11k2

1

我在实现自定义路由重用策略时遇到了以下问题:

  1. 对于一个路由的附加/分离进行操作:管理订阅、清理等;
  2. 仅保留最后一个参数化路由的状态:内存优化;
  3. 重复使用组件,而不是状态:用状态管理工具来管理状态。
  4. “无法重新附加从不同路由创建的 ActivatedRouteSnapshot” 错误。

因此,我编写了一个解决这些问题的库。该库提供了一个服务和装饰器,用于附加/分离钩子,并使用路由的组件来存储分离的路由,而不是路由的路径。

示例:

/* Usage with decorators */
@onAttach()
public onAttach(): void {
  // your code...
}

@onDetach()
public onDetach(): void {
  // your code...
}

/* Usage with a service */
public ngOnInit(): void {
  this.cacheRouteReuse
    .onAttach(HomeComponent) // or any route's component
    .subscribe(component => {
      // your code...
    });

  this.cacheRouteReuse
    .onDetach(HomeComponent) // or any route's component
    .subscribe(component => {
      // your code...
    });
}

这个库:https://www.npmjs.com/package/ng-cache-route-reuse


1
仅仅链接到自己的库或教程并不是一个好的回答。链接到它,解释为什么它可以解决问题,提供如何实现的代码,并声明你编写了它,这样会得到更好的回答。参见:什么是“好”的自我推广? - Paul Roub

0

以上所有答案都很好,但如果您有延迟加载路由器,尤其是嵌套路由器,则它们都无法正常工作。

为了克服这个问题,需要更改shouldReuseRoute和路径以比较路由:

Path A: abc/xyx/3
Path B: abc/rty/8

<abc>
 <router-outlet></router-outlet>
</abc>

/* If we move from pathA to pathB or vice versa, 
 * then  `routeConfig` will be same since we are routing within the same abc, 
 * but to store the handle properly, you should store fullPath as key.
*/

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


  private getPathFromRoot(route: ActivatedRouteSnapshot) {
    return (route["_urlSegment"]["segments"] as UrlSegment[])
      .map((seg) => seg.path)
      .join("/");
  }


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接