Skip to content

Commit 34403cd

Browse files
vsavkinchuckjaz
authored andcommitted
fix(router): do not finish bootstrap until all the routes are resolved (#15121)
Cherry-pick of 5df998d onto 2.4.x branch. DEPRECATION: Use `RouterModule.forRoot(routes, {initialNavigation: 'enabled'})` instead of `RouterModule.forRoot(routes, {initialNavigtaion: true})`. Before doing this, move the initialization logic affecting the router from the bootstrapped component to the boostrapped module. Similarly, use `RouterModule.forRoot(routes, {initialNavigation: 'disabled'})` instead of `RouterModule.forRoot(routes, {initialNavigation: false})`. Deprecated options: 'legacy_enabled', `true` (same as 'legacy_enabled'), 'legacy_disabled', `false` (same as 'legacy_disabled'). The "Router Initial Navigation" design document covers this change. Read more here: https://docs.google.com/document/d/1Hlw1fPaVs-PCj5KPeJRKhrQGAvFOxdvTlwAcnZosu5A/edit?usp=sharing
1 parent e5c9bbc commit 34403cd

12 files changed

Lines changed: 463 additions & 153 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 {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: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
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

@@ -23,10 +23,15 @@ export const WORKER_APP_LOCATION_PROVIDERS = [
2323
useFactory: appInitFnFactory,
2424
multi: true,
2525
deps: [PlatformLocation, NgZone]
26-
}
26+
},
27+
{provide: LOCATION_INITIALIZED, useFactory: locationInitialized, deps: [PlatformLocation]}
2728
];
2829

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

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: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,18 @@ type NavigationParams = {
279279
};
280280

281281

282+
/**
283+
* @internal
284+
*/
285+
export type RouterHook = (snapshot: RouterStateSnapshot) => Observable<void>;
286+
287+
/**
288+
* @internal
289+
*/
290+
function defaultRouterHook(snapshot: RouterStateSnapshot): Observable<void> {
291+
return of (null);
292+
}
293+
282294
/**
283295
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
284296
*/
@@ -320,13 +332,26 @@ 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

328342
/**
329343
* Extracts and merges URLs. Used for Angular 1 to Angular 2 migrations.
344+
* Used by RouterModule. This allows us to
345+
* pause the navigation either before preactivation or after it.
346+
* @internal
347+
*/
348+
hooks: {beforePreactivation: RouterHook, afterPreactivation: RouterHook} = {
349+
beforePreactivation: defaultRouterHook,
350+
afterPreactivation: defaultRouterHook
351+
};
352+
353+
/**
354+
* Extracts and merges URLs. Used for AngularJS to Angular migrations.
330355
*/
331356
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
332357

@@ -681,26 +706,33 @@ export class Router {
681706
urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
682707
}
683708

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

685714
// run preactivation: guards and data resolvers
686715
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-
});
716+
const preactivationTraverse$ = map.call(
717+
beforePreactivationDone$,
718+
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
719+
preActivation =
720+
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
721+
preActivation.traverse(this.outletMap);
722+
return {appliedUrl, snapshot};
723+
});
693724

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

698730
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
699731
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
700732
});
701733
});
702734

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

706738
if (p.shouldActivate) {
@@ -710,11 +742,15 @@ export class Router {
710742
}
711743
});
712744

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

714750
// create router state
715751
// this operation has side effects => route state is being affected
716752
const routerState$ =
717-
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
753+
map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => {
718754
if (shouldActivate) {
719755
const state =
720756
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);

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

Lines changed: 125 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, 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';
@@ -208,6 +210,32 @@ export function provideRoutes(routes: Routes): any {
208210
];
209211
}
210212

213+
/**
214+
* @whatItDoes Represents an option to configure when the initial navigation is performed.
215+
*
216+
* @description
217+
* * 'enabled' - the initial navigation starts before the root component is created.
218+
* The bootstrap is blocked until the initial navigation is complete.
219+
* * 'disabled' - the initial navigation is not performed. The location listener is set up before
220+
* the root component gets created.
221+
* * 'legacy_enabled'- the initial navigation starts after the root component has been created.
222+
* The bootstrap is not blocked until the initial navigation is complete. @deprecated
223+
* * 'legacy_disabled'- the initial navigation is not performed. The location listener is set up
224+
* after @deprecated
225+
* the root component gets created.
226+
* * `true` - same as 'legacy_enabled'. @deprecated
227+
* * `false` - same as 'legacy_disabled'. @deprecated
228+
*
229+
* The 'enabled' option should be used for applications unless there is a reason to have
230+
* more control over when the router starts its initial navigation due to some complex
231+
* initialization logic. In this case, 'disabled' should be used.
232+
*
233+
* The 'legacy_enabled' and 'legacy_disabled' should not be used for new applications.
234+
*
235+
* @experimental
236+
*/
237+
export type InitialNavigation =
238+
boolean | 'enabled' | 'disabled' | 'legacy_enabled' | 'legacy_disabled';
211239

212240
/**
213241
* @whatItDoes Represents options to configure the router.
@@ -228,7 +256,7 @@ export interface ExtraOptions {
228256
/**
229257
* Disables the initial navigation.
230258
*/
231-
initialNavigation?: boolean;
259+
initialNavigation?: InitialNavigation;
232260

233261
/**
234262
* A custom error handler.
@@ -278,22 +306,100 @@ export function rootRoute(router: Router): ActivatedRoute {
278306
return router.routerState.root;
279307
}
280308

281-
export function initialRouterNavigation(
282-
router: Router, ref: ApplicationRef, preloader: RouterPreloader, opts: ExtraOptions) {
283-
return (bootstrappedComponentRef: ComponentRef<any>) => {
309+
/**
310+
* To initialize the router properly we need to do in two steps:
311+
*
312+
* We need to start the navigation in a APP_INITIALIZER to block the bootstrap if
313+
* a resolver or a guards executes asynchronously. Second, we need to actually run
314+
* activation in a BOOTSTRAP_LISTENER. We utilize the afterPreactivation
315+
* hook provided by the router to do that.
316+
*
317+
* The router navigation starts, reaches the point when preactivation is done, and then
318+
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
319+
*/
320+
@Injectable()
321+
export class RouterInitializer {
322+
private initNavigation: boolean = false;
323+
private resultOfPreactivationDone = new Subject<void>();
324+
325+
constructor(private injector: Injector) {}
326+
327+
appInitializer(): Promise<any> {
328+
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
329+
return p.then(() => {
330+
let resolve: Function = null;
331+
const res = new Promise(r => resolve = r);
332+
const router = this.injector.get(Router);
333+
const opts = this.injector.get(ROUTER_CONFIGURATION);
334+
335+
if (this.isLegacyDisabled(opts) || this.isLegacyEnabled(opts)) {
336+
resolve(true);
337+
338+
} else if (opts.initialNavigation === 'disabled') {
339+
router.setUpLocationChangeListener();
340+
resolve(true);
341+
342+
} else if (opts.initialNavigation === 'enabled') {
343+
router.hooks.afterPreactivation = () => {
344+
// only the initial navigation should be delayed
345+
if (!this.initNavigation) {
346+
this.initNavigation = true;
347+
resolve(true);
348+
return this.resultOfPreactivationDone;
349+
350+
// subsequent navigations should not be delayed
351+
} else {
352+
return of (null);
353+
}
354+
};
355+
router.initialNavigation();
356+
357+
} else {
358+
throw new Error(`Invalid initialNavigation options: '${opts.initialNavigation}'`);
359+
}
360+
361+
return res;
362+
});
363+
}
364+
365+
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
366+
const opts = this.injector.get(ROUTER_CONFIGURATION);
367+
const preloader = this.injector.get(RouterPreloader);
368+
const router = this.injector.get(Router);
369+
const ref = this.injector.get(ApplicationRef);
284370

285371
if (bootstrappedComponentRef !== ref.components[0]) {
286372
return;
287373
}
288374

289-
router.resetRootComponentType(ref.componentTypes[0]);
290-
preloader.setUpPreloading();
291-
if (opts.initialNavigation === false) {
292-
router.setUpLocationChangeListener();
293-
} else {
375+
if (this.isLegacyEnabled(opts)) {
294376
router.initialNavigation();
377+
} else if (this.isLegacyDisabled(opts)) {
378+
router.setUpLocationChangeListener();
295379
}
296-
};
380+
381+
preloader.setUpPreloading();
382+
router.resetRootComponentType(ref.componentTypes[0]);
383+
this.resultOfPreactivationDone.next(null);
384+
this.resultOfPreactivationDone.complete();
385+
}
386+
387+
private isLegacyEnabled(opts: ExtraOptions): boolean {
388+
return opts.initialNavigation === 'legacy_enabled' || opts.initialNavigation === true ||
389+
opts.initialNavigation === undefined;
390+
}
391+
392+
private isLegacyDisabled(opts: ExtraOptions): boolean {
393+
return opts.initialNavigation === 'legacy_disabled' || opts.initialNavigation === false;
394+
}
395+
}
396+
397+
export function getAppInitializer(r: RouterInitializer) {
398+
return r.appInitializer.bind(r);
399+
}
400+
401+
export function getBootstrapListener(r: RouterInitializer) {
402+
return r.bootstrapListener.bind(r);
297403
}
298404

299405
/**
@@ -305,11 +411,14 @@ export const ROUTER_INITIALIZER = new OpaqueToken('Router Initializer');
305411

306412
export function provideRouterInitializer() {
307413
return [
414+
RouterInitializer,
308415
{
309-
provide: ROUTER_INITIALIZER,
310-
useFactory: initialRouterNavigation,
311-
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
416+
provide: APP_INITIALIZER,
417+
multi: true,
418+
useFactory: getAppInitializer,
419+
deps: [RouterInitializer]
312420
},
421+
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
313422
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
314423
];
315424
}

0 commit comments

Comments
 (0)