¡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 RouteReuseStrategy
y proporcione su estrategia en ngModule
. Si desea modificar cuándo se almacena la ruta, modifique la shouldDetach
función. Cuando regresa true
, Angular almacena la ruta. Si desea modificar cuándo se adjunta la ruta, modifique la shouldAttach
función. Cuando shouldAttach
devuelve 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 RouteReuseStrategy
ofrecido 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.ts
y lo guardé en la /app
carpeta 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 import
de reuse-strategy.ts
todos 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 .
- Cuando navegas,
shouldReuseRoute
dispara. 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.
- Si
shouldReuseRoute
regresa false
, shouldDetach
dispara. shouldDetach
determina si desea o no almacenar la ruta, y devuelve un boolean
indicando 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.path
y devolver falso si path
no existe en la matriz.
- Si
shouldDetach
regresa true
, store
se 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 DetachedRouteHandle
porque eso es lo que Angular usa para identificar tu componente almacenado más adelante. A continuación, almaceno tanto the DetachedRouteHandle
como the ActivatedRouteSnapshot
en 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?
- Una vez más, después de que
shouldReuseRoute
haya regresado false
, se shouldAttach
ejecuta, que es su oportunidad de averiguar si desea regenerar o usar el componente en la memoria. Si desea reutilizar un componente almacenado, regrese true
y estará bien encaminado.
- Ahora angular le preguntará, "qué componente es lo que desea que utilicemos?", Que se le indicará mediante la devolución de que los componentes
DetachedRouteHandle
de 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.params
y route.queryParams
la 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 shouldDetach
mé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/:resultId
que no desea almacenar, ya que probablemente solo estarán allí una vez. Con la implementación anterior en su lugar, ¡simplemente cambiaría el shouldDetach
método! Así es como podría verse:
En primer lugar , hagamos una serie de rutas que queremos almacenar.
private acceptedRoutes: string[] = ["search/:term"];
ahora, shouldDetach
podemos comparar el route.routeConfig.path
con 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/:term
y 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