如何实现 RouteReuseStrategy shouldDetach for Angular 2 中的特定路由

我有一个Angular 2模块,我已经实现了路由,并希望在导航时存储状态。
用户应该能够:

  1. 使用“搜索公式”搜索文档
  2. 导航到其中一个结果
  3. 导航回“搜索结果” - 无需与服务器通信

这是可能的,包括.
问题是:
我如何实现文档不应该被存储?RouteReuseStrategy

那么路由路径“文档”的状态应该被存储,路由路径“documents/:id”的状态不应该被存储吗?


答案 1

嘿,安德斯,好问题!

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

tl;博士

您需要有一个类,用于在 中实现并提供您的策略。如果要修改路由的存储时间,请修改函数。当它返回时,Angular存储路线。如果要在附加路由时进行修改,请修改函数。当返回 true 时,Angular 将使用存储的路由来代替请求的路由。这是一个Plunker供您玩。RouteReuseStrategyngModuleshouldDetachtrueshouldAttachshouldAttach

关于RouteReuseStrategy

通过提出这个问题,您已经了解了RouteReuseStrategy允许您告诉Angular不要破坏组件,而是实际上保存它以供以后重新渲染。这很酷,因为它允许:

  • 减少服务器呼叫
  • 提高速度
  • 默认情况下,组件呈现的状态与它保持相同的状态

如果您想暂时离开页面,即使用户在其中输入了大量文本,最后一个也很重要。企业应用程序会喜欢这个功能,因为表单数量过多

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

待办事项

第一确保您的项目具有@angular/路由器版本 3.4.1 或更高版本。

接下来,创建一个文件,该文件将容纳实现 的类。我打电话给我的,把它放在文件夹中保管。现在,此类应如下所示:RouteReuseStrategyreuse-strategy.ts/app

import { RouteReuseStrategy } from '@angular/router';

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(不要担心你的TypeScript错误,我们即将解决所有问题)

通过将类提供给您的 .请注意,您尚未编写 ,但应该继续进行,并且一切相同。也app.moduleCustomReuseStrategyimportreuse-strategy.tsimport { RouteReuseStrategy } from '@angular/router';

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

最后一部分是编写类,该类将控制路由是否分离,存储,检索和重新附加。在我们进入旧的复制/粘贴之前,我将在这里对机制进行简短的解释,因为我对它们有所了解。参考下面的代码来了解我描述的方法,当然,代码中有很多文档。

  1. 导航时,会触发。这个对我来说有点奇怪,但是如果它返回,那么它实际上重用了您当前所在的路由,并且没有触发其他方法。如果用户正在导航离开,我只是返回 false。shouldReuseRoutetrue
  2. 如果返回,则触发。 确定是否要存储路由,并返回一个指示。这是您应该决定存储/不存储路径的地方,我会通过检查存储的路径数组来做到这一点,如果数组中不存在,则返回false。shouldReuseRoutefalseshouldDetachshouldDetachbooleanroute.routeConfig.pathpath
  3. 如果返回 ,则触发,这是您存储有关该路线的任何信息的机会。无论你做什么,你都需要存储,因为这是Angular稍后用来识别你存储的组件的。下面,我将 和 和 都存储到我的类的局部变量中。shouldDetachtruestoreDetachedRouteHandleDetachedRouteHandleActivatedRouteSnapshot

那么,我们已经看到了存储的逻辑,但是导航到组件呢?Angular如何决定拦截您的导航并将存储的导航放在其位置?

  1. 同样,在 返回 后运行,这是您确定是否要重新生成或使用内存中的组件的机会。如果你想重用存储的组件,请返回,你就可以顺利地前进了!shouldReuseRoutefalseshouldAttachtrue
  2. 现在,Angular 会问您,“您希望我们使用哪个组件?”,您将通过返回该组件来自 来指示该组件。DetachedRouteHandleretrieve

这几乎就是您需要的所有逻辑!在下面的代码中,我还为您提供了一个漂亮的函数,它将比较两个对象。我用它来比较未来路线和存储的路线。如果这些都匹配,我想使用存储的组件,而不是生成一个新的组件。但是你如何做到这一点取决于你!reuse-strategy.tsroute.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/:termwww.yourwebsite.com/search/thingsearchedforview/:resultIdshouldDetach

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

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

现在,我们可以对照我们的数组检查。shouldDetachroute.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

其他链接

虽然目前还没有太多的文档,但这里有一些链接指向现有的文档:

角度文档: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 的默认 RouteReuseStrategy 实现:https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts


答案 2

不要被接受的答案吓倒,这很简单。以下是您需要的快速答案。我建议至少阅读公认的答案,因为它充满了非常详细的内容。

此解决方案不会像接受的答案那样进行任何参数比较,但它可以很好地存储一组路由。

app.module.ts import:

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;
 }
}