Angular router guard and ROUTER_NAVIGATION effect

2019-04-12 21:43发布

问题:

There is a simple (Angular 4) route guard, which waits for some data to be loaded from backend:

@Injectable()
export class ContractsLoadedGuard implements CanActivate {
    constructor(private store: Store<State>) { }

    waitForData(): Observable<boolean> {
        return this.store.select(state => state.contracts)
            .map(contractList => !!contractList)
            .filter(loaded => loaded)
            .take(1);
    }

    canActivate(): Observable<boolean> { return this.waitForData(); }
}

Routing:

const routes: Routes = [
    { path: 'app-list', canActivate: [ContractsLoadedGuard], component: AppListComponent },
];

And finally there is an @ngrx/effects triggered by @ngrx/router-store v4 ROUTER_NAVIGATION action:

@Effect() routeChange$ = this.actions$
    .ofType(ROUTER_NAVIGATION)
    .filter((action: RouterNavigationAction) => action.payload.routerState.url.indexOf('/app-list') > -1)
    .withLatestFrom(this.store.select(state => state.contracts))
    .switchMap(([action, contracts]: ([RouterNavigationAction, ContractList])) =>
        this.restClient.load(action.payload.routerState.queryParams, contract));

Unfortunatelly when navigation changes to /app-list the ngrx effect is executed first (before guard) and thus the data state.contracts are not available yet. The guard has not been executed yet. I do have to add .combineLatest() Rx operator to wait for the contracts data in effect also (this is guard's job):

@Effect() routeChange$ = this.actions$
    .ofType(ROUTER_NAVIGATION)
    .filter((action: RouterNavigationAction) => action.payload.routerState.url.indexOf('/app-list') > -1)
    .combineLatest(this.contractListGuard.waitForContractsToLoad(), a => a) <- HERE
    .withLatestFrom(this.store.select(state => state.contracts))
    .switchMap(/* same here */) 

I'm not unsure if this is good solution enough. There must be a better way to do it - not duplicate the guard functionality in effect.

To summarize: On application boostrap, I need to fetch some data from backend - contracts. If an user navigates to /app-list (immediate redirect) there are other data fetched from server - based on some query params and contracts - the ngrx router ROUTER_NAVIGATION effect execution order is before the guard execution order. How to handle this properly?

Based on GitHub - state_management_ngrx4

回答1:

There is one way to achieve it. You can subscribe to the Angular Router's ResolveEnd event https://angular.io/api/router/ResolveEnd in your effect and then dispatch your own action for RESOLVE_END where you can do stuff with your resolver / guard data.

Actually there is an PR in ngrx/platform that I created where ngrx/router will dispatch NAVIGATE_RESOLVE_END action out of the box. I am waiting for ngrx team to accept my PR. https://github.com/ngrx/platform/pull/524/

You can subscribe to router events and filter it for the Resolve End and dispatch your own action call it Router_Resove_End action etc.

this.router.events.filter(e => e instanceof ResolveEnd).subscribe(s => {
 // dispatch your own action here.
});


回答2:

There is very nice wrap up on Medium:

@Injectable()
export class RouterEffects {
    constructor(
        private actions$: Actions,private router: Router,private location: Location,private store: Store<any>
    ) {this.listenToRouter();}

    @Effect({ dispatch: false })
    navigate$ = this.actions$.pipe(ofType('[Router] Go')...);
    @Effect({ dispatch: false })
    navigateBack$ = this.actions$.pipe(ofType('[Router] Back'), tap(() => this.location.back()));

    @Effect({ dispatch: false })
    navigateForward$ = this.actions$.pipe(ofType('[Router] Forward'),...);

    private listenToRouter() {
        this.router.events.pipe(
            filter(event => event instanceof ActivationEnd)
        ).subscribe((event: ActivationEnd) =>
            this.store.dispatch(new RouteChange({
                params: { ...event.snapshot.params },
                path: event.snapshot.routeConfig.path
            }))
        );
    }
}

and then instead of:

@Effect() routeChange$ = this.actions$.ofType(ROUTER_NAVIGATION)

use:

@Effect() routeChange$ = this.actions$.ofType(ofType('[Router] Route Change'))