Ei Anders, ótima pergunta!
Tenho quase o mesmo caso de uso que você e queria fazer a mesma coisa! Pesquisa do usuário> obter resultados> O usuário navega até o resultado> O usuário navega de volta> BOOM retorno rápido aos resultados , mas você não deseja armazenar o resultado específico para o qual o usuário navegou.
tl; dr
Você precisa ter uma classe que implemente RouteReuseStrategy
e forneça sua estratégia no ngModule
. Se você deseja modificar quando a rota é armazenada, modifique a shouldDetach
função. Quando retorna true
, o Angular armazena a rota. Se você deseja modificar quando a rota é anexada, modifique a shouldAttach
função. Quando shouldAttach
retorna verdadeiro, o Angular usará a rota armazenada no lugar da rota solicitada. Aqui está um Plunker para você brincar.
Sobre RouteReuseStrategy
Ao fazer essa pergunta, você já entende que RouteReuseStrategy permite que você diga ao Angular para não destruir um componente, mas na verdade salvá-lo para uma nova renderização em uma data posterior. Isso é legal porque permite:
- Chamadas de servidor diminuídas
- Velocidade aumentada
- E o componente renderiza, por padrão, no mesmo estado em que foi deixado
Este último é importante se você quiser, digamos, sair de uma página temporariamente, mesmo que o usuário tenha inserido muito texto nela. Os aplicativos corporativos vão adorar esse recurso por causa do excesso quantidade de formulários!
Foi isso que eu inventei para resolver o problema. Como você disse, você precisa fazer uso do RouteReuseStrategy
oferecido pelo @ angular / router nas versões 3.4.1 e superiores.
FAÇAM
Primeiro, certifique-se de que seu projeto tenha @ angular / router versão 3.4.1 ou superior.
A seguir , crie um arquivo que hospedará sua classe que implementa RouteReuseStrategy
. Liguei para o meu reuse-strategy.ts
e coloquei-o na /app
pasta para guarda. Por enquanto, esta classe deve ser semelhante a:
import { RouteReuseStrategy } from '@angular/router';
export class CustomReuseStrategy implements RouteReuseStrategy {
}
(não se preocupe com seus erros de TypeScript, estamos prestes a resolver tudo)
Termine o trabalho de base fornecendo a aula para você app.module
. Observe que você ainda não escreveu CustomReuseStrategy
, mas deve ir em frente e import
desde reuse-strategy.ts
sempre. Além dissoimport { RouteReuseStrategy } from '@angular/router';
@NgModule({
[...],
providers: [
{provide: RouteReuseStrategy, useClass: CustomReuseStrategy}
]
)}
export class AppModule {
}
A parte final é escrever a classe que controlará se as rotas serão ou não desanexadas, armazenadas, recuperadas e reanexadas. Antes de passarmos ao antigo copiar / colar , farei uma breve explicação da mecânica aqui, como eu a entendo. Consulte o código abaixo para ver os métodos que estou descrevendo e, claro, há muita documentação no código .
- Quando você navega,
shouldReuseRoute
dispara. Este é um pouco estranho para mim, mas se voltartrue
, na verdade, ele reutiliza a rota em que você está atualmente e nenhum dos outros métodos é acionado. Acabei de retornar falso se o usuário estiver navegando para longe.
- Se
shouldReuseRoute
retornar false
, shouldDetach
dispara. shouldDetach
determina se você deseja ou não armazenar a rota e retorna um boolean
indicando isso. É aqui que você deve decidir armazenar / não armazenar caminhos , o que eu faria verificando uma matriz de caminhos que deseja armazenar route.routeConfig.path
e retornando false se path
não existir na matriz.
- Se
shouldDetach
retornar true
, store
é disparado, o que é uma oportunidade para você armazenar todas as informações que desejar sobre a rota. Faça o que fizer, você precisará armazenar o DetachedRouteHandle
porque é isso que o Angular usa para identificar seu componente armazenado mais tarde. Abaixo, eu armazeno o DetachedRouteHandle
e o ActivatedRouteSnapshot
em uma variável local para minha classe.
Então, vimos a lógica de armazenamento, mas e quanto a navegar até um componente? Como o Angular decide interceptar sua navegação e colocar aquela armazenada em seu lugar?
- Mais uma vez, depois de
shouldReuseRoute
retornar false
, shouldAttach
é executado, que é sua chance de descobrir se deseja regenerar ou usar o componente na memória. Se você deseja reutilizar um componente armazenado, volte true
e você está no caminho certo!
- Agora o Angular perguntará a você "qual componente você deseja que usemos?", Que você indicará retornando o componente
DetachedRouteHandle
de retrieve
.
Essa é praticamente toda a lógica de que você precisa! No código para reuse-strategy.ts
, abaixo, também deixei uma função bacana que irá comparar dois objetos. Eu o utilizo para comparar as rotas futuras route.params
e route.queryParams
as armazenadas. Se todos eles corresponderem, quero usar o componente armazenado em vez de gerar um novo. Mas como você faz isso é com você!
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;
}
}
Comportamento
Essa implementação armazena todas as rotas exclusivas que o usuário visita no roteador exatamente uma vez. Isso continuará a ser adicionado aos componentes armazenados na memória durante a sessão do usuário no site. Se você quiser limitar as rotas que armazena, o lugar para fazer isso é o shouldDetach
método. Ele controla quais rotas você salva.
Exemplo
Digamos que seu usuário procure algo na página inicial, o que o leva ao caminho search/:term
, que pode aparecer como www.yourwebsite.com/search/thingsearchedfor
. A página de pesquisa contém vários resultados de pesquisa. Você gostaria de armazenar esta rota, caso eles queiram voltar a ela! Agora, eles clicam em um resultado de pesquisa e são direcionados para o view/:resultId
que você não deseja armazenar, visto que provavelmente estarão lá apenas uma vez. Com a implementação acima implementada, eu simplesmente mudaria o shouldDetach
método! Pode ser assim:
Primeiro, vamos criar uma série de caminhos que queremos armazenar.
private acceptedRoutes: string[] = ["search/:term"];
agora, shouldDetach
podemos verificar o em route.routeConfig.path
relação ao nosso 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
}
}
Como o Angular armazenará apenas uma instância de uma rota, esse armazenamento será leve e armazenaremos apenas o componente localizado emsearch/:term
e não todos os outros!
Links Adicionais
Embora ainda não haja muita documentação, aqui estão alguns links para o que existe:
Documentos angulares: https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html
Artigo de introdução: https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx
Implementação padrão de RouteReuseStrategy do nativescript-angular : https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts