Как реализовать RouteReuseStrategy shouldDetach для определенных маршрутов в Angular 2

117

У меня есть модуль Angular 2, в котором я реализовал маршрутизацию и хотел бы, чтобы состояния сохранялись при навигации.
Пользователь должен уметь:

  1. поиск документов по "формуле поиска"
  2. перейдите к одному из результатов
  3. вернуться к "результату поиска" - без связи с сервером

Это возможно в том числе RouteReuseStrategy.
Возникает вопрос:
как сделать так, чтобы документ не сохранялся?

Значит, состояние "документы" пути маршрута должно быть сохранено, а состояние пути "документы /: id" НЕ должно сохраняться?

Андерс Грэм Майджинд
источник

Ответы:

211

Привет, Андерс, отличный вопрос!

У меня почти такой же вариант использования, как и у вас, и я хотел сделать то же самое! Поиск пользователя> получить результаты> Пользователь переходит к результату> Пользователь переходит назад> БУМ молниеносно быстро возвращается к результатам , но вы не хотите сохранять конкретный результат, к которому перешел пользователь.

tl; dr

Вам нужен класс, который реализует RouteReuseStrategyи предоставляет вашу стратегию в ngModule. Если вы хотите изменить время сохранения маршрута, измените shouldDetachфункцию. Когда он возвращается true, Angular сохраняет маршрут. Если вы хотите изменить, когда маршрут прикреплен, измените shouldAttachфункцию. Когда shouldAttachвозвращается true, Angular будет использовать сохраненный маршрут вместо запрошенного маршрута. Вот вам Plunker , с которым можно поиграть.

О RouteReuseStrategy

Задав этот вопрос, вы уже понимаете, что RouteReuseStrategy позволяет вам указать Angular не уничтожать компонент, а фактически сохранить его для повторного рендеринга в будущем. Это круто, потому что позволяет:

  • Снижение обращений к серверу
  • Повышенная скорость
  • И компонент по умолчанию отображает в том же состоянии, в котором он был

Последнее важно, если вы хотите, скажем, временно покинуть страницу, даже если пользователь ввел на нее много текста. Корпоративным приложениям понравится эта функция из-за чрезмерного количества форм!

Это то, что я придумал для решения проблемы. Как вы сказали, вам нужно использовать RouteReuseStrategy@ angular / router в версиях 3.4.1 и выше.

ДЕЛАТЬ

Сначала убедитесь, что в вашем проекте используется @ angular / router версии 3.4.1 или выше.

Затем создайте файл, в котором будет размещен ваш класс, реализующий RouteReuseStrategy. Я позвонил в свою reuse-strategy.tsи положил в /appпапку на хранение. На данный момент этот класс должен выглядеть так:

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

export class CustomReuseStrategy implements RouteReuseStrategy {
}

(не беспокойтесь о своих ошибках TypeScript, мы все решим)

Закончите основу , предоставив урок вашему app.module. Учтите, что вы еще не написали CustomReuseStrategy, но следует продолжить, и importэто reuse-strategy.tsвсе равно. Такжеimport { RouteReuseStrategy } from '@angular/router';

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

Последняя часть - это написание класса, который будет контролировать, будут ли маршруты отсоединяться, сохраняться, извлекаться и повторно присоединяться. Прежде чем мы перейдем к старому копированию / вставке , я сделаю здесь краткое объяснение механики, насколько я понимаю. Ссылайтесь на приведенный ниже код для описываемых мной методов, и, конечно же, в коде есть много документации .

  1. При навигации shouldReuseRouteгорит. Для меня это немного странно, но если он вернетсяtrue , то он фактически повторно использует маршрут, на котором вы сейчас находитесь, и ни один из других методов не запускается. Я просто возвращаю false, если пользователь уходит.
  2. Если shouldReuseRouteвозвращается false, shouldDetachсрабатывает. shouldDetachопределяет, хотите ли вы сохранить маршрут, и возвращает это значение boolean. Здесь вы должны решить хранить / не хранить пути , что я бы сделал, проверив массив путей, которые вы хотите сохранить route.routeConfig.path, и вернув false, если pathв массиве не существует.
  3. Если shouldDetachвозвращается true, storeзапускается, что дает вам возможность сохранить любую информацию о маршруте, которую вы хотите. Что бы вы ни делали, вам нужно будет сохранить, DetachedRouteHandleпотому что это то, что Angular использует для идентификации вашего сохраненного компонента позже. Ниже я сохраняю и DetachedRouteHandleи ActivatedRouteSnapshotв переменной, локальной для моего класса.

Итак, мы увидели логику хранилища, но как насчет перехода к компоненту? Как Angular решает перехватить вашу навигацию и поместить сохраненную на место?

  1. Опять же , после того, как shouldReuseRouteвернулся false, shouldAttachпрогоны, что это ваш шанс , чтобы выяснить , хотите ли вы , чтобы восстановить или использовать компонент в памяти. Если вы хотите повторно использовать сохраненный компонент, возвращайтесь, trueи все в порядке!
  2. Теперь Angular спросит вас: «Какой компонент вы хотите, чтобы мы использовали?», Который вы укажете, вернув этот компонент DetachedRouteHandleиз retrieve.

Это почти вся необходимая логика! В приведенном ниже коде reuse-strategy.tsя также оставил вам отличную функцию, которая сравнивает два объекта. Я использую его для сравнения будущего маршрута route.paramsи route.queryParamsсохраненного. Если все они совпадают, я хочу использовать сохраненный компонент вместо создания нового. Но как вы это сделаете, решать только вам!

повторное использование-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;
    }
}

Поведение

Эта реализация хранит каждый уникальный маршрут, который пользователь посещает на маршрутизаторе ровно один раз. Это будет продолжать добавляться к компонентам, хранящимся в памяти, на протяжении всего сеанса пользователя на сайте. Если вы хотите ограничить маршруты, которые вы храните, место для этого - shouldDetachметод. Он контролирует, какие маршруты вы сохраняете.

пример

Предположим, ваш пользователь что-то ищет на домашней странице, которая направляет их по пути search/:term, который может выглядеть как www.yourwebsite.com/search/thingsearchedfor. Страница поиска содержит несколько результатов поиска. Вы хотите сохранить этот маршрут на случай, если они захотят по нему вернуться! Теперь они нажимают на результат поиска и переходят к нему view/:resultId, который вы не хотите сохранять, так как они, вероятно, будут там только один раз. Имея вышеуказанную реализацию, я бы просто изменил shouldDetachметод! Вот как это может выглядеть:

Прежде всего, давайте создадим массив путей, которые мы хотим сохранить.

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

теперь shouldDetachмы можем проверить route.routeConfig.pathнаш массив.

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

Поскольку Angular будет хранить только один экземпляр маршрута, это хранилище будет облегченным, и мы будем хранить только компонент, расположенный в, search/:termа не все остальные!

Дополнительные ссылки

Хотя документации пока не так много, вот пара ссылок на то, что действительно существует:

Документы по Angular: https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html

Вступительная статья: https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx

Реализация RouteReuseStrategy по умолчанию в nativescript-angular : https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts

Корбфон
источник
2
@shaahin Я добавил пример, который представляет собой точный код, содержащийся в моей текущей реализации!
Corbfon
1
@Corbfon Я также открыл проблему на официальной странице github: github.com/angular/angular/issues/13869
Tjaart van der Walt,
2
Есть ли способ заставить его повторно запускать анимацию ввода при повторной активации сохраненного маршрута?
Jinder Сидх
2
ReuseRouteStrategy вернет ваш компонент маршрутизатору, так что он будет в том состоянии, в котором он был оставлен. Если вы хотите, чтобы компонент (ы) реагировал на вложение, вы можете использовать службу, которая предлагает Observable. Компонент должен подписаться на ловушку Observableво время ngOnInitжизненного цикла. Тогда вы сможете определить компонент по тому ReuseRouteStrategy, что он только что был прикреплен, и компонент может изменить свое состояние по мере необходимости.
Corbfon
1
@AndersGramMygind, если мой ответ дает ответ на предложенный вами вопрос, вы бы отметили его как ответ?
Corbfon
45

Не пугайтесь принятого ответа, это довольно просто. Вот быстрый ответ, что вам нужно. Я бы рекомендовал хотя бы прочитать принятый ответ, поскольку он полон мельчайших подробностей.

Это решение не выполняет никакого сравнения параметров, как принятый ответ, но оно отлично подойдет для хранения набора маршрутов.

app.module.ts импортирует:

import { RouteReuseStrategy } from '@angular/router';
import { CustomReuseStrategy, Routing } from './shared/routing';

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

общий / 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;
 }
}
Крис Фремген
источник
1
Будет ли это работать и для ленивых маршрутов?
bluePearl
@bluePearl Проверьте ответ ниже
Крис Фремген
2
routeConfig имеет значение null для разных маршрутов, поэтому shouldReuseRoute всегда будет возвращать значение true, что не является желаемым поведением,
Гил Эпштейн,
20

В дополнение к принятому ответу (Корбфон) и более короткому и прямому объяснению Криса Фремгена я хочу добавить более гибкий способ обработки маршрутов, который должен использовать стратегию повторного использования.

Оба ответа сохраняют маршруты, которые мы хотим кэшировать, в массиве, а затем проверяем, находится ли текущий путь маршрута в массиве или нет. Эта проверка выполняется в shouldDetachметоде.

Я считаю этот подход негибким, потому что, если мы хотим изменить имя маршрута, нам нужно не забыть также изменить имя маршрута в нашем CustomReuseStrategyклассе. Мы можем либо забыть его изменить, либо другой разработчик в нашей команде может решить изменить имя маршрута, даже не зная о существовании RouteReuseStrategy.

Вместо того, чтобы хранить маршруты, которые мы хотим кэшировать в массиве, мы можем пометить их непосредственно с RouterModuleпомощью dataобъекта. Таким образом, даже если мы изменим имя маршрута, стратегия повторного использования все равно будет применяться.

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

И затем shouldDetachмы используем это в методе.

shouldDetach(route: ActivatedRouteSnapshot): boolean {
  return route.data.reuseRoute === true;
}
Давор
источник
1
Хорошее решение. Это действительно должно быть просто встроено в стандартную стратегию повторного использования углового маршрута с простым флагом, как вы применили.
MIP1983
Отличный ответ. Большое спасибо!
claudiomatiasrg
14

Чтобы использовать стратегию Криса Фремгена с отложенной загрузкой модулей, измените класс CustomReuseStrategy на следующее:

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

наконец, в файлах маршрутизации ваших функциональных модулей определите свои ключи:

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

Больше информации здесь .

Угур Динч
источник
1
Спасибо, что добавили это! Я должен попробовать. Это может даже исправить некоторые проблемы с обработкой дочернего маршрута, с которыми сталкивается мое решение.
Corbfon
Пришлось использовать route.data["key"]для сборки без ошибок. Но проблема в том, что у меня есть компонент route +, который используется в двух разных местах. 1. sample/list/itemи 2. product/id/sample/list/itemкогда я впервые загружаю один из путей, он загружается нормально, но другой выдает повторно прикрепленную ошибку, потому что я сохраняю на основе list/itemИтак, моя работа заключается в том, что я дублировал маршрут и внес некоторые изменения в путь URL, но отображал тот же компонент. Не уверен, есть ли для этого другой способ.
bluePearl
Это сбило меня с толку, вышеперечисленное просто не сработает, оно взорвется, как только я попаду на один из маршрутов кеширования (он больше не будет перемещаться и там, где ошибки в консоли). Решение Криса Фремгена, кажется, отлично работает с моими ленивыми модулями, насколько я могу судить ...
MIP1983
12

Еще одна реализация, более достоверная, полная и многоразовая. Он поддерживает модули с отложенной загрузкой, такие как @ Uğur Dinç, и интегрирует флаг данных маршрута @Davor. Лучшее улучшение - автоматическое создание (почти) уникального идентификатора на основе абсолютного пути к странице. Таким образом, вам не нужно определять его самостоятельно на каждой странице.

Отметьте любую страницу, которую вы хотите кэшировать reuseRoute: true. Он будет использован в shouldDetachметоде.

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

Это самая простая реализация стратегии без сравнения параметров запроса.

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

Здесь также сравниваются параметры запроса. compareObjectsимеет небольшое улучшение по сравнению с версией @Corbfon: перебирает свойства как базовых, так и сравниваемых объектов. Помните, что вы можете использовать внешнюю и более надежную реализацию, например isEqualметод 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;
  }
}

Если у вас есть лучший способ сгенерировать уникальные ключи, прокомментируйте мой ответ, я обновлю код.

Спасибо всем ребятам, которые поделились своим решением.

Макгиоген
источник
3
Это должен быть принятый ответ. Многие решения, представленные выше, не могут поддерживать несколько страниц с одним и тем же дочерним URL-адресом. Потому что они сравнивают активированный URL-адрес, который не является полным путем.
zhuhang.jasper 01
4

Все упомянутые решения в нашем случае оказались недостаточными. У нас есть приложение для малого бизнеса с:

  1. Страница введения
  2. Страница авторизации
  3. Приложение (после входа в систему)

Наши требования:

  1. Лениво загружаемые модули
  2. Многоуровневые маршруты
  3. Сохранять все состояния маршрутизатора / компонентов в памяти в разделе приложения
  4. Возможность использовать стратегию повторного использования angular по умолчанию на определенных маршрутах
  5. Уничтожение всех компонентов, хранящихся в памяти, при выходе из системы

Упрощенный пример наших маршрутов:

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

Стратегия повторного использования:

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
источник
3

Следующее - работа! ссылка: 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, '_')
    }
}

红兵 伍
источник
1
Осторожно, здесь используется внутренняя переменная _routerState.
DarkNeuron
@DarkNeuron _routerStateпричиняет вред?
k11k2
2
Нет, но Google не обязан хранить эту переменную, поскольку она используется для внутренних целей и не отображается в API.
DarkNeuron
когда мы звоним deleteRouteSnapshot?
k11k2
1

Я столкнулся с этими проблемами при реализации стратегии повторного использования пользовательского маршрута:

  1. Выполнение операций над присоединением / отсоединением маршрута: управление подписками, очистка и т. Д .;
  2. Сохранить только последнее параметризованное состояние маршрута: оптимизация памяти;
  3. Повторно используйте компонент, а не состояние: управляйте состоянием с помощью инструментов управления состоянием.
  4. Ошибка «Не удается повторно подключить ActivatedRouteSnapshot, созданный на другом маршруте»;

Поэтому я написал библиотеку, решающую эти проблемы. Библиотека предоставляет службу и декораторы для присоединения / отсоединения перехватчиков и использует компоненты маршрута для хранения отсоединенных маршрутов, а не путей маршрута.

Пример:

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

Библиотека: https://www.npmjs.com/package/ng-cache-route-reuse

Стас Амасев
источник
Просто ссылка на вашу собственную библиотеку или учебник - не лучший ответ. Ссылки на него, объяснение, почему он решает проблему, предоставление кода о том, как это сделать, и отказ от того, что вы его написали, дают лучший ответ. См .: Что означает «хорошая» самореклама?
Пол Руб,