Skip to content

Commit 541de26

Browse files
vsavkinmhevery
authored andcommitted
fix(router): do not finish bootstrap until all the routes are resolved (#14327)
Fixes #12162
1 parent 74cb575 commit 541de26

File tree

11 files changed

+300
-156
lines changed

11 files changed

+300
-156
lines changed

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 {OpaqueToken} 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 OpaqueToken('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: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

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

1212
import {WebWorkerPlatformLocation} from './platform_location';
1313

1414

15+
1516
/**
1617
* Those providers should be added when the router is used in a worker context in addition to the
1718
* {@link ROUTER_PROVIDERS} and after them.
@@ -23,10 +24,15 @@ export const WORKER_APP_LOCATION_PROVIDERS = [
2324
useFactory: appInitFnFactory,
2425
multi: true,
2526
deps: [PlatformLocation, NgZone]
26-
}
27+
},
28+
{provide: LOCATION_INITIALIZED, useFactory: locationInitialized, deps: [PlatformLocation]}
2729
];
2830

29-
function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
31+
export function locationInitialized(platformLocation: WebWorkerPlatformLocation) {
32+
return platformLocation.initialized;
33+
}
34+
35+
export function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
3036
Promise<boolean> {
3137
return () => zone.runGuarded(() => platformLocation.init());
3238
}

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
@@ -25,6 +25,8 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
2525
private _hashChangeListeners: Array<Function> = [];
2626
private _location: LocationType = null;
2727
private _channelSource: EventEmitter<Object>;
28+
public initialized: Promise<any>;
29+
private initializedResolve: () => void;
2830

2931
constructor(
3032
brokerFactory: ClientMessageBrokerFactory, bus: MessageBus, private _serializer: Serializer) {
@@ -52,6 +54,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
5254
}
5355
}
5456
});
57+
this.initialized = new Promise(res => this.initializedResolve = res);
5558
}
5659

5760
/** @internal **/
@@ -63,6 +66,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
6366
(val: LocationType):
6467
boolean => {
6568
this._location = val;
69+
this.initializedResolve();
6670
return true;
6771
},
6872
(err): boolean => { throw new Error(err); });

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

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,18 @@ type NavigationParams = {
278278
source: NavigationSource,
279279
};
280280

281+
/**
282+
* @internal
283+
*/
284+
export type RouterHook = (snapshot: RouterStateSnapshot) => Observable<void>;
285+
286+
/**
287+
* @internal
288+
*/
289+
function defaultRouterHook(snapshot: RouterStateSnapshot): Observable<void> {
290+
return of (null);
291+
}
292+
281293

282294
/**
283295
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
@@ -320,11 +332,23 @@ export class Router {
320332
*/
321333
errorHandler: ErrorHandler = defaultErrorHandler;
322334

335+
336+
323337
/**
324338
* Indicates if at least one navigation happened.
325339
*/
326340
navigated: boolean = false;
327341

342+
/**
343+
* Used by RouterModule. This allows us to
344+
* pause the navigation either before preactivation or after it.
345+
* @internal
346+
*/
347+
hooks: {beforePreactivation: RouterHook, afterPreactivation: RouterHook} = {
348+
beforePreactivation: defaultRouterHook,
349+
afterPreactivation: defaultRouterHook
350+
};
351+
328352
/**
329353
* Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations.
330354
*/
@@ -681,26 +705,33 @@ export class Router {
681705
urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
682706
}
683707

708+
const beforePreactivationDone$ = mergeMap.call(
709+
urlAndSnapshot$, (p: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
710+
return map.call(this.hooks.beforePreactivation(p.snapshot), () => p);
711+
});
684712

685713
// run preactivation: guards and data resolvers
686714
let preActivation: PreActivation;
687-
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
688-
preActivation =
689-
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
690-
preActivation.traverse(this.outletMap);
691-
return {appliedUrl, snapshot};
692-
});
715+
const preactivationTraverse$ = map.call(
716+
beforePreactivationDone$,
717+
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
718+
preActivation =
719+
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
720+
preActivation.traverse(this.outletMap);
721+
return {appliedUrl, snapshot};
722+
});
693723

694-
const preactivationCheckGuards =
695-
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
724+
const preactivationCheckGuards$ = mergeMap.call(
725+
preactivationTraverse$,
726+
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
696727
if (this.navigationId !== id) return of (false);
697728

698729
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
699730
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
700731
});
701732
});
702733

703-
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
734+
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards$, (p: any) => {
704735
if (this.navigationId !== id) return of (false);
705736

706737
if (p.shouldActivate) {
@@ -710,11 +741,15 @@ export class Router {
710741
}
711742
});
712743

744+
const preactivationDone$ = mergeMap.call(preactivationResolveData$, (p: any) => {
745+
return map.call(this.hooks.afterPreactivation(p.snapshot), () => p);
746+
});
747+
713748

714749
// create router state
715750
// this operation has side effects => route state is being affected
716751
const routerState$ =
717-
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
752+
map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => {
718753
if (shouldActivate) {
719754
const state =
720755
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);

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

Lines changed: 77 additions & 17 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, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, 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, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, 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';
@@ -110,7 +112,7 @@ export function routerNgProbeToken() {
110112
* In addition, we often want to split applications into multiple bundles and load them on demand.
111113
* Doing this transparently is not trivial.
112114
*
113-
* The Angular 2 router solves these problems. Using the router, you can declaratively specify
115+
* The Angular router solves these problems. Using the router, you can declaratively specify
114116
* application states, manage state transitions while taking care of the URL, and load bundles on
115117
* demand.
116118
*
@@ -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
/**
@@ -305,11 +362,14 @@ export const ROUTER_INITIALIZER = new OpaqueToken('Router Initializer');
305362

306363
export function provideRouterInitializer() {
307364
return [
365+
RouterInitializer,
308366
{
309-
provide: ROUTER_INITIALIZER,
310-
useFactory: initialRouterNavigation,
311-
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
367+
provide: APP_INITIALIZER,
368+
multi: true,
369+
useFactory: getAppInitializer,
370+
deps: [RouterInitializer]
312371
},
372+
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
313373
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
314374
];
315375
}

0 commit comments

Comments
 (0)