Como implementar RouteReuseStrategy shouldDetach para rotas específicas no Angular 2

113

Tenho um módulo Angular 2 no qual implementei o roteamento e gostaria que os estados fossem armazenados durante a navegação. O usuário deve ser capaz de: 1. pesquisar documentos usando uma fórmula de pesquisa 2. navegar até um dos resultados 3. navegar de volta para pesquisar resultado - sem se comunicar com o servidor

Isso é possível incluindo RouteReuseStrategy. A pergunta é: Como faço para que o documento não seja armazenado?

Portanto, o estado do caminho da rota "documentos" deve ser armazenado e o estado do caminho da rota "documentos /: id" 'NÃO deve ser armazenado?

Anders Gram Mygind
fonte

Respostas:

209

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 RouteReuseStrategye forneça sua estratégia no ngModule. Se você deseja modificar quando a rota é armazenada, modifique a shouldDetachfunção. Quando retorna true, o Angular armazena a rota. Se você deseja modificar quando a rota é anexada, modifique a shouldAttachfunção. Quando shouldAttachretorna 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 RouteReuseStrategyoferecido 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.tse coloquei-o na /apppasta 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 importdesde reuse-strategy.tssempre. 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 .

  1. Quando você navega, shouldReuseRoutedispara. 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.
  2. Se shouldReuseRouteretornar false, shouldDetachdispara. shouldDetachdetermina se você deseja ou não armazenar a rota e retorna um booleanindicando 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.pathe retornando false se pathnão existir na matriz.
  3. Se shouldDetachretornar 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 DetachedRouteHandleporque é isso que o Angular usa para identificar seu componente armazenado mais tarde. Abaixo, eu armazeno o DetachedRouteHandlee o ActivatedRouteSnapshotem 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?

  1. Mais uma vez, depois de shouldReuseRouteretornar 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 truee você está no caminho certo!
  2. Agora o Angular perguntará a você "qual componente você deseja que usemos?", Que você indicará retornando o componente DetachedRouteHandlede 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.paramse route.queryParamsas 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 shouldDetachmé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/:resultIdque você não deseja armazenar, visto que provavelmente estarão lá apenas uma vez. Com a implementação acima implementada, eu simplesmente mudaria o shouldDetachmétodo! Pode ser assim:

Primeiro, vamos criar uma série de caminhos que queremos armazenar.

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

agora, shouldDetachpodemos verificar o em route.routeConfig.pathrelaçã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

Corbfon
fonte
2
@shaahin Eu adicionei um exemplo, que é o código exato contido em minha implementação atual!
Corbfon
1
@Corbfon Também abri um exemplar na página oficial do github: github.com/angular/angular/issues/13869
Tjaart van der Walt
2
Existe uma maneira de fazer com que ele execute novamente as animações de entrada ao reativar uma rota armazenada?
Jinder Sidhu
2
O ReuseRouteStrategy entregará seu componente de volta ao roteador, para que fique no estado em que foi deixado. Se desejar que o (s) componente (s) reaja (m) ao anexo, você pode usar um serviço que ofereça um Observable. O componente deve assinar o Observabledurante o ngOnInitgancho do ciclo de vida. Então você será capaz de dizer ao componente, a partir do ReuseRouteStrategy, que ele acabou de ser anexado e o componente pode modificar seu estado conforme o ajuste.
Corbfon
1
@AndersGramMygind se minha resposta fornecer uma resposta para a pergunta que você propôs, você marcaria como a resposta?
Corbfon
44

Não se intimide com a resposta aceita, isso é bastante simples. Aqui está uma resposta rápida do que você precisa. Eu recomendaria pelo menos ler a resposta aceita, pois ela é cheia de detalhes.

Esta solução não faz nenhuma comparação de parâmetro como a resposta aceita, mas funcionará bem para armazenar um conjunto de rotas.

importações de 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
fonte
1
Isso também funcionará para rotas com carregamento lento?
bluePearl
@bluePearl Verifique a resposta abaixo
Chris Fremgen
2
routeConfig é nulo, para rotas diferentes, portanto, shouldReuseRoute sempre retornará verdadeiro, o que não é o comportamento desejado
Gil Epshtain
19

Além da resposta aceita (por Corbfon) e da explicação mais curta e direta de Chris Fremgen, desejo adicionar uma maneira mais flexível de lidar com as rotas que devem usar a estratégia de reutilização.

Ambas as respostas armazenam as rotas que queremos armazenar em cache em uma matriz e, em seguida, verificam se o caminho da rota atual está na matriz ou não. Esta verificação é feita no shouldDetachmétodo.

Acho essa abordagem inflexível porque, se quisermos alterar o nome da rota, precisaremos lembrar de também alterar o nome da rota em nossa CustomReuseStrategyclasse. Podemos esquecer de alterá-lo ou algum outro desenvolvedor de nossa equipe pode decidir alterar o nome da rota sem saber da existência de RouteReuseStrategy.

Em vez de armazenar as rotas que queremos armazenar em cache em um array, podemos marcá-las diretamente em RouterModuleusing dataobject. Dessa forma, mesmo se alterarmos o nome da rota, a estratégia de reutilização ainda será aplicada.

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

E então, no shouldDetachmétodo, fazemos uso disso.

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}
Davor
fonte
1
Boa solução. Isso realmente deve ser incorporado à estratégia de reutilização de rota angular padrão com um sinalizador simples como o que você aplicou.
MIP1983
Ótima resposta. Muito obrigado!
claudiomatiasrg
14

Para usar a estratégia de Chris Fremgen com módulos carregados lentamente, modifique a classe CustomReuseStrategy para o seguinte:

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

finalmente, nos arquivos de roteamento de seus módulos de recursos, defina suas chaves:

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

Mais informações aqui .

Uğur Dinç
fonte
1
Obrigado por adicionar isso! Eu tenho que tentar. Ele pode até mesmo corrigir alguns dos problemas de manipulação de rota secundária que minha solução encontra.
Corbfon
Tive que usar route.data["key"]para construir sem erros. Mas o problema que estou tendo é que tenho um componente de rota + que é usado em dois lugares diferentes. 1. sample/list/iteme 2. product/id/sample/list/itemquando carrego pela primeira vez qualquer um dos caminhos, ele carrega bem, mas o outro gera o erro reanexado porque estou armazenando com base em list/itemEntão, minha solução é duplicar a rota e fazer algumas alterações no caminho do url, mas exibindo o mesmo componente. Não tenho certeza se há outra solução alternativa para isso.
bluePearl
Isso meio que me confundiu, o acima simplesmente não funcionava, iria explodir assim que eu acertasse uma das minhas rotas de cache, (ele não iria mais navegar e haveria erros no console). A solução de Chris Fremgen parece funcionar bem com meus módulos lazy até agora ...
MIP1983
11

Outra implementação mais válida, completa e reutilizável. Este suporta módulos carregados lentamente como @ Uğur Dinç e integra o sinalizador de dados de rota @Davor. A melhor melhoria é a geração automática de um identificador (quase) exclusivo com base no caminho absoluto da página. Dessa forma, você não precisa defini-lo sozinho em todas as páginas.

Marque qualquer página que você deseja configurar em cache reuseRoute: true. Será usado no shouldDetachmétodo.

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

Esta é a implementação de estratégia mais simples, sem comparar parâmetros de consulta.

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

Este também compara os parâmetros da consulta. compareObjectstem uma pequena melhoria em relação à versão @Corbfon: faz um loop pelas propriedades de ambos os objetos de base e de comparação. Lembre-se de que você pode usar uma implementação externa e mais confiável, como o isEqualmétodo 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 você tiver a melhor maneira de gerar chaves exclusivas, comente minha resposta, atualizarei o código.

Obrigado a todos os caras que compartilharam sua solução.

McGiogen
fonte
3
Esta deve ser a resposta aceita. Muitas das soluções fornecidas acima não oferecem suporte a várias páginas com o mesmo URL filho. Porque eles estão comparando a URL enabledRoute, que não é o caminho completo.
zhuhang.jasper
4

Todas as soluções mencionadas foram de alguma forma insuficientes em nosso caso. Temos um aplicativo de negócios menor com:

  1. Página de introdução
  2. Página de login
  3. App (após login)

Nossos requisitos:

  1. Módulos de carregamento lento
  2. Rotas multinível
  3. Armazene todos os estados do roteador / componente na memória na seção do aplicativo
  4. Opção de usar a estratégia de reutilização angular padrão em rotas específicas
  5. Destruindo todos os componentes armazenados na memória no logout

Exemplo simplificado de nossas rotas:

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

Estratégia de reutilização:

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
fonte
3

o seguinte é trabalho! referência: 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, '_')
    }
}

红兵 伍
fonte
1
Cuidado, isso usa uma variável interna _routerState.
DarkNeuron de
@DarkNeuron _routerStatecausa algum dano ?
k11k2
2
Não, mas o Google não tem obrigação de manter essa variável por perto, já que ela é usada internamente e não exposta na API.
DarkNeuron
quando estamos ligando deleteRouteSnapshot?
k11k2
0

Eu enfrentei esses problemas ao implementar uma estratégia de reutilização de rota personalizada:

  1. Realizar operações em anexar / desanexar uma rota: gerenciar assinaturas, limpar etc .;
  2. Preserve apenas o estado da última rota parametrizada: otimização de memória;
  3. Reutilize um componente, não um estado: gerencie o estado com ferramentas de gerenciamento de estado.
  4. Erro "Não é possível reanexar ActivatedRouteSnapshot criado a partir de uma rota diferente";

Então, escrevi uma biblioteca resolvendo esses problemas. A biblioteca fornece um serviço e decoradores para anexar / desconectar ganchos e usa os componentes de uma rota para armazenar rotas desconectadas, não os caminhos de uma rota.

Exemplo:

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

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

Stas Amasev
fonte
Apenas criar um link para sua própria biblioteca ou tutorial não é uma boa resposta. Vincular a ele, explicar por que ele resolve o problema, fornecer o código sobre como fazer isso e negar que você o escreveu é uma resposta melhor. Veja: O que significa “boa” autopromoção?
Paul Roub