Ответ 1
Эй, Андерс, отличный вопрос!
У меня почти такой же сценарий использования, как и у вас, и я хотел сделать то же самое! Поиск пользователя> получить результаты> Пользователь перемещается к результату> Пользователь перемещается назад> BOOM - молниеносный возврат к результатам, но вы не хотите сохранять конкретный результат, к которому переходил пользователь.
ТЛ; др
У вас должен быть класс, который реализует 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 {
}
Последняя часть - это написание класса, который будет контролировать, будут ли маршруты отсоединены, сохранены, извлечены и повторно присоединены. Прежде чем мы перейдем к старому копированию/вставке, я сделаю краткое объяснение механики здесь, насколько я понимаю. Ссылка на код ниже для методов, которые я описываю, и, конечно, там много документации в коде.
- Когда вы переходите, следует использовать
shouldReuseRoute
. Это немного странно для меня, но если он возвращаетtrue
, тогда он фактически использует маршрут, по которому вы в данный момент находитесь, и ни один из других методов не запускается. Я просто возвращаю false, если пользователь уходит. - Если
shouldReuseRoute
возвращаетfalse
,shouldDetach
срабатывает.shouldDetach
определяет, хотите ли вы сохранить маршрут, и возвращаетboolean
указывающее столько же. Именно здесь вы должны решить хранить/не хранить пути, что я бы сделал, проверив массив путей, которые вы хотите сохранить вroute.routeConfig.path
, и вернув false, еслиpath
не существует в массиве. - Если
shouldDetach
возвращаетtrue
,store
shouldDetach
, что дает вам возможность хранить любую информацию о маршруте, которую вы хотели бы получить. Что бы вы ни делали, вам нужно будет хранитьDetachedRouteHandle
потому что это то, что Angular использует для идентификации вашего сохраненного компонента позже. Ниже я храню иDetachedRouteHandle
иActivatedRouteSnapshot
в локальной переменной для моего класса.
Итак, мы увидели логику хранения, но как насчет перехода к компоненту? Как Angular решает перехватить вашу навигацию и поставить сохраненную на место?
- Опять же, после того, как
shouldReuseRoute
возвратилfalse
, следуетshouldAttach
, которыйshouldAttach
вам шанс выяснить, хотите ли вы восстановить или использовать компонент в памяти. Если вы хотите повторно использовать сохраненный компонент, вернитеtrue
и вы уже в пути! - Теперь 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
метод shouldDetach
. Он контролирует, какие маршруты вы сохраняете.
пример
Предположим, что ваш пользователь ищет что-то на домашней странице, где он переходит к search/:term
пути 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 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 Docs: 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