Cómo implementar RouteReuseStrategy shouldDetach para rutas específicas en Angular 2

115

Tengo un módulo Angular 2 en el que he implementado el enrutamiento y me gustaría que los estados se almacenan al navegar.
El usuario debería poder:

  1. buscar documentos usando una 'fórmula de búsqueda'
  2. navegar a uno de los resultados
  3. navegar de nuevo a 'searchresult' - sin comunicarse con el servidor

Esto es posible incluso RouteReuseStrategy.
La pregunta es:
¿Cómo implemento que el documento no deba almacenarse?

Entonces, ¿el estado de la ruta de la ruta "documentos" debe almacenarse y el estado de la ruta de la ruta "documentos /: id" NO debe almacenarse?

Anders Gram Mygind
fuente

Respuestas:

209

¡Hola Anders, gran pregunta!

¡Tengo casi el mismo caso de uso que tú y quería hacer lo mismo! Búsqueda de usuario> obtener resultados> El usuario navega al resultado> El usuario navega hacia atrás> BOOM regresa rápidamente a los resultados , pero no desea almacenar el resultado específico al que navegó el usuario.

tl; dr

Necesita tener una clase que implemente RouteReuseStrategyy proporcione su estrategia en ngModule. Si desea modificar cuándo se almacena la ruta, modifique la shouldDetachfunción. Cuando regresa true, Angular almacena la ruta. Si desea modificar cuándo se adjunta la ruta, modifique la shouldAttachfunción. Cuando shouldAttachdevuelve verdadero, Angular usará la ruta almacenada en lugar de la ruta solicitada. Aquí tienes un Plunker para que juegues.

Acerca de RouteReuseStrategy

Al haber hecho esta pregunta, ya comprende que RouteReuseStrategy le permite decirle a Angular que no destruya un componente, sino que lo guarde para volver a renderizarlo en una fecha posterior. Eso es genial porque permite:

  • Disminución de llamadas al servidor
  • Mayor velocidad
  • Y el componente se renderiza, de forma predeterminada, en el mismo estado en el que se dejó

Este último es importante si desea, por ejemplo, dejar una página temporalmente aunque el usuario haya ingresado mucho texto en ella. ¡A las aplicaciones empresariales les encantará esta función debido a la cantidad excesiva de formularios!

Esto es lo que se me ocurrió para resolver el problema. Como dijiste, debes hacer uso del RouteReuseStrategyofrecido por @ angular / enrutador en las versiones 3.4.1 y superiores.

QUE HACER

Primero, asegúrese de que su proyecto tenga @ angular / enrutador versión 3.4.1 o superior.

A continuación , cree un archivo que albergará su clase que implementa RouteReuseStrategy. Llamé al mío reuse-strategy.tsy lo guardé en la /appcarpeta para guardarlo. Por ahora, esta clase debería verse así:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(no se preocupe por sus errores de TypeScript, estamos a punto de resolverlo todo)

Termine el trabajo de base proporcionando la clase a su app.module. Tenga en cuenta que aún no ha escrito CustomReuseStrategy, pero debe seguir adelante y importde reuse-strategy.tstodos modos. tambiénimport { RouteReuseStrategy } from '@angular/router';

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

La pieza final es escribir la clase que controlará si las rutas se separan, almacenan, recuperan y vuelven a unir. Antes de llegar al antiguo copiar / pegar , haré una breve explicación de la mecánica aquí, según tengo entendido. Consulte el código a continuación para ver los métodos que estoy describiendo y, por supuesto, hay mucha documentación en el código .

  1. Cuando navegas, shouldReuseRoutedispara. Este es un poco extraño para mí, pero si regresa true, entonces en realidad reutiliza la ruta en la que se encuentra actualmente y no se activa ninguno de los otros métodos. Solo devuelvo falso si el usuario está navegando.
  2. Si shouldReuseRouteregresa false, shouldDetachdispara. shouldDetachdetermina si desea o no almacenar la ruta, y devuelve un booleanindicando tanto. Aquí es donde debe decidir almacenar / no almacenar rutas , lo que haría al verificar una matriz de rutas que desea almacenar route.routeConfig.pathy devolver falso si pathno existe en la matriz.
  3. Si shouldDetachregresa true, storese despide, lo cual es una oportunidad para que usted almacene cualquier información que desee sobre la ruta. Hagas lo que hagas, necesitarás almacenar el DetachedRouteHandleporque eso es lo que Angular usa para identificar tu componente almacenado más adelante. A continuación, almaceno tanto the DetachedRouteHandlecomo the ActivatedRouteSnapshoten una variable local de mi clase.

Entonces, hemos visto la lógica del almacenamiento, pero ¿qué pasa con la navegación a un componente? ¿Cómo decide Angular interceptar su navegación y colocar la almacenada en su lugar?

  1. Una vez más, después de que shouldReuseRoutehaya regresado false, se shouldAttachejecuta, que es su oportunidad de averiguar si desea regenerar o usar el componente en la memoria. Si desea reutilizar un componente almacenado, regrese truey estará bien encaminado.
  2. Ahora angular le preguntará, "qué componente es lo que desea que utilicemos?", Que se le indicará mediante la devolución de que los componentes DetachedRouteHandlede retrieve.

¡Esa es prácticamente toda la lógica que necesitas! En el código de reuse-strategy.ts, a continuación, también te dejé una función ingeniosa que comparará dos objetos. Lo uso para comparar la ruta futura route.paramsy route.queryParamsla almacenada. Si todos coinciden, quiero usar el componente almacenado en lugar de generar uno nuevo. ¡Pero cómo lo hagas depende de ti!

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

Comportamiento

Esta implementación almacena cada ruta única que el usuario visita en el enrutador exactamente una vez. Esto continuará agregándose a los componentes almacenados en la memoria durante la sesión del usuario en el sitio. Si desea limitar las rutas que almacena, el lugar para hacerlo es el shouldDetachmétodo. Controla qué rutas guarda.

Ejemplo

Supongamos que su usuario busca algo en la página de inicio, lo que lo lleva a la ruta search/:term, que podría aparecer como www.yourwebsite.com/search/thingsearchedfor. La página de búsqueda contiene una gran cantidad de resultados de búsqueda. ¡Le gustaría almacenar esta ruta, en caso de que quieran volver a ella! Ahora hacen clic en un resultado de búsqueda y se navega hasta el view/:resultIdque no desea almacenar, ya que probablemente solo estarán allí una vez. Con la implementación anterior en su lugar, ¡simplemente cambiaría el shouldDetachmétodo! Así es como podría verse:

En primer lugar , hagamos una serie de rutas que queremos almacenar.

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

ahora, shouldDetachpodemos comparar el route.routeConfig.pathcon nuestra matriz.

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

Debido a que Angular solo almacenará una instancia de una ruta, este almacenamiento será liviano y solo almacenaremos el componente ubicado en search/:termy no todos los demás.

Enlaces adicionales

Aunque todavía no hay mucha documentación, aquí hay un par de enlaces a lo que existe:

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

Artículo de introducción: https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx

Implementación predeterminada de nativescript-angular de RouteReuseStrategy : https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts

Corbfon
fuente
2
@shaahin He agregado un ejemplo, ¡que es el código exacto contenido en mi implementación actual!
Corbfon
1
@Corbfon También abrí un problema en la página oficial de github: github.com/angular/angular/issues/13869
Tjaart van der Walt
2
¿Hay alguna forma de hacer que vuelva a ejecutar las animaciones de entrada al reactivar una ruta almacenada?
Jinder Sidhu
2
ReuseRouteStrategy devolverá su componente al enrutador, por lo que estará en el estado en el que se dejó. Si desea que los componentes reaccionen al adjunto, puede usar un servicio que ofrezca un archivo Observable. El componente debe suscribirse al enlace Observabledurante el ngOnInitciclo de vida. Entonces podrá decirle al componente, desde el ReuseRouteStrategy, que se acaba de adjuntar y que el componente puede modificar su estado según se ajuste.
Corbfon
1
@AndersGramMygind si mi respuesta proporciona una respuesta a la pregunta que propuso, ¿la marcaría como la respuesta?
Corbfon
45

No se deje intimidar por la respuesta aceptada, esto es bastante sencillo. Aquí tienes una respuesta rápida a lo que necesitas. Recomendaría leer al menos la respuesta aceptada, ya que está llena de gran detalle.

Esta solución no hace ninguna comparación de parámetros como la respuesta aceptada, pero funcionará bien para almacenar un conjunto de rutas.

importaciones de 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;
 }
}
Chris Fremgen
fuente
1
¿Funcionará esto también para rutas con carga diferida?
bluePearl
@bluePearl Compruebe la respuesta a continuación
Chris Fremgen
2
routeConfig es nulo, para diferentes rutas, por lo tanto, shouldReuseRoute siempre devolverá verdadero, que no es el comportamiento deseado
Gil Epshtain
19

Además de la respuesta aceptada (por Corbfon) y la explicación más corta y sencilla de Chris Fremgen, quiero agregar una forma más flexible de manejar rutas que deberían usar la estrategia de reutilización.

Ambas respuestas almacenan las rutas que queremos almacenar en caché en una matriz y luego verifican si la ruta de la ruta actual está en la matriz o no. Esta verificación se realiza en shouldDetachmétodo.

Encuentro este enfoque inflexible porque si queremos cambiar el nombre de la ruta, tendríamos que recordar cambiar también el nombre de la ruta en nuestra CustomReuseStrategyclase. Es posible que nos olvidemos de cambiarlo o que algún otro desarrollador de nuestro equipo decida cambiar el nombre de la ruta sin siquiera saber de la existencia de RouteReuseStrategy.

En lugar de almacenar las rutas que queremos almacenar en caché en una matriz, podemos marcarlas directamente RouterModuleusando dataobject. De esta manera, incluso si cambiamos el nombre de la ruta, la estrategia de reutilización se seguiría aplicando.

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

Y luego, en el shouldDetachmétodo, hacemos uso de eso.

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}
Davor
fuente
1
Buena solución. Esto realmente debería integrarse en la estrategia de reutilización de ruta angular estándar con una bandera simple como la que ha aplicado.
MIP 1983
Gran respuesta. ¡Muchas gracias!
claudiomatiasrg
14

Para usar la estrategia de Chris Fremgen con módulos cargados de manera perezosa, modifique la clase CustomReuseStrategy a lo siguiente:

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

finalmente, en los archivos de enrutamiento de sus módulos de funciones, defina sus claves:

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

Más info aquí .

Uğur Dinç
fuente
1
¡Gracias por agregar esto! Tengo que intentarlo. Incluso podría solucionar algunos de los problemas de manejo de rutas secundarias con los que se encuentra mi solución.
Corbfon
Tuve que usar route.data["key"]para construir sin errores. Pero el problema que tengo es que tengo un componente route + que se usa en dos lugares diferentes. 1. sample/list/itemy 2. product/id/sample/list/itemcuando cargo por primera vez cualquiera de las rutas, se carga bien, pero la otra arroja el error que se vuelve a adjuntar porque estoy almacenando en función de. list/itemEntonces, mi solución es que dupliqué la ruta e hice algunos cambios en la ruta de la URL pero mostrando el mismo componente. No estoy seguro de si hay otra solución para eso.
bluePearl
Esto me confundió un poco, lo anterior simplemente no funcionaría, explotaría tan pronto como golpeara una de mis rutas de caché (ya no navegaría y allí había errores en la consola). La solución de Chris Fremgen parece funcionar bien con mis módulos perezosos por lo que puedo decir hasta ahora ...
MIP1983
12

Otra implementación más válida, completa y reutilizable. Este admite módulos de carga diferida como @ Uğur Dinç e integra el indicador de datos de ruta @Davor. La mejor mejora es la generación automática de un identificador (casi) único basado en la ruta absoluta de la página. De esta manera, no es necesario que lo defina usted mismo en cada página.

Marque cualquier página que desee almacenar en caché reuseRoute: true. Se utilizará en el shouldDetachmétodo.

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

Esta es la implementación de estrategia más simple, sin comparar parámetros de consulta.

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

Este también compara los parámetros de consulta. compareObjectstiene una pequeña mejora con respecto a la versión @Corbfon: recorre las propiedades de los objetos base y de comparación. Recuerde que puede usar una implementación externa y más confiable como el isEqualmétodo 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 tiene una mejor manera de generar claves únicas, comente mi respuesta, actualizaré el código.

Gracias a todos los chicos que compartieron su solución.

McGiogen
fuente
3
Esta debería ser la respuesta aceptada. Muchas de las soluciones proporcionadas anteriormente no pueden admitir varias páginas con la misma URL secundaria. Porque están comparando la URL de la ruta activada, que no es la ruta completa.
zhuhang.jasper
4

Todas las soluciones mencionadas fueron de alguna manera insuficientes en nuestro caso. Tenemos una aplicación empresarial más pequeña con:

  1. Página de introducción
  2. Página de inicio de sesión
  3. Aplicación (después de iniciar sesión)

Nuestras necesidades:

  1. Módulos de carga diferida
  2. Rutas de varios niveles
  3. Almacene todos los estados del enrutador / componente en la memoria en la sección de la aplicación
  4. Opción para usar la estrategia de reutilización angular predeterminada en rutas específicas
  5. Destruir todos los componentes almacenados en la memoria al cerrar la sesión

Ejemplo simplificado de nuestras rutas:

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

Estrategia de reutilización:

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
fuente
3

lo siguiente es trabajo! referencia: 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, '_')
    }
}

红兵 伍
fuente
1
Cuidado, esto usa una variable interna _routerState.
DarkNeuron
¿@DarkNeuron _routerStatecausa algún daño?
k11k2
2
No, pero Google no tiene la obligación de mantener esa variable, ya que se usa internamente y no se expone en la API.
DarkNeuron
cuando estamos llamando deleteRouteSnapshot?
k11k2
0

Me enfrenté a estos problemas al implementar una estrategia de reutilización de rutas personalizada:

  1. Realizar operaciones en una ruta adjuntar / desacoplar: administrar suscripciones, limpieza, etc .;
  2. Conservar solo el estado de la última ruta parametrizada: optimización de memoria;
  3. Reutilice un componente, no un estado: gestione el estado con herramientas de gestión de estados.
  4. Error "No se puede volver a adjuntar ActivatedRouteSnapshot creada a partir de una ruta diferente";

Entonces escribí una biblioteca para resolver estos problemas. La biblioteca proporciona un servicio y decoradores para adjuntar / separar ganchos y utiliza los componentes de una ruta para almacenar rutas separadas, no las rutas de una ruta.

Ejemplo:

/* 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 biblioteca: https://www.npmjs.com/package/ng-cache-route-reuse

Stas Amasev
fuente
Simplemente vincular a su propia biblioteca o tutorial no es una buena respuesta. Vincularlo, explicar por qué resuelve el problema, proporcionar un código sobre cómo hacerlo y negar que usted lo escribió lo convierte en una mejor respuesta. Ver: ¿Qué significa "buena" autopromoción?
Paul Roub