Skip to content

Commit 2d713f5

Browse files
atscottAndrewKushnir
authored andcommitted
refactor(router): separate router initializer into different logical providers (angular#46215)
This change separates the router initializer into different providers. While it does not actually change the tree-shakeablity or the public API, it does move us towards a world that _could_ do this. That is, instead of `initialNavigation: 'disabled'`, users could use `provideDisabledInitialNavigation` in the `bootstrapApplication` call and none of the code for `initialNavigation: 'enabledBlocking'` would be included in the application. PR Close angular#46215
1 parent ae0a63a commit 2d713f5

File tree

3 files changed

+114
-105
lines changed

3 files changed

+114
-105
lines changed

goldens/public-api/router/testing/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class RouterTestingModule {
3737
export function setupTestingRouter(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location_2, compiler: Compiler, injector: Injector, routes: Route[][], opts?: ExtraOptions | UrlHandlingStrategy | null, urlHandlingStrategy?: UrlHandlingStrategy, routeReuseStrategy?: RouteReuseStrategy, titleStrategy?: TitleStrategy): Router;
3838

3939
// @public
40-
export function setupTestingRouterInternal(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location_2, compiler: Compiler, injector: Injector, routes: Route[][], opts?: ExtraOptions | UrlHandlingStrategy | null, urlHandlingStrategy?: UrlHandlingStrategy, routeReuseStrategy?: RouteReuseStrategy, defaultTitleStrategy?: DefaultTitleStrategy, titleStrategy?: TitleStrategy): Router;
40+
export function setupTestingRouterInternal(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location_2, compiler: Compiler, injector: Injector, routes: Route[][], opts?: ExtraOptions | UrlHandlingStrategy, urlHandlingStrategy?: UrlHandlingStrategy, routeReuseStrategy?: RouteReuseStrategy, defaultTitleStrategy?: DefaultTitleStrategy, titleStrategy?: TitleStrategy): Router;
4141

4242
// (No @packageDocumentation comment for this package)
4343

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
{
4242
"name": "ArgumentOutOfRangeError"
4343
},
44+
{
45+
"name": "BOOTSTRAP_DONE"
46+
},
4447
{
4548
"name": "BROWSER_MODULE_PROVIDERS"
4649
},
@@ -236,6 +239,9 @@
236239
{
237240
"name": "HashLocationStrategy"
238241
},
242+
{
243+
"name": "INITIAL_NAVIGATION"
244+
},
239245
{
240246
"name": "INITIAL_VALUE"
241247
},
@@ -539,9 +545,6 @@
539545
{
540546
"name": "RouterEvent"
541547
},
542-
{
543-
"name": "RouterInitializer"
544-
},
545548
{
546549
"name": "RouterLink"
547550
},
@@ -1142,9 +1145,6 @@
11421145
{
11431146
"name": "getAllRouteGuards"
11441147
},
1145-
{
1146-
"name": "getAppInitializer"
1147-
},
11481148
{
11491149
"name": "getBeforeNodeForView"
11501150
},
@@ -1697,6 +1697,9 @@
16971697
{
16981698
"name": "provideForRootGuard"
16991699
},
1700+
{
1701+
"name": "provideInitialNavigation"
1702+
},
17001703
{
17011704
"name": "provideRoutes"
17021705
},

packages/router/src/router_module.ts

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

9-
import {APP_BASE_HREF, HashLocationStrategy, Location, LOCATION_INITIALIZED, LocationStrategy, PathLocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
10-
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, ENVIRONMENT_INITIALIZER, Inject, inject, Injectable, InjectFlags, InjectionToken, Injector, ModuleWithProviders, NgModule, NgProbeToken, OnDestroy, Optional, Provider, SkipSelf, Type} from '@angular/core';
11-
import {Title} from '@angular/platform-browser';
9+
import {HashLocationStrategy, Location, LOCATION_INITIALIZED, LocationStrategy, PathLocationStrategy, ViewportScroller} from '@angular/common';
10+
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, ENVIRONMENT_INITIALIZER, Inject, inject, InjectFlags, InjectionToken, Injector, ModuleWithProviders, NgModule, NgProbeToken, Optional, Provider, SkipSelf, Type} from '@angular/core';
1211
import {of, Subject} from 'rxjs';
1312

1413
import {EmptyOutletComponent} from './components/empty_outlet';
@@ -42,17 +41,19 @@ const ROUTER_DIRECTIVES =
4241
*
4342
* @publicApi
4443
*/
45-
export const ROUTER_CONFIGURATION = new InjectionToken<ExtraOptions>('ROUTER_CONFIGURATION', {
46-
providedIn: 'root',
47-
factory: () => ({}),
48-
});
44+
export const ROUTER_CONFIGURATION =
45+
new InjectionToken<ExtraOptions>(NG_DEV_MODE ? 'router config' : 'ROUTER_CONFIGURATION', {
46+
providedIn: 'root',
47+
factory: () => ({}),
48+
});
4949

5050
/**
5151
* @docsNotRequired
5252
*/
53-
export const ROUTER_FORROOT_GUARD = new InjectionToken<void>('ROUTER_FORROOT_GUARD');
53+
export const ROUTER_FORROOT_GUARD = new InjectionToken<void>(
54+
NG_DEV_MODE ? 'router duplicate forRoot guard' : 'ROUTER_FORROOT_GUARD');
5455

55-
const ROUTER_PRELOADER = new InjectionToken<RouterPreloader>('');
56+
const ROUTER_PRELOADER = new InjectionToken<RouterPreloader>(NG_DEV_MODE ? 'router preloader' : '');
5657

5758
export const ROUTER_PROVIDERS: Provider[] = [
5859
Location,
@@ -139,6 +140,7 @@ export class RouterModule {
139140
provideRouterScroller(),
140141
config?.preloadingStrategy ? providePreloading(config.preloadingStrategy) : [],
141142
{provide: NgProbeToken, multi: true, useFactory: routerNgProbeToken},
143+
config?.initialNavigation ? provideInitialNavigation(config) : [],
142144
provideRouterInitializer(),
143145
],
144146
};
@@ -511,119 +513,123 @@ export function rootRoute(router: Router): ActivatedRoute {
511513
return router.routerState.root;
512514
}
513515

514-
/**
515-
* Router initialization requires two steps:
516-
*
517-
* First, we start the navigation in a `APP_INITIALIZER` to block the bootstrap if
518-
* a resolver or a guard executes asynchronously.
519-
*
520-
* Next, we actually run activation in a `BOOTSTRAP_LISTENER`, using the
521-
* `afterPreactivation` hook provided by the router.
522-
* The router navigation starts, reaches the point when preactivation is done, and then
523-
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
524-
*/
525-
@Injectable()
526-
export class RouterInitializer implements OnDestroy {
527-
private initNavigation = false;
528-
private destroyed = false;
529-
private resultOfPreactivationDone = new Subject<void>();
530-
531-
constructor(private injector: Injector) {}
532-
533-
appInitializer(): Promise<any> {
534-
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
535-
return p.then(() => {
536-
// If the injector was destroyed, the DI lookups below will fail.
537-
if (this.destroyed) {
538-
return Promise.resolve(true);
539-
}
540-
541-
let resolve: Function = null!;
542-
const res = new Promise(r => resolve = r);
543-
const router = this.injector.get(Router);
544-
const opts = this.injector.get(ROUTER_CONFIGURATION);
545-
546-
if (opts.initialNavigation === 'disabled') {
547-
router.setUpLocationChangeListener();
548-
resolve(true);
549-
} else if (opts.initialNavigation === 'enabledBlocking') {
550-
router.afterPreactivation = () => {
551-
// only the initial navigation should be delayed
552-
if (!this.initNavigation) {
553-
this.initNavigation = true;
554-
resolve(true);
555-
return this.resultOfPreactivationDone;
556-
557-
// subsequent navigations should not be delayed
558-
} else {
559-
return of(void 0);
560-
}
561-
};
562-
router.initialNavigation();
563-
} else {
564-
resolve(true);
565-
}
566-
567-
return res;
568-
});
569-
}
570-
571-
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
572-
const opts = this.injector.get(ROUTER_CONFIGURATION);
573-
const routerScroller: RouterScroller|null =
574-
this.injector.get(ROUTER_SCROLLER, null, InjectFlags.Optional);
575-
const router = this.injector.get(Router);
576-
const ref = this.injector.get<ApplicationRef>(ApplicationRef);
516+
export function getBootstrapListener() {
517+
const injector = inject(Injector);
518+
return (bootstrappedComponentRef: ComponentRef<unknown>) => {
519+
const ref = injector.get(ApplicationRef);
577520

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

525+
const router = injector.get(Router);
526+
const bootstrapDone = injector.get(BOOTSTRAP_DONE);
527+
582528
// Default case
583-
if (opts.initialNavigation === 'enabledNonBlocking' || opts.initialNavigation === undefined) {
529+
if (injector.get(INITIAL_NAVIGATION, null, InjectFlags.Optional) === null) {
584530
router.initialNavigation();
585531
}
586532

587-
this.injector.get(ROUTER_PRELOADER, null, InjectFlags.Optional)?.setUpPreloading();
588-
routerScroller?.init();
533+
injector.get(ROUTER_PRELOADER, null, InjectFlags.Optional)?.setUpPreloading();
534+
injector.get(ROUTER_SCROLLER, null, InjectFlags.Optional)?.init();
589535
router.resetRootComponentType(ref.componentTypes[0]);
590-
this.resultOfPreactivationDone.next(void 0);
591-
this.resultOfPreactivationDone.complete();
592-
}
593-
594-
ngOnDestroy() {
595-
this.destroyed = true;
596-
}
597-
}
598-
599-
export function getAppInitializer(r: RouterInitializer) {
600-
return r.appInitializer.bind(r);
601-
}
602-
603-
export function getBootstrapListener(r: RouterInitializer) {
604-
return r.bootstrapListener.bind(r);
536+
bootstrapDone.next();
537+
bootstrapDone.complete();
538+
};
605539
}
606540

541+
// TODO(atscott): This should not be in the public API
607542
/**
608543
* A [DI token](guide/glossary/#di-token) for the router initializer that
609544
* is called after the app is bootstrapped.
610545
*
611546
* @publicApi
612547
*/
613-
export const ROUTER_INITIALIZER =
614-
new InjectionToken<(compRef: ComponentRef<any>) => void>('Router Initializer');
548+
export const ROUTER_INITIALIZER = new InjectionToken<(compRef: ComponentRef<any>) => void>(
549+
NG_DEV_MODE ? 'Router Initializer' : '');
550+
551+
function provideInitialNavigation(config: Pick<ExtraOptions, 'initialNavigation'>): Provider[] {
552+
return [
553+
config.initialNavigation === 'disabled' ? provideDisabledInitialNavigation() : [],
554+
config.initialNavigation === 'enabledBlocking' ? provideEnabledBlockingInitialNavigation() : [],
555+
];
556+
}
557+
558+
function provideRouterInitializer(): ReadonlyArray<Provider> {
559+
return [
560+
// ROUTER_INITIALIZER token should be removed. It's public API but shouldn't be. We can just
561+
// have `getBootstrapListener` directly attached to APP_BOOTSTRAP_LISTENER.
562+
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener},
563+
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
564+
];
565+
}
566+
567+
/**
568+
* A subject used to indicate that the bootstrapping phase is done. When initial navigation is
569+
* `enabledBlocking`, the first navigation waits until bootstrapping is finished before continuing
570+
* to the activation phase.
571+
*/
572+
const BOOTSTRAP_DONE =
573+
new InjectionToken<Subject<void>>(NG_DEV_MODE ? 'bootstrap done indicator' : '', {
574+
factory: () => {
575+
return new Subject<void>();
576+
}
577+
});
615578

616-
export function provideRouterInitializer(): ReadonlyArray<Provider> {
579+
function provideEnabledBlockingInitialNavigation(): Provider {
617580
return [
618-
RouterInitializer,
581+
{provide: INITIAL_NAVIGATION, useValue: 'enabledBlocking'},
619582
{
620583
provide: APP_INITIALIZER,
621584
multi: true,
622-
useFactory: getAppInitializer,
623-
deps: [RouterInitializer]
585+
deps: [Injector],
586+
useFactory: (injector: Injector) => {
587+
const locationInitialized: Promise<any> =
588+
injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
589+
let initNavigation = false;
590+
591+
return () => {
592+
return locationInitialized.then(() => {
593+
return new Promise(resolve => {
594+
const router = injector.get(Router);
595+
const bootstrapDone = injector.get(BOOTSTRAP_DONE);
596+
597+
router.afterPreactivation = () => {
598+
// only the initial navigation should be delayed
599+
if (!initNavigation) {
600+
initNavigation = true;
601+
resolve(true);
602+
return bootstrapDone;
603+
// subsequent navigations should not be delayed
604+
} else {
605+
return of(void 0);
606+
}
607+
};
608+
router.initialNavigation();
609+
});
610+
});
611+
};
612+
}
624613
},
625-
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
626-
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
614+
];
615+
}
616+
617+
const INITIAL_NAVIGATION =
618+
new InjectionToken<'disabled'|'enabledBlocking'>(NG_DEV_MODE ? 'initial navigation' : '');
619+
620+
function provideDisabledInitialNavigation(): Provider[] {
621+
return [
622+
{
623+
provide: APP_INITIALIZER,
624+
multi: true,
625+
useFactory: () => {
626+
const router = inject(Router);
627+
return () => {
628+
router.setUpLocationChangeListener();
629+
};
630+
}
631+
},
632+
{provide: INITIAL_NAVIGATION, useValue: 'disabled'}
627633
];
628634
}
629635

0 commit comments

Comments
 (0)