Skip to content

Commit 2a191ca

Browse files
authored
fix(router): do not finish bootstrap until all the routes are resolved (angular#14608)
Fixes angular#12162 closes angular#14155
1 parent c2e0f71 commit 2a191ca

12 files changed

Lines changed: 304 additions & 154 deletions

File tree

modules/@angular/common/src/location/platform_location.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {InjectionToken} from '@angular/core';
910
/**
1011
* This class should not be used directly by an application developer. Instead, use
1112
* {@link Location}.
@@ -50,6 +51,12 @@ export abstract class PlatformLocation {
5051
abstract back(): void;
5152
}
5253

54+
/**
55+
* @whatItDoes indicates when a location is initialized
56+
* @experimental
57+
*/
58+
export const LOCATION_INITIALIZED = new InjectionToken<Promise<any>>('Location Initialized');
59+
5360
/**
5461
* A serializable version of the event from onPopState or onHashChange
5562
*

modules/@angular/platform-webworker/src/web_workers/worker/location_providers.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {PlatformLocation} from '@angular/common';
9+
import {LOCATION_INITIALIZED, PlatformLocation} from '@angular/common';
1010
import {APP_INITIALIZER, InjectionToken, NgZone} from '@angular/core';
1111

1212
import {WebWorkerPlatformLocation} from './platform_location';
@@ -25,9 +25,18 @@ export const WORKER_APP_LOCATION_PROVIDERS = [
2525
multi: true,
2626
deps: [PlatformLocation, NgZone],
2727
},
28+
{
29+
provide: LOCATION_INITIALIZED,
30+
useFactory: locationInitialized,
31+
deps: [PlatformLocation],
32+
},
2833
];
2934

30-
function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
35+
export function locationInitialized(platformLocation: WebWorkerPlatformLocation) {
36+
return platformLocation.initialized;
37+
}
38+
39+
export function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
3140
Promise<boolean> {
3241
return () => zone.runGuarded(() => platformLocation.init());
3342
}

modules/@angular/platform-webworker/src/web_workers/worker/platform_location.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
2222
private _hashChangeListeners: Array<Function> = [];
2323
private _location: LocationType = null;
2424
private _channelSource: EventEmitter<Object>;
25+
public initialized: Promise<any>;
26+
private initializedResolve: () => void;
2527

2628
constructor(
2729
brokerFactory: ClientMessageBrokerFactory, bus: MessageBus, private _serializer: Serializer) {
@@ -48,6 +50,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
4850
}
4951
}
5052
});
53+
this.initialized = new Promise(res => this.initializedResolve = res);
5154
}
5255

5356
/** @internal **/
@@ -58,6 +61,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
5861
.then(
5962
(val: LocationType) => {
6063
this._location = val;
64+
this.initializedResolve();
6165
return true;
6266
},
6367
err => { throw new Error(err); });

modules/@angular/router/src/router.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,18 @@ type NavigationParams = {
180180
source: NavigationSource,
181181
};
182182

183+
/**
184+
* @internal
185+
*/
186+
export type RouterHook = (snapshot: RouterStateSnapshot) => Observable<void>;
187+
188+
/**
189+
* @internal
190+
*/
191+
function defaultRouterHook(snapshot: RouterStateSnapshot): Observable<void> {
192+
return of (null);
193+
}
194+
183195
/**
184196
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
185197
*/
@@ -221,11 +233,23 @@ export class Router {
221233
*/
222234
errorHandler: ErrorHandler = defaultErrorHandler;
223235

236+
237+
224238
/**
225239
* Indicates if at least one navigation happened.
226240
*/
227241
navigated: boolean = false;
228242

243+
/**
244+
* Used by RouterModule. This allows us to
245+
* pause the navigation either before preactivation or after it.
246+
* @internal
247+
*/
248+
hooks: {beforePreactivation: RouterHook, afterPreactivation: RouterHook} = {
249+
beforePreactivation: defaultRouterHook,
250+
afterPreactivation: defaultRouterHook
251+
};
252+
229253
/**
230254
* Extracts and merges URLs. Used for AngularJS to Angular migrations.
231255
*/
@@ -602,26 +626,33 @@ export class Router {
602626
urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
603627
}
604628

629+
const beforePreactivationDone$ = mergeMap.call(
630+
urlAndSnapshot$, (p: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
631+
return map.call(this.hooks.beforePreactivation(p.snapshot), () => p);
632+
});
605633

606634
// run preactivation: guards and data resolvers
607635
let preActivation: PreActivation;
608-
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
609-
preActivation =
610-
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
611-
preActivation.traverse(this.outletMap);
612-
return {appliedUrl, snapshot};
613-
});
636+
const preactivationTraverse$ = map.call(
637+
beforePreactivationDone$,
638+
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
639+
preActivation =
640+
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
641+
preActivation.traverse(this.outletMap);
642+
return {appliedUrl, snapshot};
643+
});
614644

615-
const preactivationCheckGuards =
616-
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
645+
const preactivationCheckGuards$ = mergeMap.call(
646+
preactivationTraverse$,
647+
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
617648
if (this.navigationId !== id) return of (false);
618649

619650
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
620651
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
621652
});
622653
});
623654

624-
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
655+
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards$, (p: any) => {
625656
if (this.navigationId !== id) return of (false);
626657

627658
if (p.shouldActivate) {
@@ -631,11 +662,15 @@ export class Router {
631662
}
632663
});
633664

665+
const preactivationDone$ = mergeMap.call(preactivationResolveData$, (p: any) => {
666+
return map.call(this.hooks.afterPreactivation(p.snapshot), () => p);
667+
});
668+
634669

635670
// create router state
636671
// this operation has side effects => route state is being affected
637672
const routerState$ =
638-
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
673+
map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => {
639674
if (shouldActivate) {
640675
const state =
641676
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);

modules/@angular/router/src/router_config_loader.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,13 @@ export class RouterConfigLoader {
5858
if (typeof loadChildren === 'string') {
5959
return fromPromise(this.loader.load(loadChildren));
6060
} else {
61-
const offlineMode = this.compiler instanceof Compiler;
62-
return mergeMap.call(
63-
wrapIntoObservable(loadChildren()),
64-
(t: any) => offlineMode ? of (<any>t) : fromPromise(this.compiler.compileModuleAsync(t)));
61+
return mergeMap.call(wrapIntoObservable(loadChildren()), (t: any) => {
62+
if (t instanceof NgModuleFactory) {
63+
return of (t);
64+
} else {
65+
return fromPromise(this.compiler.compileModuleAsync(t));
66+
}
67+
});
6568
}
6669
}
6770
}

modules/@angular/router/src/router_module.ts

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
10-
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';
9+
import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
10+
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';
11+
import {Subject} from 'rxjs/Subject';
12+
import {of } from 'rxjs/observable/of';
1113

1214
import {Route, Routes} from './config';
1315
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
@@ -19,7 +21,7 @@ import {ErrorHandler, Router} from './router';
1921
import {ROUTES} from './router_config_loader';
2022
import {RouterOutletMap} from './router_outlet_map';
2123
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
22-
import {ActivatedRoute} from './router_state';
24+
import {ActivatedRoute, RouterStateSnapshot} from './router_state';
2325
import {UrlHandlingStrategy} from './url_handling_strategy';
2426
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
2527
import {flatten} from './utils/collection';
@@ -278,22 +280,77 @@ export function rootRoute(router: Router): ActivatedRoute {
278280
return router.routerState.root;
279281
}
280282

281-
export function initialRouterNavigation(
282-
router: Router, ref: ApplicationRef, preloader: RouterPreloader, opts: ExtraOptions) {
283-
return (bootstrappedComponentRef: ComponentRef<any>) => {
283+
/**
284+
* To initialize the router properly we need to do in two steps:
285+
*
286+
* We need to start the navigation in a APP_INITIALIZER to block the bootstrap if
287+
* a resolver or a guards executes asynchronously. Second, we need to actually run
288+
* activation in a BOOTSTRAP_LISTENER. We utilize the afterPreactivation
289+
* hook provided by the router to do that.
290+
*
291+
* The router navigation starts, reaches the point when preactivation is done, and then
292+
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
293+
*/
294+
@Injectable()
295+
export class RouterInitializer {
296+
private initNavigation: boolean;
297+
private resultOfPreactivationDone = new Subject<void>();
298+
299+
constructor(private injector: Injector) {}
300+
301+
appInitializer(): Promise<any> {
302+
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
303+
return p.then(() => {
304+
let resolve: Function = null;
305+
const res = new Promise(r => resolve = r);
306+
const router = this.injector.get(Router);
307+
const opts = this.injector.get(ROUTER_CONFIGURATION);
308+
309+
if (opts.initialNavigation === false) {
310+
router.setUpLocationChangeListener();
311+
} else {
312+
router.hooks.afterPreactivation = () => {
313+
// only the initial navigation should be delayed
314+
if (!this.initNavigation) {
315+
this.initNavigation = true;
316+
resolve(true);
317+
return this.resultOfPreactivationDone;
318+
319+
// subsequent navigations should not be delayed
320+
} else {
321+
return of (null);
322+
}
323+
};
324+
router.initialNavigation();
325+
}
326+
327+
return res;
328+
});
329+
}
284330

331+
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
332+
const ref = this.injector.get(ApplicationRef);
285333
if (bootstrappedComponentRef !== ref.components[0]) {
286334
return;
287335
}
288336

289-
router.resetRootComponentType(ref.componentTypes[0]);
337+
const preloader = this.injector.get(RouterPreloader);
290338
preloader.setUpPreloading();
291-
if (opts.initialNavigation === false) {
292-
router.setUpLocationChangeListener();
293-
} else {
294-
router.initialNavigation();
295-
}
296-
};
339+
340+
const router = this.injector.get(Router);
341+
router.resetRootComponentType(ref.componentTypes[0]);
342+
343+
this.resultOfPreactivationDone.next(null);
344+
this.resultOfPreactivationDone.complete();
345+
}
346+
}
347+
348+
export function getAppInitializer(r: RouterInitializer) {
349+
return r.appInitializer.bind(r);
350+
}
351+
352+
export function getBootstrapListener(r: RouterInitializer) {
353+
return r.bootstrapListener.bind(r);
297354
}
298355

299356
/**
@@ -306,11 +363,14 @@ export const ROUTER_INITIALIZER =
306363

307364
export function provideRouterInitializer() {
308365
return [
366+
RouterInitializer,
309367
{
310-
provide: ROUTER_INITIALIZER,
311-
useFactory: initialRouterNavigation,
312-
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
368+
provide: APP_INITIALIZER,
369+
multi: true,
370+
useFactory: getAppInitializer,
371+
deps: [RouterInitializer]
313372
},
373+
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
314374
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
315375
];
316376
}

0 commit comments

Comments
 (0)