RouteReuseStrategyとは
その名の通り、RouteをReuseする為のStrategyになります。(おい)例えば、/hogeでHogeComponentが描画されたとして、そこから、/fugaへ画面遷移してFugaComponentが描画され、その後、ブラウザバック等で/hogeに戻りました、というときに、HogeComponentを画面遷移前のまま表示したりできます。
要するにキャッシュのような仕組みということですね。
(実際はAndroidのFragmentManagerのaddに近いのかな?)
パッと思いつくユースケースは、一覧 → 詳細 → 一覧へ戻る、とかでページング状態等を保持するとかでしょうか。
RouteReuseStrategyの実装
まずは、RouteReuseStrategyをimplします。
export class MyRouteReuseStrategy implements RouteReuseStrategy {}
Moduleにprovideも必要です。
providers:[
{ provide: RouteReuseStrategy, useClass: MyRouteReuseStrategy }
]
共通処理の実装
先に共通処理を実装します。
private storedHandles: { [path: string]: DetachedRouteHandle } = {};
private getPath(route: ActivatedRouteSnapshot) {
let url = route.pathFromRoot.filter(r => r.url.length > 0).map(r => r.url.map(u => u.path).join("/")).join("/");
return url.length > 0? url: null;
}
private isReuse(route: ActivatedRouteSnapshot) {
return route.routeConfig && !route.routeConfig.loadChildren &&
route.routeConfig.data && route.routeConfig.data.reuse;
}
それぞれ、
storedHandlesが、Componentのキャッシュに相当するものを保持しておく入れ物になります。
getPathが、routeからpathの文字列を取得する処理で、取得したpathはstoredHandlesの配列キーになります。
isReuseが、reuse対象かどうかの判定処理で、
・loadChildrenなrouteでないこと=そのrouteがmoduleを跨がないこと
・後述のroute.dataにreuseが指定されていること
としました。
RouteReuseStrategyで実装が必要なメソッドは5つ
shouldDetach、storeの実装
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return this.isReuse(route);
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
if (this.isReuse(route)) {
let path = this.getPath(route);
if (path) this.storedHandles[path] = handle;
}
}
shouldDetachは、そのrouteを残すかどうかを判定します。trueを返した場合、storeが呼ばれます。
storeは、実際に保持する処理になります。渡されてくるhandleをpath単位で保持する形としました。
shouldAttach、retrieveの実装
shouldAttach(route: ActivatedRouteSnapshot): boolean {
if (this.isReuse(route)) {
let path = this.getPath(route);
if (path) return !!this.storedHandles[path];
}
return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
if (this.isReuse(route)) {
let path = this.getPath(route);
if (path) return this.storedHandles[path] || null;
}
return null;
}
shouldAttachは、保持されているものを使うかどうか判定します。trueを返した場合、retrieveが呼ばれます。
retrieveは、実際に保持されているものを渡す処理になります。pathを利用して取得できたら返す形としました。ちなみに、nullを返した場合は通常どおりに新しいComponentが作成されます。
shouldReuseRouteの実装
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
shouldReuseRouteはreuseする対象かどうかの判定に使います。
futureとcurrはそれぞれ今と次のRouteで、同じrouteであることを条件とすることにしました。
これで一応の機構はできました。
Routesの定義時に上のisReuseで取得しているdata.reuseを設定したrouteが、対象になります。
Routesの定義
const HogeRoutes: Routes = [
{ path: '', component: HogeList, data: { reuse: true } },
{ path: ':id', component: HogeEdit }
];
この場合、HogeList(一覧)とHogeEdit(編集)画面のうち、一覧画面のみ保持され、編集画面から戻ってもHogeListのローカル変数等はそのままになります。
当然ですが、編集画面は:idで都度表示内容が変更される画面を想定しているので、reuseするのは望ましくないです。
さて、これでHogeListは使い回されることになるわけですが、この状態では、例えば、編集画面で項目が削除されたり作成されたとしても、一覧へ反映されないことになります。
もちろんお互いのComponentで同じデータの変更をsubscribeしているような造りであれば問題は無いのですが、それでなくとも、任意にキャッシュしているComponentを再生成したいときも出てきます。
retrieveがnullを返せばいいので、storedHandlesから消す処理があれば、実現できそうです。
refreshの実装
refresh(route: ActivatedRouteSnapshot) {
let path = this.getPath(route);
if (path && this.storedHandles[path]) this.storedHandles[path] = null;
}
これをHogeEditでSaveが呼ばれた場合等に以下のように呼び出します。
class HogeEdit {
constructor(route: ActivatedRoute, router: Router) {}
afterSave() {
(this.router.routeReuseStrategy as MyRouteReuseStrategy).refresh(this.route.parent.snapshot);
}
}
RouteReuseStrategyはRouterが持っているので、そのままアクセスできます。
refreshに渡すのは保持されているかもしれないrouteのsnapshotなので、今回はparentから取っています。
これで、afterSaveを通った後にHogeListへ戻った場合に限り、Componentが使い回されなくなりました。
以下、クラス全体です。
最低限は満たせましたが、実際はもう少し調整が必要になるケースが多いかと思います。
export class MyRouteReuseStrategy implements RouteReuseStrategy {
private storedHandles: { [path: string]: DetachedRouteHandle } = {};
private getPath(route: ActivatedRouteSnapshot) {
let url = RouteUtil.getPathFromRoot(route);
return url.length > 0? url: null;
}
private isReuse(route: ActivatedRouteSnapshot) {
return route.routeConfig && !route.routeConfig.loadChildren &&
route.routeConfig.data && route.routeConfig.data.reuse;
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return this.isReuse(route);
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
if (this.isReuse(route)) {
let path = this.getPath(route);
if (path) this.storedHandles[path] = handle;
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
if (this.isReuse(route)) {
let path = this.getPath(route);
if (path) return !!this.storedHandles[path];
}
return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
if (this.isReuse(route)) {
let path = this.getPath(route);
if (path) return this.storedHandles[path] || null;
}
return null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
refresh(route: ActivatedRouteSnapshot) {
let path = this.getPath(route);
if (path && this.storedHandles[path]) this.storedHandles[path] = null;
}
}
いじょ