Comment implémenter RouteReuseStrategy shouldDetach pour des routes spécifiques dans Angular 2

115

J'ai un module Angular 2 dans lequel j'ai implémenté le routage et j'aimerais que les états soient stockés lors de la navigation.
L'utilisateur doit être capable de:

  1. rechercher des documents à l'aide d'une «formule de recherche»
  2. accédez à l'un des résultats
  3. revenir à 'searchresult' - sans communiquer avec le serveur

Ceci est possible notamment RouteReuseStrategy.
La question est:
comment mettre en œuvre le fait que le document ne doit pas être stocké?

Ainsi, l'état du chemin de route "documents" doit être stocké et l'état du chemin "documents /: id" "ne doit PAS être stocké?

Anders Gram Mygind
la source

Réponses:

209

Salut Anders, excellente question!

J'ai presque le même cas d'utilisation que vous et je voulais faire la même chose! Recherche utilisateur> obtenir les résultats> L'utilisateur accède au résultat> L'utilisateur revient en arrière> BOOM retourne rapidement aux résultats , mais vous ne voulez pas stocker le résultat spécifique vers lequel l'utilisateur a navigué.

tl; dr

Vous devez avoir une classe qui implémente RouteReuseStrategyet fournit votre stratégie dans le ngModule. Si vous souhaitez modifier le moment où l'itinéraire est enregistré, modifiez la shouldDetachfonction. Lorsqu'il revient true, Angular stocke l'itinéraire. Si vous souhaitez modifier le moment où l'itinéraire est attaché, modifiez la shouldAttachfonction. Lorsque le shouldAttachretour est vrai, Angular utilisera l'itinéraire stocké à la place de l'itinéraire demandé. Voici un Plunker avec lequel vous pouvez jouer.

À propos de RouteReuseStrategy

En ayant posé cette question, vous comprenez déjà que RouteReuseStrategy vous permet de dire à Angular de ne pas détruire un composant, mais en fait de le sauvegarder pour un re-rendu à une date ultérieure. C'est cool car cela permet:

  • Diminution des appels au serveur
  • Augmentation de la vitesse
  • ET le composant restitue, par défaut, dans le même état dans lequel il a été laissé

Ce dernier est important si vous souhaitez, par exemple, quitter une page temporairement même si l'utilisateur y a entré beaucoup de texte. Les applications d'entreprise adoreront cette fonctionnalité en raison du nombre excessif de formulaires!

C'est ce que j'ai proposé pour résoudre le problème. Comme vous l'avez dit, vous devez utiliser l' RouteReuseStrategyoffre de @ angular / router dans les versions 3.4.1 et supérieures.

FAIRE

Assurez-vous d' abord que votre projet a @ angular / router version 3.4.1 ou supérieure.

Ensuite , créez un fichier qui hébergera votre classe qui implémente RouteReuseStrategy. J'ai appelé le mien reuse-strategy.tset l' ai placé dans le /appdossier pour le garder en lieu sûr. Pour l'instant, cette classe devrait ressembler à:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(ne vous inquiétez pas pour vos erreurs TypeScript, nous sommes sur le point de tout résoudre)

Terminez le travail de base en fournissant la classe à votre app.module. Notez que vous n'avez pas encore écrit CustomReuseStrategy, mais que vous devriez continuer et importcela de reuse-strategy.tstout de même. Aussiimport { RouteReuseStrategy } from '@angular/router';

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

Le dernier élément est l'écriture de la classe qui contrôlera si les routes sont ou non détachées, stockées, récupérées et rattachées. Avant d'arriver à l'ancien copier / coller , je vais faire une brève explication des mécanismes ici, tels que je les comprends. Référencez le code ci-dessous pour les méthodes que je décris, et bien sûr, il y a beaucoup de documentation dans le code .

  1. Lorsque vous naviguez, se shouldReuseRoutedéclenche. Celui-ci est un peu étrange pour moi, mais s'il revient true, il réutilise en fait l'itinéraire sur lequel vous êtes actuellement et aucune des autres méthodes n'est déclenchée. Je renvoie simplement false si l'utilisateur s'éloigne.
  2. Si shouldReuseRouterevient false, se shouldDetachdéclenche. shouldDetachdétermine si vous souhaitez ou non stocker l'itinéraire et renvoie un booleanindiquant autant. C'est là que vous devez décider de stocker / ne pas stocker les chemins , ce que je ferais en vérifiant un tableau de chemins que vous souhaitez stocker route.routeConfig.path, et en retournant false si le pathn'existe pas dans le tableau.
  3. Si shouldDetachretourne true, storeest déclenché, ce qui est une opportunité pour vous de stocker toutes les informations que vous souhaitez sur l'itinéraire. Quoi que vous fassiez, vous devrez stocker le DetachedRouteHandlecar c'est ce que Angular utilise pour identifier votre composant stocké plus tard. Ci-dessous, je stocke le DetachedRouteHandleet le ActivatedRouteSnapshotdans une variable locale de ma classe.

Nous avons donc vu la logique du stockage, mais qu'en est-il de la navigation vers un composant? Comment Angular décide-t-il d'intercepter votre navigation et de remettre celle stockée à sa place?

  1. Encore une fois, après le shouldReuseRouteretour false, shouldAttachs'exécute, ce qui est votre chance de déterminer si vous souhaitez régénérer ou utiliser le composant en mémoire. Si vous souhaitez réutiliser un composant stocké, revenez trueet vous êtes sur la bonne voie!
  2. Maintenant angulaire vous demander: « quel composant vous nous souhaitez utiliser? », Que vous indiquerez en retournant c'est composant à DetachedRouteHandlepartir retrieve.

C'est à peu près toute la logique dont vous avez besoin! Dans le code reuse-strategy.tsci-dessous, je vous ai également laissé une fonction astucieuse qui comparera deux objets. Je l'utilise pour comparer les futurs itinéraires route.paramset ceux route.queryParamsenregistrés. Si tout cela correspond, je souhaite utiliser le composant stocké au lieu d'en générer un nouveau. Mais comment vous le faites dépend de vous!

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

Comportement

Cette implémentation stocke chaque route unique que l'utilisateur visite sur le routeur exactement une fois. Cela continuera à s'ajouter aux composants stockés en mémoire tout au long de la session de l'utilisateur sur le site. Si vous souhaitez limiter les itinéraires que vous stockez, l'endroit où le faire est la shouldDetachméthode. Il contrôle les itinéraires que vous enregistrez.

Exemple

Supposons que votre utilisateur recherche quelque chose sur la page d'accueil, ce qui le dirige vers le chemin search/:term, qui peut ressembler à www.yourwebsite.com/search/thingsearchedfor. La page de recherche contient un tas de résultats de recherche. Vous souhaitez enregistrer cet itinéraire, au cas où ils voudraient y revenir! Maintenant, ils cliquent sur un résultat de recherche et accèdent à view/:resultIdce que vous ne voulez pas stocker, car ils ne seront probablement là qu'une seule fois. Avec l'implémentation ci-dessus en place, je changerais simplement la shouldDetachméthode! Voici à quoi cela pourrait ressembler:

Tout d'abord , créons un tableau de chemins que nous voulons stocker.

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

maintenant, shouldDetachnous pouvons vérifier le route.routeConfig.pathcontre notre tableau.

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

Comme Angular ne stockera qu'une seule instance d'une route, ce stockage sera léger, et nous ne stockerons que le composant situé sur search/:termet pas tous les autres!

Liens supplémentaires

Bien qu'il n'y ait pas encore beaucoup de documentation, voici quelques liens vers ce qui existe:

Docs angulaires: https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html

Article d'introduction: https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx

Implémentation par défaut de RouteReuseStrategy par nativescript-angular : https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts

Corbfon
la source
2
@shaahin J'ai ajouté un exemple, qui est le code exact contenu dans mon implémentation actuelle!
Corbfon
1
@Corbfon J'ai également ouvert un numéro sur la page officielle de github: github.com/angular/angular/issues/13869
Tjaart van der Walt
2
Existe-t-il un moyen de le faire réexécuter des animations d'entrée lors de la réactivation d'un itinéraire stocké?
Jinder Sidhu
2
Le ReuseRouteStrategy remettra votre composant au routeur, il sera donc dans l'état dans lequel il a été laissé. Si vous souhaitez que le ou les composants réagissent à la pièce jointe, vous pouvez utiliser un service qui offre un Observable. Le composant doit s'abonner au hook Observablependant le ngOnInitcycle de vie. Ensuite, vous pourrez dire au composant, à partir de ReuseRouteStrategy, qu'il vient d'être attaché et le composant peut modifier son état comme il convient.
Corbfon
1
@AndersGramMygind si ma réponse fournit une réponse à la question que vous avez proposée, la marqueriez-vous comme réponse?
Corbfon
45

Ne soyez pas intimidé par la réponse acceptée, c'est assez simple. Voici une réponse rapide à ce dont vous avez besoin. Je recommanderais au moins de lire la réponse acceptée, car elle est pleine de détails.

Cette solution ne fait aucune comparaison de paramètres comme la réponse acceptée mais elle fonctionnera bien pour stocker un ensemble d'itinéraires.

app.module.ts importe:

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;
 }
}
Chris Fremgen
la source
1
Cela fonctionnera-t-il également pour les itinéraires chargés paresseusement?
bluePearl
@bluePearl Vérifiez la réponse ci
Chris Fremgen
2
routeConfig est nul, pour différentes routes, donc shouldReuseRoute retournera toujours true, ce qui n'est pas le comportement souhaité
Gil Epshtain
19

En plus de la réponse acceptée (par Corbfon) et de l'explication plus courte et plus simple de Chris Fremgen, je souhaite ajouter une manière plus flexible de gérer les routes qui devrait utiliser la stratégie de réutilisation.

Les deux réponses stockent les routes que nous voulons mettre en cache dans un tableau, puis vérifient si le chemin actuel de la route est dans le tableau ou non. Cette vérification se fait en shouldDetachméthode.

Je trouve cette approche inflexible car si nous voulons changer le nom de la route, nous devons nous rappeler de changer également le nom de la route dans notre CustomReuseStrategyclasse. Nous pouvons soit oublier de le changer, soit un autre développeur de notre équipe peut décider de changer le nom de la route sans même savoir l'existence de RouteReuseStrategy.

Au lieu de stocker les routes que nous voulons mettre en cache dans un tableau, nous pouvons les marquer directement en RouterModuleutilisant dataobject. De cette façon, même si nous modifions le nom de la route, la stratégie de réutilisation serait toujours appliquée.

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

Et puis dans la shouldDetachméthode, nous en faisons usage.

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}
Davor
la source
1
Bonne solution. Cela devrait vraiment être intégré dans la stratégie de réutilisation de route angulaire standard avec un simple indicateur comme vous l'avez appliqué.
MIP1983 le
Très bonne réponse. Merci beaucoup!
claudiomatiasrg
14

Pour utiliser la stratégie de Chris Fremgen avec des modules chargés paresseusement, modifiez la classe CustomReuseStrategy comme suit:

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

enfin, dans les fichiers de routage de vos modules de fonctionnalités, définissez vos clés:

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

Plus d'infos ici .

Uğur Dinç
la source
1
Merci d'avoir ajouté ceci! Je dois essayer. Cela pourrait même résoudre certains des problèmes de gestion des itinéraires enfants que ma solution rencontre.
Corbfon
J'ai dû utiliser route.data["key"]pour construire sans erreur. Mais le problème que je rencontre est que j'ai un composant route + qui est utilisé à deux endroits différents. 1. sample/list/itemet 2. product/id/sample/list/itemquand je charge pour la première fois l'un des chemins, il se charge bien mais l'autre jette l'erreur de rattachement parce que je stocke en fonction de list/itemDonc mon travail consiste à dupliquer l'itinéraire et à modifier le chemin de l'url mais en affichant le même composant. Je ne sais pas s'il existe une autre solution pour cela.
bluePearl
Ce genre de confusion m'a confus, ce qui précède ne fonctionnerait tout simplement pas, il exploserait dès que je frapperais l'une de mes routes de cache, (il ne naviguerait plus et là où des erreurs dans la console). La solution de Chris Fremgen semble fonctionner correctement avec mes modules paresseux pour autant que je
sache
12

Une autre implémentation plus valide, complète et réutilisable. Celui-ci prend en charge les modules chargés paresseusement comme @ Uğur Dinç et intègre l'indicateur de données de route @Davor. La meilleure amélioration est la génération automatique d'un identifiant (presque) unique basé sur le chemin absolu de la page. De cette façon, vous n'avez pas à le définir vous-même sur chaque page.

Marquez toutes les pages que vous souhaitez mettre en cache reuseRoute: true. Il sera utilisé dans la shouldDetachméthode.

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

Celui-ci est la mise en œuvre de stratégie la plus simple, sans comparaison des paramètres de requête.

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('/');
  }
}

Celui-ci compare également les paramètres de requête. compareObjectsa une petite amélioration par rapport à la version @Corbfon: boucle à travers les propriétés des objets de base et de comparaison. N'oubliez pas que vous pouvez utiliser une implémentation externe et plus fiable comme la isEqualméthode lodash .

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

Si vous avez un meilleur moyen de générer des clés uniques commentez ma réponse, je mettrai à jour le code.

Merci à tous les gars qui ont partagé leur solution.

McGiogen
la source
3
Cela devrait être la réponse acceptée. De nombreuses solutions fournies ci-dessus ne peuvent pas prendre en charge plusieurs pages avec la même URL enfant. Parce qu'ils comparent l'URL enabledRoute, qui n'est pas le chemin complet.
zhuhang.jasper
4

Toutes les solutions mentionnées étaient en quelque sorte insuffisantes dans notre cas. Nous avons une application professionnelle plus petite avec:

  1. Page d'introduction
  2. Page de connexion
  3. App (après connexion)

Nos exigences:

  1. Modules chargés paresseusement
  2. Itinéraires à plusieurs niveaux
  3. Stocker tous les états du routeur / composant en mémoire dans la section de l'application
  4. Possibilité d'utiliser la stratégie de réutilisation angulaire par défaut sur des itinéraires spécifiques
  5. Détruire tous les composants stockés en mémoire lors de la déconnexion

Exemple simplifié de nos itinéraires:

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

Stratégie de réutilisation:

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('');
    }
}
hovado
la source
3

ce qui suit est du travail! référence: 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, '_')
    }
}

红兵 伍
la source
1
Attention, cela utilise une variable interne _routerState.
DarkNeuron
@DarkNeuron _routerStatecause- t-il des dommages?
k11k2
2
Non, mais Google n'a aucune obligation de conserver cette variable, car elle est utilisée en interne et non exposée dans l'API.
DarkNeuron
quand nous appelons deleteRouteSnapshot?
k11k2
0

J'ai été confronté à ces problèmes en mettant en œuvre une stratégie de réutilisation d'itinéraire personnalisée:

  1. Effectuer des opérations sur une route attach / dettach: gestion des abonnements, nettoyage, etc .;
  2. Conserver uniquement l'état de la dernière route paramétrée: optimisation de la mémoire;
  3. Réutiliser un composant, pas un état: gérez l'état avec des outils de gestion d'état.
  4. Erreur "Impossible de rattacher ActivatedRouteSnapshot créé à partir d'une route différente";

J'ai donc écrit une bibliothèque pour résoudre ces problèmes. La bibliothèque fournit un service et des décorateurs pour attacher / détacher des crochets et utilise les composants d'une route pour stocker les routes détachées, pas les chemins d'une route.

Exemple:

/* 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...
    });
}

La bibliothèque: https://www.npmjs.com/package/ng-cache-route-reuse

Stas Amasev
la source
Le simple fait de créer un lien vers votre propre bibliothèque ou tutoriel n'est pas une bonne réponse. Le lien, en expliquant pourquoi il résout le problème, en fournissant du code sur la façon de le faire et en refusant que vous l'ayez écrit, c'est une meilleure réponse. Voir: Que signifie une «bonne» auto-promotion?
Paul Roub le