Come implementare RouteReuseStrategy shouldDetach per percorsi specifici in Angular 2


115

Ho un modulo Angular 2 in cui ho implementato il routing e vorrei che gli stati venissero memorizzati durante la navigazione.
L'utente dovrebbe essere in grado di:

  1. cercare documenti utilizzando una "formula di ricerca"
  2. vai a uno dei risultati
  3. torna a "searchresult", senza comunicare con il server

Questo è possibile includendo RouteReuseStrategy.
La domanda è:
come imposto che il documento non debba essere archiviato?

Quindi lo stato del percorso della rotta "documenti" dovrebbe essere memorizzato e lo stato del percorso della rotta "documenti /: id" 'NON dovrebbe essere memorizzato?

Risposte:


209

Ehi Anders, ottima domanda!

Ho quasi lo stesso caso d'uso di te e volevo fare la stessa cosa! Ricerca utente> ottieni risultati> L'utente naviga fino al risultato> L'utente torna indietro> BOOM velocissimo ritorno ai risultati , ma non si desidera memorizzare il risultato specifico a cui l'utente è andato.

tl; dr

È necessario disporre di una classe che implementa RouteReuseStrategye fornisce la propria strategia in ngModule. Se vuoi modificare quando il percorso è memorizzato, modifica la shouldDetachfunzione. Quando ritorna true, Angular memorizza il percorso. Se vuoi modificare quando la rotta è collegata, modifica la shouldAttachfunzione. Quando shouldAttachrestituisce true, Angular utilizzerà la rotta memorizzata al posto della rotta richiesta. Ecco un Plunker con cui giocare.

Informazioni su RouteReuseStrategy

Avendo posto questa domanda, hai già capito che RouteReuseStrategy ti consente di dire ad Angular di non distruggere un componente, ma di salvarlo per il rendering in un secondo momento. È fantastico perché consente:

  • Diminuzione delle chiamate al server
  • Velocità aumentata
  • E il componente viene visualizzato, per impostazione predefinita, nello stesso stato in cui è stato lasciato

Quest'ultimo è importante se, ad esempio, desideri lasciare una pagina temporaneamente anche se l'utente ha inserito molto testo in essa. Le applicazioni aziendali apprezzeranno questa funzione a causa dell'eccessiva quantità di moduli!

Questo è ciò che ho pensato per risolvere il problema. Come hai detto, devi utilizzare l' RouteReuseStrategyofferta offerta da @ angular / router nelle versioni 3.4.1 e successive.

FARE

Innanzitutto assicurati che il tuo progetto abbia @ angular / router versione 3.4.1 o successiva.

Quindi , crea un file che ospiterà la tua classe che implementa RouteReuseStrategy. Ho chiamato il mio reuse-strategy.tse l' ho messo nella /appcartella per custodia. Per ora, questa classe dovrebbe assomigliare a:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(non preoccuparti per i tuoi errori TypeScript, stiamo per risolvere tutto)

Termina le basi fornendo la lezione al tuo app.module. Si noti che non è ancora stato scritto CustomReuseStrategy, ma deve andare avanti e importda reuse-strategy.tstutto lo stesso. Ancheimport { RouteReuseStrategy } from '@angular/router';

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

Il pezzo finale è scrivere la classe che controllerà se le rotte vengono scollegate, archiviate, recuperate e ricollegate. Prima di arrivare al vecchio copia / incolla , farò qui una breve spiegazione delle meccaniche, come ho capito. Fai riferimento al codice seguente per i metodi che sto descrivendo e, naturalmente, c'è molta documentazione nel codice .

  1. Quando navighi, shouldReuseRoutespara. Questo è un po 'strano per me, ma se ritorna true, in realtà riutilizza il percorso su cui ti trovi attualmente e nessuno degli altri metodi viene attivato. Restituisco solo false se l'utente sta navigando.
  2. Se shouldReuseRouteritorna false, shouldDetachspara. shouldDetachdetermina se si desidera o meno memorizzare il percorso e restituisce un che ne booleanindica tanto. Qui è dove dovresti decidere di memorizzare / non memorizzare i percorsi , cosa che farei controllando un array di percorsi su cui desideri archiviare route.routeConfig.pathe restituendo false se pathnon esiste nell'array.
  3. Se shouldDetachritorna true, storeviene attivato, che è un'opportunità per te di memorizzare tutte le informazioni che desideri sul percorso. Qualunque cosa tu faccia, dovrai memorizzare il DetachedRouteHandleperché è ciò che Angular usa per identificare il tuo componente memorizzato in seguito. Di seguito, memorizzo sia il DetachedRouteHandleche il ActivatedRouteSnapshotin una variabile locale della mia classe.

Quindi, abbiamo visto la logica per l'archiviazione, ma per quanto riguarda la navigazione verso un componente? In che modo Angular decide di intercettare la tua navigazione e di rimettere quella memorizzata al suo posto?

  1. Di nuovo, dopo che shouldReuseRouteè tornato false, shouldAttachviene eseguito, che è la tua occasione per capire se vuoi rigenerare o usare il componente in memoria. Se vuoi riutilizzare un componente immagazzinato, torna truee sei sulla buona strada!
  2. Ora Angular ti chiederà "quale componente vuoi che usiamo?", Che indicherai restituendo quel componente DetachedRouteHandleda retrieve.

Questa è praticamente tutta la logica di cui hai bisogno! Nel codice per reuse-strategy.ts, di seguito, ti ho anche lasciato un'elegante funzione che confronterà due oggetti. Io lo uso per confrontare i del percorso futuro route.paramse route.queryParamscon le stored propri. Se tutti corrispondono, voglio utilizzare il componente memorizzato invece di generarne uno nuovo. Ma come lo fai dipende da te!

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

Comportamento

Questa implementazione memorizza ogni percorso univoco che l'utente visita sul router esattamente una volta. Ciò continuerà ad aggiungersi ai componenti archiviati in memoria durante la sessione dell'utente sul sito. Se desideri limitare i percorsi che memorizzi, il posto giusto per farlo è il shouldDetachmetodo. Controlla quali rotte salvi.

Esempio

Supponiamo che il tuo utente cerchi qualcosa dalla home page, che lo indirizza al percorso search/:term, che potrebbe apparire come www.yourwebsite.com/search/thingsearchedfor. La pagina di ricerca contiene una serie di risultati di ricerca. Vorresti memorizzare questo percorso, nel caso volessero tornare su di esso! Ora fanno clic su un risultato di ricerca e vengono indirizzati a view/:resultId, che non si desidera memorizzare, visto che probabilmente saranno presenti solo una volta. Con l'implementazione di cui sopra, cambierei semplicemente il shouldDetachmetodo! Ecco come potrebbe apparire:

Prima di tutto creiamo una serie di percorsi che vogliamo memorizzare.

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

ora, shouldDetachpossiamo controllare il route.routeConfig.pathcontro il nostro array.

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

Poiché Angular memorizzerà solo un'istanza di un percorso, questa memoria sarà leggera e memorizzeremo solo il componente situato in search/:terme non tutti gli altri!

Collegamenti aggiuntivi

Sebbene non ci sia ancora molta documentazione là fuori, ecco un paio di collegamenti a ciò che esiste:

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

Articolo introduttivo: https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx

Implementazione predefinita di nativescript - angular di RouteReuseStrategy : https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts


2
@shaahin ho aggiunto un esempio, che è il codice esatto contenuto nella mia attuale implementazione!
Corbfon

1
@Corbfon Ho anche aperto un numero sulla pagina github ufficiale: github.com/angular/angular/issues/13869
Tjaart van der Walt

2
C'è un modo per far sì che riesca a eseguire le animazioni di immissione quando si riattiva un percorso memorizzato?
Jinder Sidhu

2
ReuseRouteStrategy restituirà il tuo componente al router, quindi sarà nello stato in cui è stato lasciato. Se desideri che il componente (i) reagisca all'allegato, puoi utilizzare un servizio che offre un file Observable. Il componente dovrebbe iscriversi all'hook Observabledurante il ngOnInitciclo di vita. Quindi sarai in grado di dire al componente, dal ReuseRouteStrategy, che è stato appena collegato e il componente può modificare il suo stato come adatto.
Corbfon

1
@ AndersGramMygind se la mia risposta fornisce una risposta alla domanda che hai proposto, la contrassegneresti come risposta?
Corbfon

45

Non lasciarti intimidire dalla risposta accettata, questo è abbastanza semplice. Ecco una rapida risposta di cosa hai bisogno. Consiglierei almeno di leggere la risposta accettata, poiché è piena di grandi dettagli.

Questa soluzione non esegue alcun confronto dei parametri come la risposta accettata, ma funzionerà bene per memorizzare un insieme di percorsi.

app.module.ts importa:

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

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

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

1
Funzionerà anche per rotte caricate in modo pigro?
bluePearl

@bluePearl Controlla la risposta qui sotto
Chris Fremgen

2
routeConfig è null, per percorsi diversi, quindi shouldReuseRoute restituirà sempre true, che non è il comportamento desiderato
Gil Epshtain

19

Oltre alla risposta accettata (di Corbfon) e alla spiegazione più breve e diretta di Chris Fremgen, voglio aggiungere un modo più flessibile di gestire i percorsi che dovrebbe utilizzare la strategia di riutilizzo.

Entrambe le risposte memorizzano le rotte che vogliamo memorizzare nella cache in un array e quindi controllano se il percorso della rotta corrente è nell'array o meno. Questo controllo viene eseguito inshouldDetach metodo.

Trovo questo approccio poco flessibile perché se vogliamo cambiare il nome della rotta dovremmo ricordarci di cambiare anche il nome della rotta nella nostra CustomReuseStrategyclasse. Potremmo dimenticarci di cambiarlo o qualche altro sviluppatore del nostro team potrebbe decidere di cambiare il nome della rotta senza nemmeno sapere dell'esistenza di RouteReuseStrategy.

Invece di memorizzare le rotte che vogliamo memorizzare nella cache in un array, possiamo contrassegnarle direttamente in RouterModuleusing dataobject. In questo modo, anche se cambiamo il nome della rotta, la strategia di riutilizzo verrà comunque applicata.

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

E poi nel shouldDetachmetodo ne facciamo uso.

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}

1
Buona soluzione. Questo dovrebbe essere inserito nella strategia di riutilizzo del percorso angolare standard con una semplice bandiera come quella che hai applicato.
MIP1983

Bella risposta. Grazie mille!
claudiomatiasrg

14

Per utilizzare la strategia di Chris Fremgen con moduli caricati in modo lento, modifica la classe CustomReuseStrategy nel modo seguente:

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

infine, nei file di instradamento dei moduli delle funzionalità, definisci le tue chiavi:

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

Maggiori info qui .


1
Grazie per aver aggiunto questo! Devo fare un tentativo. Potrebbe anche risolvere alcuni dei problemi di gestione del percorso figlio in cui si imbatte la mia soluzione.
Corbfon

Ho dovuto usare route.data["key"]per costruire senza errori. Ma il problema che ho è che ho un componente route + che viene utilizzato in due posti diversi. 1. sample/list/iteme 2. product/id/sample/list/itemquando carico per la prima volta uno dei percorsi, si carica bene ma l'altro genera l'errore ricollegato perché sto memorizzando in base a list/itemQuindi il mio lavoro è duplicato il percorso e apportato alcune modifiche al percorso dell'URL ma visualizzando lo stesso componente. Non sono sicuro che ci sia un'altra soluzione per questo.
bluePearl

Questo mi ha confuso, quanto sopra non funzionerebbe, sarebbe esploso non appena avessi raggiunto uno dei miei percorsi di cache (non sarebbe più navigato e lì dove si verificano errori nella console). La soluzione di Chris Fremgen sembra funzionare bene con i miei moduli pigri per quanto posso dire finora ...
MIP1983

12

Un'altra implementazione più valida, completa e riutilizzabile. Questo supporta i moduli caricati pigri come @ Uğur Dinç e integra il flag dei dati di rotta @Davor. Il miglior miglioramento è la generazione automatica di un identificatore (quasi) univoco basato sul percorso assoluto della pagina. In questo modo non devi definirlo tu stesso in ogni pagina.

Contrassegna qualsiasi pagina che desideri memorizzare nella cache reuseRoute: true. Sarà utilizzato nel shouldDetachmetodo.

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

Questa è l'implementazione della strategia più semplice, senza confrontare i parametri della query.

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

Questo confronta anche i parametri della query. compareObjectsha un piccolo miglioramento rispetto alla versione @Corbfon: scorre le proprietà di entrambi gli oggetti di base e confronta. Ricorda che puoi utilizzare un'implementazione esterna e più affidabile come il isEqualmetodo 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;
  }
}

Se hai un modo migliore per generare chiavi univoche commenta la mia risposta, aggiornerò il codice.

Grazie a tutti i ragazzi che hanno condiviso la loro soluzione.


3
Questa dovrebbe essere la risposta accettata. Molte soluzioni fornite sopra non possono supportare più pagine con lo stesso URL figlio. Perché stanno confrontando l'URL attivatoRoute, che non è il percorso completo.
zhuhang.jasper

4

Tutte le soluzioni menzionate erano in qualche modo insufficienti nel nostro caso. Abbiamo un'app per piccole imprese con:

  1. Pagina introduttiva
  2. Pagina di login
  3. App (dopo il login)

I nostri requisiti:

  1. Moduli a caricamento lento
  2. Percorsi a più livelli
  3. Archivia tutti gli stati del router / componente in memoria nella sezione app
  4. Possibilità di utilizzare la strategia di riutilizzo angolare predefinita su percorsi specifici
  5. Distruggere tutti i componenti archiviati in memoria al logout

Esempio semplificato dei nostri percorsi:

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

Strategia di riutilizzo:

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

3

il seguente è lavoro! riferimento: 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, '_')
    }
}


1
Attenzione, utilizza una variabile interna _routerState.
DarkNeuron

@DarkNeuron _routerStatecausa danni?
k11k2

2
No, ma Google non ha l'obbligo di mantenere quella variabile, poiché è utilizzata internamente e non è esposta nell'API.
DarkNeuron

quando stiamo chiamando deleteRouteSnapshot?
k11k2

0

Ho affrontato questi problemi implementando una strategia di riutilizzo del percorso personalizzato:

  1. Eseguire operazioni su una rotta allegare / dettach: gestire abbonamenti, pulizia, ecc .;
  2. Conserva solo lo stato dell'ultimo percorso parametrizzato: ottimizzazione della memoria;
  3. Riutilizza un componente, non uno stato: gestisci lo stato con strumenti di gestione dello stato.
  4. Errore "Impossibile ricollegare ActivatedRouteSnapshot creato da un percorso diverso";

Quindi ho scritto una libreria per risolvere questi problemi. La libreria fornisce un servizio e decoratori per collegare / scollegare i ganci e utilizza i componenti di un percorso per memorizzare i percorsi staccati, non i percorsi di un percorso.

Esempio:

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


Il semplice collegamento alla tua libreria o tutorial non è una buona risposta. Collegarsi ad esso, spiegare perché risolve il problema, fornire codice su come farlo e negare di averlo scritto rappresenta una risposta migliore. Vedere: Cosa significa auto promozione "buona"?
Paul Roub
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.