ルーティングを実装した Angular 2 モジュールがあり、ナビゲーション時に状態を保存したいと考えています。
ユーザーは次のことができるはずです。
- 「検索式」を使用してドキュメントを検索する
- 結果の1つに移動する
- サーバーと通信せずに「検索結果」に戻る
これは、 を含めて可能ですRouteReuseStrategy
。
質問は、
ドキュメントを保存しないようにするにはどうすればよいかということです。
つまり、ルート パス「documents」の状態は保存する必要があり、ルート パス「documents/:id」の状態は保存しない必要があるのでしょうか?
ベストアンサー1
アンダースさん、素晴らしい質問ですね!
私もあなたとほぼ同じユースケースを持っていて、同じことをしたいと思っていました。ユーザー検索 > 結果を取得 > ユーザーが結果に移動 > ユーザーが戻る >ブーム 結果への驚異的な速さの復帰ただし、ユーザーが移動した特定の結果は保存したくない場合があります。
要約
RouteReuseStrategy
を実装し、戦略を提供するクラスが必要ですngModule
。ルートが保存されるタイミングを変更したい場合は、関数を変更しますshouldDetach
。 が返されるとtrue
、Angularはルートを保存します。ルートがアタッチされるタイミングを変更したい場合は、関数を変更します。 がtrueを返すshouldAttach
とshouldAttach
、Angularは要求されたルートの代わりに保存されたルートを使用します。プランカーぜひ遊んでみてください。
RouteReuseStrategyについて
この質問をすることで、RouteReuseStrategyを使用するとAngularに次のことを伝えることができることがすでに理解できます。ないコンポーネントを破棄するのではなく、後日再レンダリングするために保存します。これは次のことを可能にするので便利です。
- 減少サーバー呼び出し
- 増加スピード
- そしてデフォルトでは、コンポーネントはそのままの状態でレンダリングされます。
最後の項目は、例えばユーザーが入力したにもかかわらず一時的にページを離れる場合に重要です。多くテキストを入力できます。エンタープライズアプリケーションでは、この機能が役立ちます。過度のフォームの量!
これが問題を解決するために私が思いついた方法です。おっしゃるとおり、RouteReuseStrategy
バージョン 3.4.1 以降では @angular/router が提供するものを使用する必要があります。
やるべきこと
初めプロジェクトに @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
、path
配列内に が存在しない場合は false を返します。shouldDetach
が を返す場合true
、store
が起動され、ルートに関する必要な情報を保存できるようになります。 何をするにしても、 を保存する必要があります。これは、Angular が後で保存されたコンポーネントを識別するために使用するものだからです。以下では、と の両方をクラスのローカル変数にDetachedRouteHandle
保存しています。DetachedRouteHandle
ActivatedRouteSnapshot
ストレージのロジックについては説明しましたが、ナビゲーションについてはどうでしょうかにコンポーネントですか? Angular はどのようにしてナビゲーションをインターセプトし、保存されているナビゲーションをその場所に配置するのでしょうか?
- 再度、
shouldReuseRoute
が返された後にfalse
がshouldAttach
実行され、メモリ内のコンポーネントを再生成するか使用するかを判断する機会となります。保存されたコンポーネントを再利用する場合は、 を返せば準備true
完了です。 - ここで、Angular は「どのコンポーネントを使用しますか?」と尋ねます。そのコンポーネントの
DetachedRouteHandle
fromを返すことでそれを示しますretrieve
。
必要なロジックはこれでほぼすべてです。reuse-strategy.ts
以下の のコードでは、2つのオブジェクトを比較する便利な関数も用意しました。これを使用して、将来のルートroute.params
とroute.queryParams
保存されているものを比較します。これらがすべて一致する場合は、新しいコンポーネントを生成するのではなく、保存されているコンポーネントを使用します。ただし、その方法は次のとおりです。あなた次第!
再利用戦略.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;
}
}
行動
この実装では、ユーザーがルーターでアクセスするすべての一意のルートが 1 回だけ保存されます。これにより、サイトでのユーザーのセッション全体にわたって、メモリに保存されるコンポーネントが追加され続けます。保存するルートを制限したい場合は、メソッドで行います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は1つのインスタンスのみ保存するルートの場合、このストレージは軽量になり、 にあるコンポーネントのみが保存されsearch/:term
、他のすべてのコンポーネントは保存されません。
追加リンク
まだ多くのドキュメントは公開されていませんが、存在するものへのリンクをいくつか示します。
Angular ドキュメント:https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html
nativescript-angularのデフォルト実装ルート再利用戦略:https://github.com/NativeScript/nativescript-angular/blob/cb4fd3a/nativescript-angular/router/ns-route-reuse-strategy.ts