Jak zaimplementować RouteReuseStrategy shouldDetach dla określonych tras w Angular 2

116

Mam moduł Angular 2, w którym zaimplementowałem routing i chciałbym, aby stany były przechowywane podczas nawigacji.
Użytkownik powinien umieć:

  1. wyszukuj dokumenty za pomocą „formuły wyszukiwania”
  2. przejdź do jednego z wyników
  3. przejdź z powrotem do „searchresult” - bez komunikacji z serwerem

Jest to możliwe, w tym RouteReuseStrategy.
Pytanie brzmi:
jak mam zaimplementować, że dokument nie powinien być przechowywany?

Zatem stan „dokumentów” ścieżki trasy powinien być przechowywany, a stan ścieżki trasy „dokumenty /: id” NIE powinien być przechowywany?

Anders Gram Mygind
źródło

Odpowiedzi:

210

Hej Anders, świetne pytanie!

Mam prawie taki sam przypadek użycia jak ty i chciałem zrobić to samo! Wyszukiwanie użytkownika> pobierz wyniki> użytkownik przechodzi do wyniku> użytkownik przechodzi wstecz> Boom błyskawicznie szybki powrót do wyników , ale nie chcesz zapisywać konkretnego wyniku, do którego przeszedł użytkownik.

tl; dr

Musisz mieć klasę, która implementuje RouteReuseStrategyi udostępnia twoją strategię w ngModule. Jeśli chcesz zmienić czas zapisywania trasy, zmodyfikuj shouldDetachfunkcję. Kiedy wróci true, Angular zapisze trasę. Jeśli chcesz zmodyfikować, kiedy trasa jest dołączona, zmodyfikuj shouldAttachfunkcję. Gdy shouldAttachzwróci true, Angular użyje zapisanej trasy zamiast żądanej trasy. Oto Plunker, z którym możesz się bawić.

O RouteReuseStrategy

Zadając to pytanie, rozumiesz już, że RouteReuseStrategy pozwala powiedzieć Angularowi, aby nie niszczył komponentu, ale w rzeczywistości zapisał go do ponownego renderowania w późniejszym terminie. To fajne, ponieważ pozwala:

  • Zmniejszone wywołania serwera
  • Zwiększona prędkość
  • ORAZ komponent renderuje się domyślnie w tym samym stanie, w jakim został

Ta ostatnia jest ważna, jeśli chcesz, powiedzmy, tymczasowo opuścić stronę, mimo że użytkownik dużo wszedł tekstu. Aplikacje korporacyjne pokochają tę funkcję ze względu na nadmierną liczbę formularzy!

Oto, co wymyśliłem, aby rozwiązać problem. Jak powiedziałeś, musisz skorzystać z RouteReuseStrategyoferowanego przez @ angular / router w wersjach 3.4.1 i wyższych.

DO ZROBIENIA

Pierwszy upewnij się, że projekt ma @ angular / router w wersji 3.4.1 lub nowszej.

Następnie utwórz plik, który będzie zawierał twoją klasę implementującą RouteReuseStrategy. Zadzwoniłem do mojego reuse-strategy.tsi umieściłem go w /appfolderze na przechowanie. Na razie ta klasa powinna wyglądać następująco:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(nie martw się o błędy TypeScript, za chwilę wszystko rozwiążemy)

Zakończ prace przygotowawcze , udostępniając klasę swojemu app.module. Zauważ, że jeszcze nie napisałeś CustomReuseStrategy, ale powinieneś iść dalej i importto od reuse-strategy.tstego samego. Równieżimport { RouteReuseStrategy } from '@angular/router';

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

Ostatnim elementem jest napisanie klasy, która będzie kontrolować, czy trasy zostaną odłączone, zapisane, odzyskane i ponownie dołączone. Zanim przejdziemy do starego kopiowania / wklejania , przedstawię tutaj krótkie wyjaśnienie mechaniki, tak jak ją rozumiem. Odwołaj się do poniższego kodu do metod, które opisuję, i oczywiście w kodzie jest mnóstwo dokumentacji .

  1. Podczas nawigacji shouldReuseRoutepożary. Ten jest dla mnie trochę dziwny, ale jeśli wrócitrue , w rzeczywistości ponownie wykorzysta trasę, na której aktualnie jesteś, i żadna z pozostałych metod nie zostanie uruchomiona. Po prostu zwracam false, jeśli użytkownik odchodzi.
  2. Jeśli shouldReuseRoutewróci false, shouldDetachstrzela. shouldDetachokreśla, czy chcesz zapisać trasę, i zwraca booleanwskazującą wartość. W tym miejscu powinieneś zdecydować się na przechowywanie / nieprzechowywanie ścieżek , co zrobiłbym, sprawdzając tablicę ścieżek, dla których chcesz przechowywać route.routeConfig.path, i zwracając false, jeśli pathnie istnieje w tablicy.
  3. Jeśli shouldDetachwróci true, storezostanie zwolniony, co jest okazją do przechowywania dowolnych informacji o trasie. Cokolwiek robisz, będziesz musiał przechowywać ten DetachedRouteHandleplik, ponieważ Angular używa go do późniejszej identyfikacji przechowywanego komponentu. Poniżej przechowywać zarówno DetachedRouteHandlei ActivatedRouteSnapshotdo zmiennej lokalnej do mojej klasy.

Więc widzieliśmy logikę przechowywania, ale co z nawigacją do komponentu? W jaki sposób Angular decyduje się przechwycić Twoją nawigację i umieścić zapisaną na swoim miejscu?

  1. Ponownie, po shouldReuseRoutepowrocie false, shouldAttachdziała, co jest Twoją szansą, aby dowiedzieć się, czy chcesz zregenerować, czy użyć komponentu w pamięci. Jeśli chcesz ponownie użyć przechowywanego komponentu, zwróćtrue i jesteś na dobrej drodze!
  2. Teraz kątowa cię zapytać, „który element chcesz nam korzystać?”, Który będzie wskazywać wracając to komponent DetachedRouteHandlez retrieve.

To prawie cała logika, której potrzebujesz! W kodzie reuse-strategy.tsponiżej zostawiłem ci również fajną funkcję, która porówna dwa obiekty. Używam go do porównania przyszłych tras route.paramsi route.queryParamszapisanych. Jeśli te wszystkie pasują do siebie, chcę użyć przechowywanego komponentu zamiast generować nowy. Ale jak to zrobisz, zależy od Ciebie!

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

Zachowanie

Ta implementacja zapisuje każdą unikalną trasę, którą użytkownik odwiedza na routerze dokładnie raz. Będzie to kontynuować dodawanie składników przechowywanych w pamięci przez całą sesję użytkownika w witrynie. Jeśli chcesz ograniczyć trasy, które przechowujesz, miejscem, w którym możesz to zrobić, jest shouldDetachmetoda. Kontroluje, które trasy zapisujesz.

Przykład

Załóżmy, że użytkownik szuka czegoś na stronie głównej, co prowadzi go do ścieżki search/:term, która może wyglądać jak www.yourwebsite.com/search/thingsearchedfor. Strona wyszukiwania zawiera kilka wyników wyszukiwania. Chciałbyś zapisać tę trasę na wypadek, gdyby chcieli do niej wrócić! Teraz klikają wynik wyszukiwania i przechodzą do view/:resultId, którego nie chcesz przechowywać, ponieważ prawdopodobnie będą tam tylko raz. Mając powyższą implementację, po prostu zmieniłbym shouldDetachmetodę! Oto, jak może to wyglądać:

Po pierwsze stwórzmy tablicę ścieżek, które chcemy przechowywać.

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

teraz shouldDetachmożemy sprawdzić route.routeConfig.pathz naszą tablicą.

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

Ponieważ Angular będzie przechowywać tylko jedno wystąpienie trasy, ten magazyn będzie lekki i będziemy przechowywać tylko komponent znajdujący się w, search/:terma nie wszystkie pozostałe!

Dodatkowe linki

Chociaż nie ma jeszcze dużej dokumentacji, oto kilka linków do tego, co istnieje:

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

Artykuł wprowadzający: https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx

nativescript-angular's default Implementation of RouteReuseStrategy : https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts

Corbfon
źródło
2
@shaahin Dodałem przykład, czyli dokładny kod zawarty w mojej obecnej implementacji!
Corbfon,
1
@Corbfon Otworzyłem również numer na oficjalnej stronie github: github.com/angular/angular/issues/13869
Tjaart van der Walt
2
Czy istnieje sposób, aby ponownie uruchomić animacje wprowadzania podczas ponownej aktywacji zapisanej trasy?
Jinder Sidhu
2
ReuseRouteStrategy przekaże twój komponent z powrotem do routera, więc będzie w jakimkolwiek stanie, w jakim został pozostawiony. Jeśli chcesz, aby komponenty zareagowały na załącznik, możesz użyć usługi, która oferuje Observable. Komponent powinien subskrybować hak Observablepodczas ngOnInitcyklu życia. Wtedy będziesz w stanie określić komponent na podstawie tego ReuseRouteStrategy, że został właśnie dołączony i komponent może zmodyfikować swój stan tak, aby pasował.
Corbfon,
1
@AndersGramMygind, jeśli moja odpowiedź zawiera odpowiedź na zadane przez Ciebie pytanie, czy oznaczysz ją jako odpowiedź?
Corbfon,
45

Nie daj się zastraszyć zaakceptowanej odpowiedzi, to całkiem proste. Oto szybka odpowiedź, czego potrzebujesz. Polecam przynajmniej przeczytanie zaakceptowanej odpowiedzi, ponieważ jest ona bardzo szczegółowa.

To rozwiązanie nie wykonuje żadnego porównania parametrów, takich jak zaakceptowana odpowiedź, ale będzie działać dobrze do przechowywania zestawu tras.

Import 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
źródło
1
Czy to zadziała również w przypadku tras, które są wczytywane leniwie?
bluePearl
@bluePearl Sprawdź odpowiedź poniżej
Chris Fremgen
2
routeConfig ma wartość null, dla różnych tras, dlatego shouldReuseRoute zawsze zwraca wartość true, co nie jest pożądanym zachowaniem
Gil Epshtain
19

Oprócz zaakceptowanej odpowiedzi (autorstwa Corbfona) i krótszego i prostszego wyjaśnienia Chrisa Fremgena, chcę dodać bardziej elastyczny sposób obsługi tras, który powinien wykorzystywać strategię ponownego wykorzystania.

Obie odpowiedzi przechowują trasy, które chcemy buforować w tablicy, a następnie sprawdzają, czy bieżąca ścieżka trasy znajduje się w tablicy, czy nie. To sprawdzenie odbywa się w shouldDetachmetodzie.

Uważam to podejście za nieelastyczne, ponieważ gdybyśmy chcieli zmienić nazwę trasy, musielibyśmy pamiętać o zmianie również nazwy trasy w naszej CustomReuseStrategyklasie. Możemy albo zapomnieć o jej zmianie, albo inny programista z naszego zespołu może zdecydować o zmianie nazwy trasy, nawet nie wiedząc o jej istnieniu RouteReuseStrategy.

Zamiast przechowywać trasy, które chcemy buforować w tablicy, możemy oznaczyć je bezpośrednio przy RouterModuleużyciu dataobiektu. W ten sposób, nawet jeśli zmienimy nazwę trasy, strategia ponownego wykorzystania będzie nadal stosowana.

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

A potem w shouldDetachmetodzie wykorzystujemy to.

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}
Davor
źródło
1
Dobre rozwiązanie. Powinno to po prostu zostać wprowadzone do standardowej strategii ponownego wykorzystania tras kątowych z prostą flagą, taką jak zastosowana.
MIP1983
Świetna odpowiedź. Dziękuję Ci bardzo!
claudiomatiasrg
14

Aby użyć strategii Chrisa Fremgena z leniwie ładowanymi modułami, zmień klasę CustomReuseStrategy na następującą:

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

na koniec w plikach routingu modułów funkcji zdefiniuj klucze:

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

Więcej informacji tutaj .

Uğur Dinç
źródło
1
Dzięki za dodanie tego! Muszę spróbować. Może nawet rozwiązać niektóre problemy z obsługą tras podrzędnych, na które napotyka moje rozwiązanie.
Corbfon
Musiałem użyć route.data["key"]do zbudowania bez błędów. Ale problem, który mam, polega na tym, że mam komponent trasy +, który jest używany w dwóch różnych miejscach. 1. sample/list/itema 2. product/id/sample/list/itemkiedy po raz pierwszy ładuję jedną ze ścieżek, ładuje się dobrze, ale druga generuje ponownie dołączony błąd, ponieważ przechowuję w oparciu o list/itemWięc moja praca polega na zduplikowaniu trasy i dokonaniu pewnej zmiany w ścieżce adresu URL, ale wyświetlając ten sam składnik. Nie jestem pewien, czy jest na to inny sposób.
bluePearl
To mnie wprawiło w zakłopotanie, powyższe po prostu nie zadziałałoby, wybuchłoby, gdy tylko trafię na jedną z moich tras pamięci podręcznej (nie będzie już nawigować i tam, gdzie błędy w konsoli). Rozwiązanie Chrisa Fremgena wydaje się działać dobrze z moimi leniwymi modułami, o ile wiem ...
MIP1983
12

Kolejna implementacja, bardziej aktualna, kompletna i wielokrotnego użytku. Ten obsługuje leniwie ładowane moduły jako @ Uğur Dinç i integruje flagę danych trasy @Davor. Najlepszym ulepszeniem jest automatyczne generowanie (prawie) unikalnego identyfikatora na podstawie bezwzględnej ścieżki strony. W ten sposób nie musisz samodzielnie definiować tego na każdej stronie.

Zaznacz dowolną stronę, którą chcesz zapisać w pamięci podręcznej reuseRoute: true. Zostanie użyty w shouldDetachmetodzie.

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

Jest to najprostsza implementacja strategii, bez porównywania parametrów zapytań.

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

Ten również porównuje parametry zapytania. compareObjectsma niewielką poprawę w stosunku do wersji @Corbfon: pętla przez właściwości obiektów bazowych i porównawczych. Pamiętaj, że możesz użyć zewnętrznej i bardziej niezawodnej implementacji, takiej jak isEqualmetoda 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;
  }
}

Jeśli masz najlepszy sposób na generowanie unikalnych kluczy, skomentuj moją odpowiedź, zaktualizuję kod.

Dziękuję wszystkim, którzy podzielili się swoim rozwiązaniem.

McGiogen
źródło
3
To powinna być akceptowana odpowiedź. Wiele rozwiązań przedstawionych powyżej nie obsługuje wielu stron z tym samym podrzędnym adresem URL. Ponieważ porównują adres URL activeRoute, który nie jest pełną ścieżką.
zhuhang.jasper
4

Wszystkie wymienione rozwiązania były w naszym przypadku jakoś niewystarczające. Mamy mniejszą aplikację biznesową z:

  1. Strona wprowadzenia
  2. Strona logowania
  3. Aplikacja (po zalogowaniu)

Nasze wymagania:

  1. Leniwie ładowane moduły
  2. Trasy wielopoziomowe
  3. Przechowuj wszystkie stany routera / komponentu w pamięci w sekcji aplikacji
  4. Możliwość użycia domyślnej strategii ponownego wykorzystania kątowego na określonych trasach
  5. Niszczenie wszystkich komponentów przechowywanych w pamięci przy wylogowaniu

Uproszczony przykład naszych tras:

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 ponownego wykorzystania:

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
źródło
3

Oto praca! źródło: 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, '_')
    }
}

红兵 伍
źródło
1
Ostrożnie, to używa zmiennej wewnętrznej _routerState.
DarkNeuron
@DarkNeuron _routerStatepowoduje jakiekolwiek szkodliwe?
k11k2
2
Nie, ale Google nie ma obowiązku utrzymywania tej zmiennej, ponieważ jest używana wewnętrznie i nie jest ujawniana w interfejsie API.
DarkNeuron,
gdy wzywamy deleteRouteSnapshot?
k11k2
0

Miałem do czynienia z następującymi problemami, wdrażając niestandardową strategię ponownego wykorzystania tras:

  1. Wykonywanie operacji na trasie dołączanie / odłączanie: zarządzanie subskrypcjami, czyszczenie itp .;
  2. Zachowaj tylko stan ostatniej sparametryzowanej trasy: optymalizacja pamięci;
  3. Ponownie użyj komponentu, a nie stanu: zarządzaj stanem za pomocą narzędzi do zarządzania stanem.
  4. Błąd „Nie można ponownie dołączyć ActivatedRouteSnapshot utworzonego z innej trasy”;

Więc napisałem bibliotekę rozwiązującą te problemy. Biblioteka zapewnia usługę i dekoratory do dołączania / odłączania punktów zaczepienia i wykorzystuje komponenty trasy do przechowywania odłączonych tras, a nie ścieżek tras.

Przykład:

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

Biblioteka: https://www.npmjs.com/package/ng-cache-route-reuse

Stas Amasev
źródło
Samo łącze do własnej biblioteki lub samouczka nie jest dobrą odpowiedzią. Łączenie się do niego, wyjaśnianie, dlaczego rozwiązuje problem, dostarczanie kodu, jak to zrobić i zrzeczenie się, że napisałeś, zapewnia lepszą odpowiedź. Zobacz: Co oznacza „dobrą” autopromocję?
Paul Roub