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

问题描述 投票:75回答:4

我有一个Angular 2模块,我在其中实现了路由,并希望在导航时存储状态。用户应该能够:1。使用搜索公式搜索文档2.导航到其中一个结果3.导航回searchresult - 无需与服务器通信

这可能包括RouteReuseStrategy。问题是:如何实现不应存储文档?

那么应该存储路径路径“文档”的状态,并且不应存储路径路径“documents /:id”'状态?

javascript angular typescript angular-ui-router
4个回答
154
投票

嘿安德斯,好问题!

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

tl;博士

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

关于RouteReuseStrategy

通过提出这个问题,你已经明白RouteReuseStrategy允许你告诉Angular不要销毁一个组件,但实际上要保存它以便以后重新渲染。这很酷,因为它允许:

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

如果您希望暂时离开页面,即使用户已经输入了大量文本,那么最后一个也很重要。由于表单数量过多,企业应用程序会喜欢这个功能!

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

ALL

首先确保您的项目具有@ 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会问你,“你希望我们使用哪个组件?”,你将通过从DetachedRouteHandle返回该组件的retrieve来表明。

这几乎是你需要的所有逻辑!在下面的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的组件,而不是所有其他组件!

其他链接

虽然目前还没有太多的文档,但这里有几个链接到存在的内容:

Angular Docs: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


27
投票

不要被接受的答案吓倒,这非常简单。这里是您需要的快速答案。我建议至少阅读接受的答案,因为它充满了非常详细的信息。

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

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

10
投票

要在延迟加载的模块中使用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},
]}

更多信息here


4
投票

以下是工作!参考: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, '_')
    }
}

0
投票

除了接受的答案(由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;
}
© www.soinside.com 2019 - 2024. All rights reserved.