Skip to content

Commit

Permalink
fix(router): do not finish bootstrap until all the routes are resolved (
Browse files Browse the repository at this point in the history
angular#14762)

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
  • Loading branch information
vsavkin authored and chuckjaz committed Mar 7, 2017
1 parent 1cff125 commit 5df998d
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 158 deletions.
7 changes: 7 additions & 0 deletions modules/@angular/common/src/location/platform_location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

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

/**
* @whatItDoes indicates when a location is initialized
* @experimental
*/
export const LOCATION_INITIALIZED = new InjectionToken<Promise<any>>('Location Initialized');

/**
* A serializable version of the event from onPopState or onHashChange
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {PlatformLocation} from '@angular/common';
import {LOCATION_INITIALIZED, PlatformLocation} from '@angular/common';
import {APP_INITIALIZER, InjectionToken, NgZone} from '@angular/core';

import {WebWorkerPlatformLocation} from './platform_location';
Expand All @@ -18,16 +18,20 @@ import {WebWorkerPlatformLocation} from './platform_location';
* @experimental
*/
export const WORKER_APP_LOCATION_PROVIDERS = [
{provide: PlatformLocation, useClass: WebWorkerPlatformLocation},
{
{provide: PlatformLocation, useClass: WebWorkerPlatformLocation}, {
provide: APP_INITIALIZER,
useFactory: appInitFnFactory,
multi: true,
deps: [PlatformLocation, NgZone],
deps: [PlatformLocation, NgZone]
},
{provide: LOCATION_INITIALIZED, useFactory: locationInitialized, deps: [PlatformLocation]}
];

function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
export function locationInitialized(platformLocation: WebWorkerPlatformLocation) {
return platformLocation.initialized;
}

export function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () =>
Promise<boolean> {
return () => zone.runGuarded(() => platformLocation.init());
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
private _hashChangeListeners: Array<Function> = [];
private _location: LocationType = null;
private _channelSource: EventEmitter<Object>;
public initialized: Promise<any>;
private initializedResolve: () => void;

constructor(
brokerFactory: ClientMessageBrokerFactory, bus: MessageBus, private _serializer: Serializer) {
Expand All @@ -46,6 +48,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
}
}
});
this.initialized = new Promise(res => this.initializedResolve = res);
}

/** @internal **/
Expand All @@ -56,6 +59,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
.then(
(val: LocationType) => {
this._location = val;
this.initializedResolve();
return true;
},
err => { throw new Error(err); });
Expand Down
55 changes: 45 additions & 10 deletions modules/@angular/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -602,26 +626,33 @@ 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) => {
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});

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

if (p.shouldActivate) {
Expand All @@ -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);
Expand Down
11 changes: 7 additions & 4 deletions modules/@angular/router/src/router_config_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
}
}
}
141 changes: 125 additions & 16 deletions modules/@angular/router/src/router_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* 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 {ɵgetDOM as getDOM} from '@angular/platform-browser';
import {Subject} from 'rxjs/Subject';
import {of } from 'rxjs/observable/of';

import {Route, Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
Expand All @@ -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';
Expand Down Expand Up @@ -208,6 +210,32 @@ export function provideRoutes(routes: Routes): any {
];
}

/**
* @whatItDoes Represents an option to configure when the initial navigation is performed.
*
* @description
* * 'enabled' - the initial navigation starts before the root component is created.
* The bootstrap is blocked until the initial navigation is complete.
* * 'disabled' - the initial navigation is not performed. The location listener is set up before
* the root component gets created.
* * 'legacy_enabled'- the initial navigation starts after the root component has been created.
* The bootstrap is not blocked until the initial navigation is complete. @deprecated
* * 'legacy_disabled'- the initial navigation is not performed. The location listener is set up
* after @deprecated
* the root component gets created.
* * `true` - same as 'legacy_enabled'. @deprecated
* * `false` - same as 'legacy_disabled'. @deprecated
*
* The 'enabled' option should be used for applications unless there is a reason to have
* more control over when the router starts its initial navigation due to some complex
* initialization logic. In this case, 'disabled' should be used.
*
* The 'legacy_enabled' and 'legacy_disabled' should not be used for new applications.
*
* @experimental
*/
export type InitialNavigation =
true | false | 'enabled' | 'disabled' | 'legacy_enabled' | 'legacy_disabled';

/**
* @whatItDoes Represents options to configure the router.
Expand All @@ -228,7 +256,7 @@ export interface ExtraOptions {
/**
* Disables the initial navigation.
*/
initialNavigation?: boolean;
initialNavigation?: InitialNavigation;

/**
* A custom error handler.
Expand Down Expand Up @@ -278,22 +306,100 @@ 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 = false;
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 (this.isLegacyDisabled(opts) || this.isLegacyEnabled(opts)) {
resolve(true);

} else if (opts.initialNavigation === 'disabled') {
router.setUpLocationChangeListener();
resolve(true);

} else if (opts.initialNavigation === 'enabled') {
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();

} else {
throw new Error(`Invalid initialNavigation options: '${opts.initialNavigation}'`);
}

return res;
});
}

bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
const opts = this.injector.get(ROUTER_CONFIGURATION);
const preloader = this.injector.get(RouterPreloader);
const router = this.injector.get(Router);
const ref = this.injector.get(ApplicationRef);

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

router.resetRootComponentType(ref.componentTypes[0]);
preloader.setUpPreloading();
if (opts.initialNavigation === false) {
router.setUpLocationChangeListener();
} else {
if (this.isLegacyEnabled(opts)) {
router.initialNavigation();
} else if (this.isLegacyDisabled(opts)) {
router.setUpLocationChangeListener();
}
};

preloader.setUpPreloading();
router.resetRootComponentType(ref.componentTypes[0]);
this.resultOfPreactivationDone.next(null);
this.resultOfPreactivationDone.complete();
}

private isLegacyEnabled(opts: ExtraOptions): boolean {
return opts.initialNavigation === 'legacy_enabled' || opts.initialNavigation === true ||
opts.initialNavigation === undefined;
}

private isLegacyDisabled(opts: ExtraOptions): boolean {
return opts.initialNavigation === 'legacy_disabled' || opts.initialNavigation === false;
}
}

export function getAppInitializer(r: RouterInitializer) {
return r.appInitializer.bind(r);
}

export function getBootstrapListener(r: RouterInitializer) {
return r.bootstrapListener.bind(r);
}

/**
Expand All @@ -306,11 +412,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},
];
}
Loading

0 comments on commit 5df998d

Please sign in to comment.