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; } }
いじょ