Let's assume we have following routes:

    {path: 'settings', component: SettingsComponent, children: []},
    {path: 'project', component: ProjectListComponent, children: [
        {path: ':id', component: ProjectComponent},
    ]},
  • You open/project/1. So you tell Angular to load a route which contains 2 routes (the actual one and one children).
  • Then open/settings. What happens now is interesting:

    • Angular triggersRouteReuseStrategy::storefor routeproject, which contains the very moment one children in itshandle. So you save that including the children (as you can't and shouldn't modify thehandle)
  • You go back to/project(note:notthe one from 1.). Now Angular wants from your ReuseStrategy the handle forproject. You return the one with 1 children (since you have stored that, remember?)

    • Angular wants now to load a view containing only one route with a handle that contains two routes. What to do? Well, angular decides to throw an error, which is fine to me, as the behavior is not very obvious. Should angular instead remove that children route and destroy its Component? I don't know. Why don't you return a correct one?

So what's the issue here? The issue is that you return a handle that is not the correct one. You return a handle containing a children although angular wanted the handle for/project(no children involved here).

The solution is simple: Store the handle not based on route name, not even on url, but on the whole state with children. Usually you store it like this:

store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
   this.myStore[route.routeConfig.path] = handle;
}

This is not enough. Also using the full url including all parentspathis not enough. You need a different key to define the current state ofActivatedRouteSnapshot, which is perfectly defined by its full urlPLUSits childrens.

Additions:

  1. Memory Leak

once you store a handle in your reuse strategy, angular is never destroying related Components. You need to calldestroy()on them once you remove them out of your cache. However, sinceDetachedRouteHandleis defined as{}you need currently a little hack.

  1. Multiple caches per resolved url

When you store the handle based on a simple keypathof your config or the full path including all parents, then you store for dynamic routes (like/project/:id) only ever the latest. My following implementation allows you to define for example to hold the last 5 detail views of/project/:id.

Here is an implementation that should cover all use-cases:

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

interface RouteStates {

max: number;
handles: {[handleKey: string]: DetachedRouteHandle};
handleKeys: string[];

}

function getResolvedUrl(route: ActivatedRouteSnapshot): string {

return route.pathFromRoot
    .map(v => v.url.map(segment => segment.toString()).join('/'))
    .join('/');

}

function getConfiguredUrl(route: ActivatedRouteSnapshot): string {

return '/' + route.pathFromRoot
    .filter(v => v.routeConfig)
    .map(v => v.routeConfig!.path)
    .join('/');

}

export class ReuseStrategy implements RouteReuseStrategy {

private routes: {[routePath: string]: RouteStates } = {
    '/project': {max: 1, handles: {}, handleKeys: []},
    '/project/:id': {max: 5, handles: {}, handleKeys: []}
};

/** Determines if this route (and its subtree) should be detached to be reused later */
shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return !!this.routes[getConfiguredUrl(route)];
}

private getStoreKey(route: ActivatedRouteSnapshot) {
    const baseUrl = getResolvedUrl(route);

    //this works, as ActivatedRouteSnapshot has only every one children ActivatedRouteSnapshot
    //as you can't have more since urls like `/project/1,2` where you'd want to display 1 and 2 project at the
    //same time
    const childrenParts = [];
    let deepestChild = route;
    while (deepestChild.firstChild) {
        deepestChild = deepestChild.firstChild;
        childrenParts.push(deepestChild.url.join('/'));
    }

    //it's important to separate baseUrl with childrenParts so we don't have collisions.
    return baseUrl + '////' + childrenParts.join('/');
}

/**
 * Stores the detached route.
 *
 * Storing a `null` value should erase the previously stored value.
 */
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
    if (route.routeConfig) {
        const config = this.routes[getConfiguredUrl(route)];
        if (config) {
            const storeKey = this.getStoreKey(route);
            if (handle) {
                if (!config.handles[storeKey]) {
                    //add new handle
                    if (config.handleKeys.length >= config.max) {
                        const oldestUrl = config.handleKeys[0];
                        config.handleKeys.splice(0, 1);

                        //this is important to work around memory leaks, as Angular will never destroy the Component
                        //on its own once it got stored in our router strategy.
                        const oldHandle = config.handles[oldestUrl] as { componentRef: ComponentRef<any> };
                        oldHandle.componentRef.destroy();

                        delete config.handles[oldestUrl];
                    }
                    config.handles[storeKey] = handle;
                    config.handleKeys.push(storeKey);
                }
            } else {
                //we do not delete old handles on request, as we define when the handle dies
            }
        }
    }
}

/** Determines if this route (and its subtree) should be reattached */
shouldAttach(route: ActivatedRouteSnapshot): boolean {
    if (route.routeConfig) {
        const config = this.routes[getConfiguredUrl(route)];

        if (config) {
            const storeKey = this.getStoreKey(route);
            return !!config.handles[storeKey];
        }
    }

    return false;
}

/** Retrieves the previously stored route */
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    if (route.routeConfig) {
        const config = this.routes[getConfiguredUrl(route)];

        if (config) {
            const storeKey = this.getStoreKey(route);
            return config.handles[storeKey];
        }
    }

    return null;
}

/** Determines if `curr` route should be reused */
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return getResolvedUrl(future) === getResolvedUrl(curr) && future.routeConfig === curr.routeConfig;
}

}


Chang_Chen
0 声望0 粉丝