Angular 2 implementing RouteReuseStrategy

2019-02-19 20:15发布

问题:

I implemented RouteReuseStrategy suggested here and also updated a bit, because on shouldAttach the routeConfig.path was empty, and the handler was not cached. I have @angular/router: 3.4.7.

import {RouteReuseStrategy, DetachedRouteHandle, ActivatedRouteSnapshot} from "@angular/router"

export class CustomReuseStrategy implements RouteReuseStrategy {

    handlers: {[key: string]: DetachedRouteHandle} = {};
    currentPath: string = '';

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        return true
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        this.handlers[route.routeConfig.path] = handle
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        // todo route.routeConfig.path was empty
        if (!!this.currentPath && !!this.handlers[this.currentPath]) {
            route.routeConfig.path = this.currentPath;
            return true
        } else {
            return false
        }
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig) return null;
        return this.handlers[route.routeConfig.path]
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        this.currentPath = curr.routeConfig && curr.routeConfig.path;

        return future.routeConfig === curr.routeConfig
    }

    deleteHandler(handler:string) {
        this.handlers[handler] && delete this.handlers[handler]
    }

}

Everything is working, except when I try to navigate back the 3rd time to a route I got the following error:

EXCEPTION: Uncaught (in promise): Error: Cannot reattach ActivatedRouteSnapshot with a different number of children
Error: Cannot reattach ActivatedRouteSnapshot with a different number of children
    at setFutureSnapshotsOfActivatedRoutes (http://localhost:4200/js/vendor.bundle.js:93110:15) [angular]
    at createNode (http://localhost:4200/js/vendor.bundle.js:93091:9) [angular]
    at http://localhost:4200/js/vendor.bundle.js:93131:16 [angular]
    at Array.map (native) [angular]
    at createOrReuseChildren (http://localhost:4200/js/vendor.bundle.js:93124:26) [angular]
    at createNode (http://localhost:4200/js/vendor.bundle.js:93086:41) [angular]
    at http://localhost:4200/js/vendor.bundle.js:93128:24 [angular]
    at Array.map (native) [angular]
    at createOrReuseChildren (http://localhost:4200/js/vendor.bundle.js:93124:26) [angular]
    at createNode (http://localhost:4200/js/vendor.bundle.js:93086:41) [angular]
    at createRouterState (http://localhost:4200/js/vendor.bundle.js:93072:33) [angular]
    at MapSubscriber.project (http://localhost:4200/js/vendor.bundle.js:26366:153) [angular]
    at MapSubscriber._next (http://localhost:4200/js/vendor.bundle.js:15890:35) [angular]
    at MapSubscriber.Subscriber.next (http://localhost:4200/js/vendor.bundle.js:4861:18) [angular]
    at MergeMapSubscriber.notifyNext (http://localhost:4200/js/vendor.bundle.js:19133:30) [angular]
    at InnerSubscriber._next (http://localhost:4200/js/vendor.bundle.js:106234:21) [angular]
    at InnerSubscriber.Subscriber.next (http://localhost:4200/js/vendor.bundle.js:4861:18) [angular]
    at MapSubscriber._next (http://localhost:4200/js/vendor.bundle.js:15896:26) [angular]
    at MapSubscriber.Subscriber.next (http://localhost:4200/js/vendor.bundle.js:4861:18) [angular]
    at ReduceSubscriber._complete (http://localhost:4200/js/vendor.bundle.js:36808:30) [angular]
    at ReduceSubscriber.Subscriber.complete (http://localhost:4200/js/vendor.bundle.js:4886:18) [angular]
    at MergeMapSubscriber._complete (http://localhost:4200/js/vendor.bundle.js:19125:30) [angular]
    at MergeMapSubscriber.Subscriber.complete (http://localhost:4200/js/vendor.bundle.js:4886:18) [angular]
    at ArrayObservable._subscribe (http://localhost:4200/js/vendor.bundle.js:12465:24) [angular]
    at ArrayObservable.Observable._trySubscribe (http://localhost:4200/js/vendor.bundle.js:221:25) [angular]
    at ArrayObservable.Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:209:27) [angular]
    at MergeMapOperator.call (http://localhost:4200/js/vendor.bundle.js:19075:23) [angular]
    at Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:206:22) [angular]
    at ReduceOperator.call (http://localhost:4200/js/vendor.bundle.js:36763:23) [angular]
    at Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:206:22) [angular]
    at MapOperator.call (http://localhost:4200/js/vendor.bundle.js:15867:23) [angular]
    at Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:206:22) [angular]
    at Object.subscribeToResult (http://localhost:4200/js/vendor.bundle.js:5096:27) [angular]
...

回答1:

I have same problem and was trying for last one week. Seems like it works fine for me now. Main challenge is defining a unique key for any given routes. I find RouteReuseStrategy become bit complex to understand and define key for each route when your configure you app with child routing and lazy loading. I try to explain my understanding and I hope this information helpful.

RouteReuseStrategy
I was trying to figure out the RouteReuseStrategy method calls and parameter details to generate unique key and come up with following diagram by debugging my application. Noticed sometimes stage parameters passed to method calls and try to ignore those parameters in RouteReuseStrategy implementation.

CustomReuseStrategy

RouteReuseStrategy Implementation:

/* tslint:disable */
import {ActivatedRouteSnapshot, DetachedRouteHandle, Params, RouteReuseStrategy} from "@angular/router";

interface RouteStorageObject {
    snapshot: ActivatedRouteSnapshot;
    handle: DetachedRouteHandle;
}

export class CustomReuseStrategy implements RouteReuseStrategy {
    storedRoutes: { [key: string]: RouteStorageObject } = {};

    getFurthestDecendantParams(route: ActivatedRouteSnapshot, sum: any): ActivatedRouteSnapshot {
        if (route.children.length > 0) {
            let child: ActivatedRouteSnapshot = route.children[0];
            sum.sum                           = sum.sum + this.sumParams(child.params);
            return this.getFurthestDecendantParams(child, sum);
        }
        return route;
    }

    sumParams(params: Params): string {
        return Object.keys(params).reduce((param, key) => {
            return param + params[key];
        }, "");
    }

    calcKey(route: ActivatedRouteSnapshot) {
        let paramKey = {
            sum: ""
        }
        if (route.children.length > 0) {
            this.getFurthestDecendantParams(route, paramKey).params;
        } else {
            paramKey.sum = this.sumParams(route.params);
        }
        if (paramKey.sum != "") {
            paramKey.sum = "_" + paramKey.sum;
        }
        return route.data.key + paramKey.sum;
    }

    public shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.info("CustomReuseStrategy.shouldDetach() - route key: " + this.calcKey(route));
        return !("product" === this.calcKey(route));
    }


    public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        let storedRoute: RouteStorageObject = {
            snapshot: route,
            handle: handle
        };
        console.info("CustomReuseStrategy.store() - route key: " + this.calcKey(route));
        this.storedRoutes[this.calcKey(route)] = storedRoute;
    }


    public shouldAttach(route: ActivatedRouteSnapshot): boolean {
        console.info("CustomReuseStrategy.shouldAttach() - route key: " + this.calcKey(route));
        return this.storedRoutes[this.calcKey(route)] !== undefined;
    }

    public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        console.info("CustomReuseStrategy.retrieve() - route key: " + this.calcKey(route));
        if (this.storedRoutes[this.calcKey(route)] === undefined) {
            /* Just return undefined */
            return null;
        } else {
            return this.storedRoutes[this.calcKey(route)].handle;
        }
    }


    public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        let returnValue = (future.routeConfig === curr.routeConfig);
        if (future.routeConfig != null && curr.routeConfig != null) {
            returnValue = this.calcKey(future) === this.calcKey(curr);
            console.info("CustomReuseStrategy.shouldReuseRoute() - future: " + this.calcKey(future) + ", curr: " + this.calcKey(curr) +
                ", future.routeConfig.path:" + future.routeConfig.path + ", curr.routeConfig.path:" + curr.routeConfig.path + ", returnValue: " + returnValue);
        } else {
            console.info("CustomReuseStrategy.shouldReuseRoute() - future: " + this.calcKey(future) + ", curr: " + this.calcKey(curr) +
                ", future.routeConfig:" + future.routeConfig + ", curr.routeConfig:" + curr.routeConfig + ", returnValue: " + returnValue);
        }
        return returnValue;
    }
}

In Summary

Following things define in my app and RouteReuseStrategy works fine.

  • Defined routes with data key and children lazy loading
  • Only single route-outlet
  • Unique key for each routes
  • Angular Version 2.4.10 and Router Version 3.4.10

Reference

  • How to implement RouteReuseStrategy shouldDetach for specific routes in Angular 2