fix(router): do not finish bootstrap until all the routes are resolved (#14608)
Fixes #12162 closes #14155
This commit is contained in:
@ -180,6 +180,18 @@ type NavigationParams = {
|
||||
source: NavigationSource,
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type RouterHook = (snapshot: RouterStateSnapshot) => Observable<void>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function defaultRouterHook(snapshot: RouterStateSnapshot): Observable<void> {
|
||||
return of (null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
|
||||
*/
|
||||
@ -221,11 +233,23 @@ export class Router {
|
||||
*/
|
||||
errorHandler: ErrorHandler = defaultErrorHandler;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Indicates if at least one navigation happened.
|
||||
*/
|
||||
navigated: boolean = false;
|
||||
|
||||
/**
|
||||
* Used by RouterModule. This allows us to
|
||||
* pause the navigation either before preactivation or after it.
|
||||
* @internal
|
||||
*/
|
||||
hooks: {beforePreactivation: RouterHook, afterPreactivation: RouterHook} = {
|
||||
beforePreactivation: defaultRouterHook,
|
||||
afterPreactivation: defaultRouterHook
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts and merges URLs. Used for AngularJS to Angular migrations.
|
||||
*/
|
||||
@ -602,18 +626,25 @@ export class Router {
|
||||
urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
|
||||
}
|
||||
|
||||
const beforePreactivationDone$ = mergeMap.call(
|
||||
urlAndSnapshot$, (p: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
|
||||
return map.call(this.hooks.beforePreactivation(p.snapshot), () => p);
|
||||
});
|
||||
|
||||
// run preactivation: guards and data resolvers
|
||||
let preActivation: PreActivation;
|
||||
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
|
||||
preActivation =
|
||||
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
|
||||
preActivation.traverse(this.outletMap);
|
||||
return {appliedUrl, snapshot};
|
||||
});
|
||||
const preactivationTraverse$ = map.call(
|
||||
beforePreactivationDone$,
|
||||
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
|
||||
preActivation =
|
||||
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
|
||||
preActivation.traverse(this.outletMap);
|
||||
return {appliedUrl, snapshot};
|
||||
});
|
||||
|
||||
const preactivationCheckGuards =
|
||||
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
|
||||
const preactivationCheckGuards$ = mergeMap.call(
|
||||
preactivationTraverse$,
|
||||
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
|
||||
if (this.navigationId !== id) return of (false);
|
||||
|
||||
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
|
||||
@ -621,7 +652,7 @@ export class Router {
|
||||
});
|
||||
});
|
||||
|
||||
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
|
||||
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards$, (p: any) => {
|
||||
if (this.navigationId !== id) return of (false);
|
||||
|
||||
if (p.shouldActivate) {
|
||||
@ -631,11 +662,15 @@ export class Router {
|
||||
}
|
||||
});
|
||||
|
||||
const preactivationDone$ = mergeMap.call(preactivationResolveData$, (p: any) => {
|
||||
return map.call(this.hooks.afterPreactivation(p.snapshot), () => p);
|
||||
});
|
||||
|
||||
|
||||
// create router state
|
||||
// this operation has side effects => route state is being affected
|
||||
const routerState$ =
|
||||
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
|
||||
map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => {
|
||||
if (shouldActivate) {
|
||||
const state =
|
||||
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);
|
||||
|
@ -58,10 +58,13 @@ export class RouterConfigLoader {
|
||||
if (typeof loadChildren === 'string') {
|
||||
return fromPromise(this.loader.load(loadChildren));
|
||||
} else {
|
||||
const offlineMode = this.compiler instanceof Compiler;
|
||||
return mergeMap.call(
|
||||
wrapIntoObservable(loadChildren()),
|
||||
(t: any) => offlineMode ? of (<any>t) : fromPromise(this.compiler.compileModuleAsync(t)));
|
||||
return mergeMap.call(wrapIntoObservable(loadChildren()), (t: any) => {
|
||||
if (t instanceof NgModuleFactory) {
|
||||
return of (t);
|
||||
} else {
|
||||
return fromPromise(this.compiler.compileModuleAsync(t));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
|
||||
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
||||
import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
|
||||
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
|
||||
import {Route, Routes} from './config';
|
||||
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||
@ -19,7 +21,7 @@ import {ErrorHandler, Router} from './router';
|
||||
import {ROUTES} from './router_config_loader';
|
||||
import {RouterOutletMap} from './router_outlet_map';
|
||||
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||
import {ActivatedRoute} from './router_state';
|
||||
import {ActivatedRoute, RouterStateSnapshot} from './router_state';
|
||||
import {UrlHandlingStrategy} from './url_handling_strategy';
|
||||
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
||||
import {flatten} from './utils/collection';
|
||||
@ -278,22 +280,77 @@ export function rootRoute(router: Router): ActivatedRoute {
|
||||
return router.routerState.root;
|
||||
}
|
||||
|
||||
export function initialRouterNavigation(
|
||||
router: Router, ref: ApplicationRef, preloader: RouterPreloader, opts: ExtraOptions) {
|
||||
return (bootstrappedComponentRef: ComponentRef<any>) => {
|
||||
/**
|
||||
* To initialize the router properly we need to do in two steps:
|
||||
*
|
||||
* We need to start the navigation in a APP_INITIALIZER to block the bootstrap if
|
||||
* a resolver or a guards executes asynchronously. Second, we need to actually run
|
||||
* activation in a BOOTSTRAP_LISTENER. We utilize the afterPreactivation
|
||||
* hook provided by the router to do that.
|
||||
*
|
||||
* The router navigation starts, reaches the point when preactivation is done, and then
|
||||
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RouterInitializer {
|
||||
private initNavigation: boolean;
|
||||
private resultOfPreactivationDone = new Subject<void>();
|
||||
|
||||
constructor(private injector: Injector) {}
|
||||
|
||||
appInitializer(): Promise<any> {
|
||||
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
|
||||
return p.then(() => {
|
||||
let resolve: Function = null;
|
||||
const res = new Promise(r => resolve = r);
|
||||
const router = this.injector.get(Router);
|
||||
const opts = this.injector.get(ROUTER_CONFIGURATION);
|
||||
|
||||
if (opts.initialNavigation === false) {
|
||||
router.setUpLocationChangeListener();
|
||||
} else {
|
||||
router.hooks.afterPreactivation = () => {
|
||||
// only the initial navigation should be delayed
|
||||
if (!this.initNavigation) {
|
||||
this.initNavigation = true;
|
||||
resolve(true);
|
||||
return this.resultOfPreactivationDone;
|
||||
|
||||
// subsequent navigations should not be delayed
|
||||
} else {
|
||||
return of (null);
|
||||
}
|
||||
};
|
||||
router.initialNavigation();
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
|
||||
const ref = this.injector.get(ApplicationRef);
|
||||
if (bootstrappedComponentRef !== ref.components[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.resetRootComponentType(ref.componentTypes[0]);
|
||||
const preloader = this.injector.get(RouterPreloader);
|
||||
preloader.setUpPreloading();
|
||||
if (opts.initialNavigation === false) {
|
||||
router.setUpLocationChangeListener();
|
||||
} else {
|
||||
router.initialNavigation();
|
||||
}
|
||||
};
|
||||
|
||||
const router = this.injector.get(Router);
|
||||
router.resetRootComponentType(ref.componentTypes[0]);
|
||||
|
||||
this.resultOfPreactivationDone.next(null);
|
||||
this.resultOfPreactivationDone.complete();
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppInitializer(r: RouterInitializer) {
|
||||
return r.appInitializer.bind(r);
|
||||
}
|
||||
|
||||
export function getBootstrapListener(r: RouterInitializer) {
|
||||
return r.bootstrapListener.bind(r);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,11 +363,14 @@ export const ROUTER_INITIALIZER =
|
||||
|
||||
export function provideRouterInitializer() {
|
||||
return [
|
||||
RouterInitializer,
|
||||
{
|
||||
provide: ROUTER_INITIALIZER,
|
||||
useFactory: initialRouterNavigation,
|
||||
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: getAppInitializer,
|
||||
deps: [RouterInitializer]
|
||||
},
|
||||
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
|
||||
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
|
||||
];
|
||||
}
|
||||
|
Reference in New Issue
Block a user