Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): route initializer #36084

Closed

Conversation

kristofdegrave
Copy link

@kristofdegrave kristofdegrave commented Mar 16, 2020

This is an initial POC to provide an initialization hook for lazy loaded Modules. Implementation is Open for Discussion!

Additional Docs will be added when a consent about the solution is made.

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.io application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

A lazy route is now just loaded without being block by initialization logic

Issue Number: #17606

What is the new behavior?

A lazy route is now loaded with an option being block by initialization logic. If all initialization logic has run, the route gets loaded

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

It introduces a new injection toke ROUTE_INITIALIZER.

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here with @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@pullapprove pullapprove bot requested a review from atscott March 16, 2020 11:44
@kristofdegrave
Copy link
Author

@googlebot I signed it!

Add an injection token ROUTE_INITIALIZE to provide one or more initialization functions.
These function are injected when a route is loaded. If any of these functions returns
a Promise, initialization does not complete until the Promise is resolved. If the initialization
is completed the LoadedRouterConfig will be returned.
Did some code refactoring after feedback from frederikprijck.
@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@AndrewKushnir AndrewKushnir added area: router feature Issue that requests a new feature labels Mar 18, 2020
@ngbot ngbot bot added this to the needsTriage milestone Mar 18, 2020
@steve-todorov
Copy link

Hey @atscott, have you had a chance to checkout this implementation yet? :)

Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this provide value beyond what CanActivate at the forChild root could do?

RouterModule.forChild([
  {path: '', canActivate: [MyInitializerGuard], children: [...]}
]);

The canActivate there would allow for asynchronous initialization before attempting to activate any child routes in the module. If you think of canActivate conceptually like "can this route activate yet?" rather than "is it allowed to activate at all?" it makes a bit more sense to do this.

if (this.onLoadEndListener) {
this.onLoadEndListener(route);
}

const module = factory.create(parentInjector);
const routeInits: (() => any)[] = module.injector.get(ROUTE_INITIALIZER, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type should be consistent with the injection token: Array<() => void> here and in runInitializers

}));
}

private runInitializers(routeInits: (() => any)[] = []) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value [] isn't necessary here.

}));
}

private runInitializers(routeInits: (() => any)[] = []) {
return from(Promise.all(routeInits.map(routeInit => routeInit()).filter(isPromise)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be easier to understand if you extracted some variables. I see what you're doing here and I do like how clever it is, but it might more immediately understandable if it were deconstructed a bit.

Suggested change
return from(Promise.all(routeInits.map(routeInit => routeInit()).filter(isPromise)));
const initResults = routeInits.map(routeInit => routeInit());
const promiseInits = initResults.filter(isPromise);
return from(Promise.all(promiseInits));

flatten(module.injector.get(ROUTES)).map(standardizeConfig), module);
return this.runInitializers(routeInits)
.pipe(
map(() => new LoadedRouterConfig(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right operator here? While from(Promise.all) would only emit once, this code shouldn't rely on the implementation details of the runInitializers function. I think this should instead be last or even finalize/finally. What happens when an initializer rejects isn't really discussed here or tested (please add a test or two for it).

/**
* The [DI token](guide/glossary/#di-token) for a router configuration.
* @see `ROUTES`
* @publicApi
*/
export const ROUTES = new InjectionToken<Route[][]>('ROUTES');

/**
* An injection token that allows you to provide one or more initialization functions.
* These function are injected when a route is loaded. If any of these functions returns
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* These function are injected when a route is loaded. If any of these functions returns
* These function are injected when a module is loaded. If any of these functions returns

These initializers have more to do with the modules being lazy loaded than they do with a "Route". This documentation is a bit misleading and seems to imply the initializers are linked to Routes and not the act of lazy loading the module they're provided in. It's maybe a subtle distinction, but I think it's important.

That said, maybe the token token name needs some more bikeshedding

* a Promise, initialization does not complete until the Promise is resolved.
*
* You can, for example, create a factory function that loads language data
* or an external configuration, and provide that function to the `ROUTER_INITIALIZER` token.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* or an external configuration, and provide that function to the `ROUTER_INITIALIZER` token.
* or an external configuration, and provide that function to the `ROUTE_INITIALIZER` token.

@@ -0,0 +1,183 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are good, but it would also be nice to have coverage in integration.spec.ts to verify correct behavior during route activation.

import {Compiler, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
import {isPromise} from '@angular/core/src/util/lang';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be imported from @angular/core like what's done in utils/collection.ts

@@ -6,19 +6,37 @@
* found in the LICENSE file at https://angular.io/license
*/

// TODO(i): switch to fromPromise once it's expored in rxjs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove this comment

@kristofdegrave
Copy link
Author

@atscott I think the canActivate hook will be to late. The loading of the module depends on the loading strategy and can be preloaded. The case I need it for is to make sure that I can fetch configuration for example before the the module and it's dependencies are loaded, because they can depend on that configuration.
I will look into the review you gave asap.

@atscott
Copy link
Contributor

atscott commented Aug 24, 2020

@atscott I think the canActivate hook will be to late. The loading of the module depends on the loading strategy and can be preloaded. The case I need it for is to make sure that I can fetch configuration for example before the the module and it's dependencies are loaded, because they can depend on that configuration.

Outside of preloading, CanLoad could be used in this case. With preloading, it seems like you could actually build this functionality into your preloader. I think you’re going to need to provide more concrete examples before we can consider moving this PR forward. It seems like the existing router guards allow for handling the behavior desired. This also does not resolve the linked issue since modules can be loaded without the router.

@kristofdegrave
Copy link
Author

@atscott never looked this way at it, you could have a point. The desired behavior is the same as the APP_INITIALIZER I want to have, and ideal it shouldn't matter if the module is loaded immediately or trough a lazy route.
I wanted this behavior to prepare my application to move to a MicroFrontend architecture. So every (routed) module would be a micro frontend in this case, with it's own configuration file. So to get this behavior right I should write some logic in the preloader and the canLoad guard that gets the APP_INITIALIZER tokens from the module that needs to get loaded and initialize them, but is this even possible?
The module would need to be loaded before I can get them in those cases? Also I would need something to determine if the initialization has run

@atscott
Copy link
Contributor

atscott commented Aug 25, 2020

The module would need to be loaded before I can get them in those cases? Also I would need something to determine if the initialization has run

That's right, I was really just suggesting canLoad because you said canActivate is too late. I guess what you meant was that if the module is preloaded, canActivate still won't run until you try navigating to a route in that module but you would want the initializers to run before that.

Here's an [over-engineered] example of how this could all work:
https://stackblitz.com/edit/angular-ivy-72wgbc?file=src%2Fapp%2Finitializing-router.module.ts
Unfortunately, there's still a "small" hack since _loadedConfig is internal and the preloader doesn't technically have any way to access the module that was loaded. Maybe there's a way to do this that I'm not thinking of... Other than that, I think the would be what you're looking for.

@atscott
Copy link
Contributor

atscott commented Dec 4, 2020

Closing this PR per the comments mentioned above, namely:

  • This does not resolve the linked issue since modules can be loaded without the router.
  • Some of this behavior can be achieved with the router guards
  • I think this should be addressed in a more complete way along with Ergonomic component-level code splitting and lazy loading #18093 rather than extending the router API to only partially fix the issues.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jan 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: router cla: yes feature Issue that requests a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants