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 RouteReuseStrategy
i udostępnia twoją strategię w ngModule
. Jeśli chcesz zmienić czas zapisywania trasy, zmodyfikuj shouldDetach
funkcję. Kiedy wróci true
, Angular zapisze trasę. Jeśli chcesz zmodyfikować, kiedy trasa jest dołączona, zmodyfikuj shouldAttach
funkcję. Gdy shouldAttach
zwró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 RouteReuseStrategy
oferowanego 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.ts
i umieściłem go w /app
folderze 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 import
to od reuse-strategy.ts
tego 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 .
- Podczas nawigacji
shouldReuseRoute
poż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.
- Jeśli
shouldReuseRoute
wróci false
, shouldDetach
strzela. shouldDetach
określa, czy chcesz zapisać trasę, i zwraca boolean
wskazują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 path
nie istnieje w tablicy.
- Jeśli
shouldDetach
wróci true
, store
zostanie zwolniony, co jest okazją do przechowywania dowolnych informacji o trasie. Cokolwiek robisz, będziesz musiał przechowywać ten DetachedRouteHandle
plik, ponieważ Angular używa go do późniejszej identyfikacji przechowywanego komponentu. Poniżej przechowywać zarówno DetachedRouteHandle
i ActivatedRouteSnapshot
do 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?
- Ponownie, po
shouldReuseRoute
powrocie false
, shouldAttach
dział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!
- Teraz kątowa cię zapytać, „który element chcesz nam korzystać?”, Który będzie wskazywać wracając to komponent
DetachedRouteHandle
z retrieve
.
To prawie cała logika, której potrzebujesz! W kodzie reuse-strategy.ts
poniżej zostawiłem ci również fajną funkcję, która porówna dwa obiekty. Używam go do porównania przyszłych tras route.params
i route.queryParams
zapisanych. 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 shouldDetach
metoda. 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 shouldDetach
metodę! Oto, jak może to wyglądać:
Po pierwsze stwórzmy tablicę ścieżek, które chcemy przechowywać.
private acceptedRoutes: string[] = ["search/:term"];
teraz shouldDetach
możemy sprawdzić route.routeConfig.path
z 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/:term
a 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