如何在 Angular 2 中为特定路由实现 RouteReuseStrategy shouldDetach

新手上路,请多包涵

我有一个 Angular 2 模块,我在其中实现了路由,并希望在导航时存储状态。

用户应该能够:

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

这可能包括 RouteReuseStrategy

问题是:

如何实现不应存储文档?

所以应该存储路由路径“documents”的状态,而不应该存储路由路径“documents/:id”的状态?

原文由 Anders Gram Mygind 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 525
2 个回答

嘿安德斯,好问题!

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

tl;博士

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

关于 RouteReuseStrategy

通过问这个问题,您已经了解 RouteReuseStrategy 允许您告诉 Angular 不要 销毁组件,但实际上是保存它以备日后重新渲染。这很酷,因为它允许:

  • 减少 服务器调用
  • 提高 速度
  • 并且 默认情况下,组件以与离开时相同的状态呈现

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

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

去做

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

接下来,创建一个文件,该文件将包含实现 RouteReuseStrategy 的类。我调用了我的 reuse-strategy.ts 并将其放在 /app 文件夹中以便妥善保管。现在,这个类应该是这样的:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
}

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

通过为您的 app.module 提供课程来 完成基础 工作。请注意,您尚未编写 CustomReuseStrategy ,但应该继续编写 --- import 它来自 reuse-strategy.ts 都是一样的。还有 import { RouteReuseStrategy } from '@angular/router';

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

最后一部分 是编写类,该类将控制路由是否分离、存储、检索和重新附加。在我们开始旧的 复制/粘贴 之前,我将在这里根据我的理解对机制做一个简短的解释。我所描述的方法可以参考下面的代码,当然, 代码中 有很多文档。

  1. 当您导航时, shouldReuseRoute 触发。这对我来说有点奇怪,但如果它返回 true ,那么它实际上会重用您当前所在的路线,并且不会触发其他任何方法。如果用户离开,我只返回 false。
  2. 如果 shouldReuseRoute 返回 falseshouldDetach 触发。 shouldDetach 判断是否要存储路由,返回一个 boolean 表示。 这是你应该决定存储/不存储路径 的地方,我会通过检查你 想要 存储的路径数组 route.routeConfig.path ,如果 path 不返回 false存在于数组中。
  3. 如果 shouldDetach 返回 truestore 被解雇,这是你存储任何你想要的关于路线的信息的机会。无论你做什么,你都需要存储 DetachedRouteHandle 因为 Angular 稍后会用它来识别你存储的组件。下面,我将 DetachedRouteHandleActivatedRouteSnapshot 存储到我班级的本地变量中。

所以,我们已经看到了存储逻辑,但是如何导航 一个组件呢? Angular 是如何决定拦截你的导航并将存储的导航放到它的位置的?

  1. 同样,在 shouldReuseRoute 返回 false 之后, shouldAttach 运行,这是您确定是要重新生成内存还是使用组件内存的机会。如果您想重用存储的组件,请返回 true 就可以了!
  2. 现在 Angular 会问您,“您希望我们使用哪个组件?”,您将通过从 retrieve 返回该组件的 DetachedRouteHandle 来表明这一点。

这几乎就是您需要的所有逻辑!在下面 reuse-strategy.ts 的代码中,我还为您留下了一个比较两个对象的漂亮函数。我用它来比较未来路线的 route.paramsroute.queryParams 与存储的路线。如果这些都匹配,我想使用存储的组件而不是生成一个新组件。但是你怎么做 取决于你!

重用-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 的组件,而不是所有其他组件!

附加链接

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

角度文档: 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

原文由 Corbfon 发布,翻译遵循 CC BY-SA 4.0 许可协议

不要被接受的答案吓倒,这非常简单。这是您需要的快速答案。我建议至少阅读已接受的答案,因为它包含很多细节。

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

app.module.ts 进口:

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

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

共享/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;
 }
}

原文由 Chris Fremgen 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏