Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat($injector): Deferred loading of providers into the injector #11015

Closed
btford opened this issue Feb 9, 2015 · 76 comments
Closed

feat($injector): Deferred loading of providers into the injector #11015

btford opened this issue Feb 9, 2015 · 76 comments

Comments

@btford
Copy link
Contributor

btford commented Feb 9, 2015

This is a draft for this feature. I'm still working on the API. Feedback welcome.

Summary

This is a proposal for a new API in Angular that would make it easier for developers to lazy load parts of their application.

This proposal is only for adding loaded providers into a bootstrapped app's injector. It does not include the lazy loading functionality itself, but rather complements module loaders like require.js, webpack, or SystemJS.

Motivations

Developers want to be able to lazy load application code for a variety of reasons. One is to deliver smaller initial payloads. Another is to hide application logic from unprivileged users (think admin panels) as a first layer of security.

Developers are already hacking their own lazy-loading solutions into 1.x, so there's a clear demand for support. Angular 2 will support lazy-loading, so having facilities in Angular 1 means we can write facades that better align APIs (for example, in the new router) and ease migration.

Finally, this API only addresses adding loaded code into Angular's injector, and does not implement lazy loading itself. There are already existing tools that can do this, so we just want to provide a nice API for them to hook into Angular.

Implementation

The implementation would consist of a new method on Angular Modules and a new service.

You would register some placeholder in the injector. Then you use a service to add the provider later.

lazy.js:

var lazyModule = angular.module('lazy', []);
lazyModule.factory('a', );

app.js:

var ngModule = angular.module('app', []);

// names of services that some module will provide
ngModule.placeholder(['a', 'b', 'c']);
ngModule.directivePlaceholder(['myFirstDirective', 'mySecondDirective']);

ngModule.run(function($lazyProvide) {
  $lazyProvide.addModule('lazy'); // or: $lazyProvide.addModule(lazyModuleRef);
});

The only change to the behavior of the injector is that trying to inject a service that only has a placeholder, and not a corresponding provider implementation will throw a new type of error. The injector is still synchronous. HTML that has already been compiled, will not be affected by newly loaded directives.

Placeholders

The goal of this placeholder API is to make it easy to reason about how an app should be put together. Still, it's important that the ability to lazy-load parts of your app isn't prohibitively expensive because the work of adding the placeholders is too much.

The placeholder API is designed so that if a developer is using the AngularJS module system in an idiomatic way, you could statically analyze an app and automatically generate placeholders. ng-annotate already has most of this functionality, so I suspect it'd be easy to add generating placeholders to it.

Okay but I really hate the placeholders thing

You can disable the requirement to have a placeholder before a module is lazily added with the following directive:

<div ng-app="2Cool4SchoolApp" ng-allow-dangerous-lazy-providers>
  <!-- ... -->
</div>

This works the same as ngStrictDi.

If manually bootstrapping, you can use the following bootstrap option:

angular.bootstrap(someElt, ['2Cool4SchoolApp'], {
  allowDangerousLazyProviders: true
});

This is for developers who really know what they're doing, and are willing to maintain the invariants about lazy loading manually.

My goal is to make it so easy to provide placeholders that we can deprecate this API because it's never used. But because there's a clear demand for such an option in the Angular community, I want to make sure that it's possible.

Module redefinition

To avoid situations where it's ambiguous which implementation of a module is used in an app, once an app has been bootstrapped, any modules that include a placeholder cannot be redefined. Trying to redefine a module like this will throw an error:

it('should throw if a module with placeholders is redefined after being bootstrapped', function () {
  angular.module('lazy', []).placeholder(['a', 'b', 'c']);
  angular.bootstrap(document, ['lazy']);
  expect(function () {
    angular.module('lazy', []).placeholder(['d', 'e', 'f']);
  }).toThrow();
});

it('should not throw if a module with placeholders is redefined before being bootstrapped', function () {
  angular.module('lazy', []).placeholder(['a', 'b', 'c']);
  expect(function () {
    angular.module('lazy', []).placeholder(['d', 'e', 'f']);
  }).not.toThrow();
  angular.bootstrap(document, ['lazy']);
  // expect the bootstrapped app to have placeholders for `d`, `e`, `f`.
});

Adding to a module after bootstrap

To avoid situations where it's ambiguous what is actually included in a module, once a module with placeholders has been included in a bootstrapped app, it cannot have new placeholders or providers added to it.

it('should throw if a module has placeholders added after being bootstrapped', function () {
  angular.module('lazy', []).placeholder(['a', 'b', 'c']);
  angular.bootstrap(document, ['lazy']);
  expect(function () {
    angular.module('lazy').placeholder(['d', 'e', 'f']);
  }).toThrow();
  expect(function () {
    angular.module('lazy').factory('foo', function () {});
  }).toThrow();
});

it('should not throw if a module has placeholders added before being bootstrapped', function () {
  angular.module('lazy', []).placeholder(['a', 'b', 'c']);
  expect(function () {
    angular.module('lazy').placeholder(['d', 'e', 'f']);
  }).not.toThrow();
  angular.bootstrap(document, ['lazy']);
  // expect the bootstrapped app to have placeholders for `a`, `b`, `c`, `d`, `e`, and `f`.
});

Loading new code

Loading the provider implementation would be left up to the application developer. It is the responsibility of the developer to make sure that components are loaded at the right time.

For instance, you might use it with ngRoute's resolve:

$routeConfig.when('/', {
  controller: 'MyController',
  resolve: {
    'a': () => $http.get('./lazy.js').then((contents) => {
        // this example is silly
        $injector.addModule(eval(contents));
        return $injector.get('a');
      })
    }
  });

This API intentionally does not include a way to "unload" a service or directive.

Run and config blocks

Lazy loaded modules will not run config blocks:

it('should throw if a module has config blocks', function () {
  angular.module('lazy', []).config(function () {});
  expect(function () {
    $injector.addModule('lazy');
  }).toThrow();
});

But lazily loaded modules will run run blocks when they are loaded.

it('should run run blocks', function () {
  var spy = jasmine.createSpy();
  angular.module('lazy', []).run(spy);
  $injector.addModule('lazy');
  // flush
  expect(spy).toHaveBeenCalled();
});

Risks

The API should mitigate the possibility of making it difficult about the state of the injector. For example, we want developers to be able to distinguish between a case where a user mistyped a provider's name from a case when it was requested before it was loaded. Since compiled templates will not be affected by lazily loaded directives, the compiler should also warn if it compiles a template with placeholder directives, but not their implementation.

On the other hand, the "placeholders" should not require too much upkeep, otherwise this API would be too cumbersome to use. Ideally, the placeholders could be automatically generated at build time.

Prior art

I looked at these lazy-loading solutions:

@btford btford changed the title feat($injector): Deferred loading of providers into Angular 1.4's injector feat($injector): Deferred loading of providers into the injector Feb 9, 2015
@petebacondarwin
Copy link
Member

So just to be clear: this solution explicitly does not support unloading of components, right?

Also, although it allows directives to be lazily loaded, HTML that has already been compiled, will not then receive the benefit of the newly loaded directives, right?

Finally, what is the motivation for using a new service $lazyProvide, rather than, say, adding a new method to the $injector? The latter would seem more intuitive to me.

@btford
Copy link
Contributor Author

btford commented Feb 9, 2015

So just to be clear: this solution explicitly does not support unloading of components, right?

yes – amended my original post

Also, although it allows directives to be lazily loaded, HTML that has already been compiled, will not then receive the benefit of the newly loaded directives, right?

yes – amended my original post

Finally, what is the motivation for using a new service $lazyProvide, rather than, say, adding a new method to the $injector? The latter would seem more intuitive to me.

Agreed. That seems better.

@ocombe
Copy link
Contributor

ocombe commented Feb 9, 2015

Wooo this is awesome!! Sad for my lib, but awesome for angular !
I like the placeholder idea, and also the new injector function, you should add both.

Also, any chance that we could add an interceptor to the loading of a placeholder ?

Here is the scenario: you access a controller that requires a service, angular will try to inject the service, this service hasn't been loaded yet and the interceptor (I say interceptor, but it might be something that you define in the config of your main module) will load the new file and return a promise to the controller which will not be instantiated until the promise is resolved. This would work the same way as the resolve method in the router.

@lgalfaso
Copy link
Contributor

lgalfaso commented Feb 9, 2015

This looks very promising. There are a few things that I find not clear, probably the most problematic is how will this work when there is a module redefinition. Eg.

var moduleA = angular.module('lazy', []);
moduleA.placeholder(['a', 'b', 'c']);
moduleA.run(function($lazyProvide) {
  [...]
});

var moduleX = angular.module('foo', ['lazy']);

[...]

var anotherModuleA = angular.module('lazy', []);
moduleA.placeholder(['d', 'e', 'f']);
moduleA.run(function($lazyProvide) {
  [...]
});

var moduleY = angular.module('bar', ['lazy']);

In this case, there are two apps, the app foo and the app bar. The former uses one definition of a module named lazy and the later another. How should the implementation of each of those lazy look like? The reason being that it cannot be

var lazyModule = angular.module('lazy');
lazyModule.service(...);

as if this is done after the module redefinition, then it will add these service to the new version of lazy.

A second question is the following (in this scenario there are no modules redefinitions at all): If there is a lazy module, and this lazy module is used in two different apps. If the first of the apps does whatever needed happen and the implementation is loaded. Will this also load the definition for the second app?

@petebacondarwin
Copy link
Member

@lgalfaso - this is not how I understand it working. You do not declare a dependency upon a lazy loaded module. You specify in your loaded module placeholders that will be provided by some lazy loaded module later.

So in your example, the injector gets created with a bunch of placeholders for services. The actual implementation of these services can be loaded via any kind of module at a later time:

  • app foo's $injector contains placeholders for a, b, c
  • app bar's $injector contains placeholders for a, b, c, d, e, f

We don't reopen modules to load the lazy components. We actually create a completely new module and load it into the injector - it just happens to fill some or all of these placeholders.

@lgalfaso
Copy link
Contributor

lgalfaso commented Feb 9, 2015

Right now, when the line var anotherModuleA = angular.module('lazy', []); is executed, then a new module named lazy is created, but foo is still hooked to the old lazy. When bar is created, then it is hooked to the new lazy. Will this change?

@shahata
Copy link
Contributor

shahata commented Feb 9, 2015

I'm not sure I understand the purpose of having to define placeholders for the lazy loaded module. Is this only so that users get a more descriptive exception if they try to use those services prematurely? What happens to services that are defined in the lazy loaded module and were not defined as placeholders? Won't they get added to the injector as well when the lazy module is loaded into the injector?

@vitaly-t
Copy link

vitaly-t commented Feb 9, 2015

I don't see it being a good idea.

If you look at the definition of Lazy Loading, you will see it comes from languages (C, C++, C#) that support the concept of interfaces, which gives the natural ability to intercept invalid calls, i.e. calls on instances not yet loaded, so they can be loaded on demand. JavaScript cannot intercept invalid calls, so unless you provide a coding around for every single call attempt, things won't work. And throwing an exception as a standard approach isn't a good idea for JavaScript. Your Achilles heel is right there.

The only lazy loading that JavaScript can do is loading up pieces of code in accordance with the UI partitioning as dictated by the business/dependency logic. For this you already have: Browserify and Webpack. I sincerely hope you are not suggesting to implement one of those as part of Angular core, it would be a shot in the head. A separate module though might make more sense, but the existing alternatives are already too good to just pass them by. There are only so many things Angular can absorb before imploding on the account of its overall reliability (800 open issues on the list speak for themselves).

Links:
http://webpack.github.io/
http://esa-matti.suuronen.org/blog/2013/04/15/asynchronous-module-loading-with-browserify/
http://en.wikipedia.org/wiki/Lazy_loading

@btford
Copy link
Contributor Author

btford commented Feb 10, 2015

@VitalyTomilov – not sure if you read the proposal, but it's for an API to be used alongside a module loader, not a module loader itself. The idea is to complement something like webpack or require.js. Your points seem targeted at the "virtual proxy" style of lazy loading, which is totally unrelated to the implementation being proposed.

I added a bit more to the summary to make this more explicit.

@btford
Copy link
Contributor Author

btford commented Feb 10, 2015

@shahata – great questions.

I'm not sure I understand the purpose of having to define placeholders for the lazy loaded module. Is this only so that users get a more descriptive exception if they try to use those services prematurely?

Yes. This is something @IgorMinar suggested.

What happens to service that are defined in the lazy loaded and were not defined as placeholders? Won't they get added to the injector as well when the lazy module is loaded into the injector?

Let's say you load module 'lazy' with some service foo for which a placeholder was not provided. If you attempt to lazily add a provider for foo, the injector will throw an error, explaining that it did not expect foo.

I'm trying to think how to clarify my original post with these answers. Suggestions welcome!

@shahata
Copy link
Contributor

shahata commented Feb 10, 2015

I think this "strict" behavior of throwing if a provider without a placeholder is added lazily should be optional (maybe even opt-in). It sounds like a place where people will keep banging their head when they add some service in the lazy loaded module and forget to add a placeholder....

@btford
Copy link
Contributor Author

btford commented Feb 10, 2015

Also, any chance that we could add an interceptor to the loading of a placeholder ?

Here is the scenario: you access a controller that requires a service, angular will try to inject the service, this service hasn't been loaded yet and the interceptor (I say interceptor, but it might be something that you define in the config of your main module) will load the new file and return a promise to the controller which will not be instantiated until the promise is resolved. This would work the same way as the resolve method in the router.

@ocombe – this is something I already considered, but it would mean totally rewriting the injector (and anything that uses it) to be totally asynchronous. This means rewriting the compiler, and anything that relies on it. In short, it's entirely too much work and would be a huge breaking change. The good news is that Angular 2 will support Async DI out-of-the-box, so you'll be able to do something like what you propose.

I think the proposed API makes it easy to lazy-load on route changes, which seem to be the 90% case. And it does so without a sweeping breaking change.

@geddski
Copy link
Contributor

geddski commented Feb 10, 2015

So glad this is on the radar! Thanks @btford. I agree with @shahata though on the placeholders. If you have to define a placeholder for every service, directive, filter, controller etc to be lazy loaded that would be an enormous amount of extra work for the developer. It's also redundant information that now lives outside of your lazy-load boundaries, reducing the benefit of separation/lazy loading in the first place. Could the placeholders with their associated error messages be optional?

@petebacondarwin
Copy link
Member

The idea of placeholders is not so painful if you think of it in terms of "module interfaces". the idea is that for any module that you wish to lazy load, you provide an empty shell module that only specifies the placeholders.

For example we could load these three modules at our normal application startup:

angular.module('myApp', ['lazy1Interface', 'lazy2Interface']);


angular.module('lazy1Interface', []).placeholder(['a', 'b', 'c');
angular.module('lazy2Interface', []).placeholder(['d', 'e', 'f');

Then later we could lazily load modules containing the real implementation of those services:

var lazy1 = angular.module('lazy1', [])
  .factory('a', function() { ... })
  .factory('b', function() { ... })
  .factory('c', function() { ... });

$injector.lazyLoad(lazy1);

var lazy2 = angular.module('lazy2', [])
  .factory('d', function() { ... })
  .factory('e', function() { ... })
  .factory('f', function() { ... });

$injector.lazyLoad(lazy2);

One could even generate these lazy interface modules automatically from you concrete modules if you wanted via a build tool.

@btford
Copy link
Contributor Author

btford commented Feb 10, 2015

If you have to define a placeholder for every service, directive, filter, controller etc to be lazy loaded that would be an enormous amount of extra work for the developer.

Agree that this is tedious by hand, but I think this it's solvable with tooling.

It's also redundant information that now lives outside of your lazy-load boundaries, reducing the benefit of separation/lazy loading in the first place.

The information isn't exactly redundant, but it's true that this info is only useful during development.

Could the placeholders with their associated error messages be optional?

I'm worried if we go this route, developers will at first not find the feature valuable, only to later discover that they want it when their app is complex. It's the same trap some developers fell into with DI and minification.

@shahata
Copy link
Contributor

shahata commented Feb 10, 2015

@petebacondarwin @btford the thing is that the services defined by some module are not by any way its public interface. A module might have many services which it uses only internally and not any business of anyone who wants to load it lazily. More over, imagine I set up some lazy load for some module and everything works fine - a new version of that module which was refactored to extract some internal service could completely break my app...

@shahata
Copy link
Contributor

shahata commented Feb 10, 2015

Another thing (unrelated to the current discussion) - the description above talks a lot about lazy loading providers, but actually we cannot lazy load providers (as in angular.module('myApp').provider(function () {});), right? (since the "config" phase is already over by now)

@ocombe
Copy link
Contributor

ocombe commented Feb 10, 2015

Yeah that might be a problem, you might not know the full extent of the services/directives/... of a module. You might just use one service, but if you need to predefine them all then you will have to read the source code for that.

Why do we need to define placeholders like that in the first place? Couldn't you just allow the definition of any angular component at any time? If you lazy load a new module it will define its own services & directives at the same time and you should execute the associated invokeQueue & configBlocks when they are defined (if the app has already been bootstrapped).

I get that you might want to know that a service is a placeholder when you try to instantiate it. Maybe we could have a specific syntax for that like ::$service instead of $service ? Or maybe we could just define the place holder for the services we need in our not-lazy-loaded component, but if a new lazy loaded module has more services than you defined then they might still work (because they are defined at the same time of the module?).

@gautelo
Copy link

gautelo commented Feb 13, 2015

My concern

The instructions of what to load (url), and how to load them (procedure) is located where you request the module, rather than where you declare your module placeholder. This means that if you need the same thing in several places you have to maintain the instructions in all of those places. This means two things:

  1. We have to add instructions to the consumers, which isn't very DRY, and moving files around means you need to update all of those urls (which is allready annoying for templateUrls).
  2. If you start without lazy-loading and want to turn it on later, you have to modify your code by adding instructions.

Let's deal with them one at a time.

Counter proposal 1

To deal with the first issue I propose that we add a new declaration syntax which, in addition to the names of the injectable symbols and directives, includes the url or loading-function of the module:

// Same base for both approaches
var mod = angular.module('mymod', ['dep1','dep2'])
mod.lazySymbols(['a', 'b', 'c'])
mod.lazyDirectives(['dir1', 'dir2'])

// By url
mod.lazyUrl('./mymod.js');

// By loader function
mod.lazyLoader(myLoader, ['stuffArgs']);

function myLoader(stuff) {
  return "the code stuff";
}

In effect this simply moves the instructions of how and what to load from the consumer to the declaration. Also I made some changes to naming which I like. This way each lazy-loading method starts with "lazy" so that it's obvious that they belong together. (What's the common term for services, values, factories etc? Would lazyInjectables() be better than lazySymbols()?)

The result is that tooling can now rewrite urls for us automatically without messing with our source code, allowing us to move things around without worrying. Furthermore this is much cleaner as regards separation of concerns; the consumer really shouldn't care where the module comes from, just that it needs that module.

If you keep this proposal, but discard my counter proposal part two then we will still need code at the consumer asking for a lazyloaded module. However it is now much simpler than in the original proposal. Where the consumer used to say "I need that module to be loaded from that location in this way" it is now simply saying "I need that module to be loaded", which I think is a pretty big improvement. Here's what it will look like:

$routeConfig.when('/', {
  controller: function(a){},
  resolve: {
    '__lazy': () => $injector.lazy('mymod')
    }
  });

Where $injector.lazy() returns a promise which resolves once the module has been loaded and the placeholders replaced. Once that's done calling $injector.get('a') should work as normal, and the controller and compiler should get what they expect.

Notes

  • Doing an $injector.lazy('mymod') should also load any downstream modules it depends on that are also lazy declared.
  • It should be permissible to provide the same lazyUrl(..) for several modules and have them all defined in the same file.
  • Those two mean you usually shouldn't have to load many modules. But if you do, you can use $q.all or add several resolve properties.

Counter proposal 2

In order to deal with issue 2, we have to go a step further. If the $injector is going to be able to lazy load lazy-declared modules seamlessly it would need to be based on promisses. By changing the $injector so that $injector.get() would return a promisse it could use a preloaded or lazyloaded module interchangeably.

I am unsure how much work would be involved, but my gut feeling is that it isn't trivial. I mean, that last snippet with the one-liner resolve didn't look that bad, so I'm inclined to say that it might not be worth it. But I think it's at least worth discussing.

The result

  • We wouldn't need a resolve block at all.
  • It would be a breaking change for anybody calling $injector.get(). However I find it probable that mostly people call it to get around the limitations we are discussing, and possibly for some manual bootstrapping, which should be fairly easy to migrate.
  • By removing the need for an $injector.lazy() method, it would be much easier to use lazy-loading of modules for any part of a system. Without it there will always be a need for a structured choice of when and architecturally where to load. In an application using a router the choice is obvious and outlined above. But not all applications uses a router.

@petebacondarwin
Copy link
Member

@gautelo - thank you for you input. I think that this issue thread has brought up some really useful points and discussion.

We have talked about making the injector async in the past but I think that this would be too much of a change for Angular 1.x. As you said, putting the load into a resolve is enough to work around this problem and itself is fairly explicit and intuitive.

Regarding, moving lazy load declaration information to the module, I really like that idea. I wonder if one could go a step further and separate the loader from the module too? Then you would have a situation where the user of the lazy loaded code would not need to know about where and how it was loaded, but also the creator of the module would not need to know what loader was to be used to load it. Does that make sense? An implementation I am thinking about would be to define the loader(s) separately with some mapping between lazy module (url?) and loader.

@gautelo
Copy link

gautelo commented Feb 13, 2015

@petebacondarwin It's funny you should mention that. I actually had a code example like this, but decided to remove it to get my point across as simply as possible.

The lazyLoader function parameter could be set globally, and then you would simply specify the arguments instead of both the loader and the argument.

// At, or before, bootstrapping
angular.setLazyLoader(myLoader);

// At module definition time
mod.lazyArgs(['stuffArgs']); // There are probably better names.

// If you don't set a custom loader this will be synonymous with
mod.lazyUrl('stuffArgs');

The problem is that this doesn't take us all the way as the content of the lazyArgs depend on the loader function, so I don't see how we can proceed to get a clean separation. We need to describe how/where to load along with the placeholders in some way shape or form.

@btford
Copy link
Contributor Author

btford commented Feb 18, 2015

@gautelo – totally agree with your first counter proposal. You should be able to add some metadata about how to load the module to where you declare the placeholders.

Your second counter-proposal would not work. See #11015 (comment)

I'll try and spend some time this evening updating my proposal accordingly.

@btford
Copy link
Contributor Author

btford commented Mar 17, 2015

@geddski @ocombe – I just added a new section called "Okay but I really hate the placeholders thing"

Let me know what you think.

@ocombe
Copy link
Contributor

ocombe commented Mar 17, 2015

Hey, works for me :)
Thanks

PS: you were coding in java before javascript? :P

@geddski
Copy link
Contributor

geddski commented Mar 17, 2015

Yay thx @btford!

@raul-arabaolaza
Copy link

Hi,

Currently at work we are implementing a very very modular angular app using lazy loading, the basic idea is to be able to create a sort of Angular RCP (a la eclipse) and create plugins instead of apps.

Every plugin is implemented as an angular module.

This means we have only one angular app but it's functionality is completely dynamic depending on the plugins/components/modules you want to load. For example, we have a main menu directive with a provider that allow us to add new items to the menu, so if we want to expose a new functionality in the app we only need to create a new plugin, hook into the main menu provider and voilá the app is extended.

Every plugin defines its dependencies that are lazy loaded (the plugins itself are also lazy loaded) into the angular app by using the great oclazyLoad library and requirejs.

The initial list of plugins to load comes from a backend service. So our platform is an angular app that simply lazy loads a bunch of components.

This gives us a great amount of flexibility we can expand/modify the app's functionality without code changes, great for role based authorization for example. And allow us to work on reusable isolated components instead of apps. Also we have only one deployed app which can give service to a lot of different user needs based on the components to load

I told all this because I believe this is a use of lazy loading you guys have not considered, all this proposal seems to be based on the idea of lazy loading components that are previously known, for performance reasons, reduce footprint, etc. But what about if I want to lazy load some modules dynamically, that is modules not known at coding time?

Some friction points:

  • Placeholder thing: The ng-allow-dangerous-lazy-providers directive seems a must, I would have nothing against placeholders if I could define them after app bootstrap (which I believe is not possible in the current proposal).
  • Config and run blocks: This is another problem, it could be very interesting for a lazy loaded module to be able to add functionality to the app using standard angular ways. One of this ways I believe is providers. If config or run blocks are not allowed then somehow lazy modules are not regular angular code but a sort of "second class citizens" in an angular app which would IMHO seriously limit its usefulness or capability to be reused as usual angular components or libraries.

HTH, Raúl

@btford
Copy link
Contributor Author

btford commented Mar 28, 2015

Config and run blocks: This is another problem, it could be very interesting for a lazy loaded module to be able to add functionality to the app using standard angular ways. One of this ways I believe is providers. If config or run blocks are not allowed then somehow lazy modules are not regular angular code but a sort of "second class citizens" in an angular app which would IMHO seriously limit its usefulness or capability to be reused as usual angular components or libraries.

Run lazily-loaded run blocks are fine, but it's very easy to introduce unintended behavior with race conditions between different values for the same settings specified in different config blocks. Config blocks are for setting up behavior before an app bootstraps. If you need to change behavior after bootstrap, you should expose this in the service itself, not the provider.

Yes, this means you might have to re-write existing code that uses config blocks incorrectly. The good news is that your app becomes easier to understand.

Can you give me a specific example where it's somehow desirable to lazy-load a module with a config block?

I told all this because I believe this is a use of lazy loading you guys have not considered, all this proposal seems to be based on the idea of lazy loading components that are previously known, for performance reasons, reduce footprint, etc. But what about if I want to lazy load some modules dynamically, that is modules not known at coding time?

This is a use case that I have specifically considered. See this comment: #11015 (comment). In the future, I'd recommend reading the entire thread before commenting.

The "ng-allow-dangerous-lazy" option is a direct result of this discussion. Apps that want this level of dynamicity at the expense of being easy to reason about can use it. Most apps know ahead of time all possible injectables, hence the placeholders.

@geddski
Copy link
Contributor

geddski commented Mar 28, 2015

Can you give me a specific example where it's somehow desirable to lazy-load a module with a config block?

Loading an isolated app that has its own routes. This is our primary use case for lazy loading.

@btford
Copy link
Contributor Author

btford commented Mar 28, 2015

Loading an isolated app that has its own routes. This is our primary use case for lazy loading.

The New Router lets you re-configure it at run-time. It will soon have a shim that lets it use ngRoute's DSL. In theory this will address your concern, correct?

@geddski
Copy link
Contributor

geddski commented Mar 28, 2015

@btford yep should. You're smarter than you look ;)

@raul-arabaolaza
Copy link

@btford
Sorry I did a quick read of comments and didn't realize the one about the plugin system :(

I know config blocks could introduce race conditions but my point is that without config blocks any module that depends upon libraries with providers could potentially not be compatible with lazy loading.
There are a lot of great libraries out there that use providers, ui router for example. In my opinion trying to find and solve specific cases seems a waste of time, if lazy loading allow the use of providers for lazy modules good, if not and we are aware of the limitations is good too.

Correct me if I am wrong but this proposal means that for a library to be usable with lazy loaded modules must port all/some/part/none configuration logic from providers to services like the new router service you are mentioning.

Honestly speaking I don't mind that on my components, as you say there are advantages on that and probably I have implemented wrong uses for providers. But I understand it could be not very interesting for library owners out there to do a big refactor in their code to make their libraries usable under lazy load. Which in turn could make this feature not worth to be used instead of the existing solutions out there

Regards, Raúl

@raul-arabaolaza
Copy link

I forgot to ask for constants, if no config blocks are run, that means a lazy loaded module can not use constants??

@btford
Copy link
Contributor Author

btford commented Mar 28, 2015

module that depends upon libraries with providers could potentially not be compatible with lazy loading

Yes, if the library is using config blocks in a way that is not intended.

There are a lot of great libraries out there that use providers, ui router for example.

The mere presence of a provider is not a problem. Also you would never lazy-load UI Router. You'd always need it upfront.

Correct me if I am wrong but this proposal means that for a library to be usable with lazy loaded modules must port all/some/part/none configuration logic from providers to services like the new router service you are mentioning.

This isn't correct. Any library you lazily load that requires reconfiguration at run-time must be re-written just for those services that require configuration. This will mean changing just a few lines of code, which is a small price to pay for determinism.

But I understand it could be not very interesting for library owners out there to do a big refactor in their code to make their libraries usable under lazy load.

Please show me an example where you'd need to do a ton of refactoring.

I forgot to ask for constants, if no config blocks are run, that means a lazy loaded module can not use constants??

This is incorrect, they'll still be useable post-config phase.

@christopherthielen
Copy link

The mere presence of a provider is not a problem. Also you would never lazy-load UI Router. You'd always need it upfront.

I think his point is that ui-router states are declared by registering them with the ui-router state provider, which obviously must happen in the config block. Any lazy loaded user modules that register states with ui-router would have to be reworked to do so at runtime (which is something that ui-router has no official mechanism for)

@btford
Copy link
Contributor Author

btford commented Mar 28, 2015

Right, but lazy loading was never officially supported. Backwards
compatibility with existing hacks is not one of my goals here. And again,
the changes to user code would be minimal.

I feel like this thread is going in circles. If you have a specific,
concrete case where it would be difficult for a third party library to be
lazy loaded, and where it cannot be easily adapted, feel free to share.
Otherwise please make sure that what you post hasn't already been
discussed. Thanks!

On Sat, Mar 28, 2015, 11:55 Chris Thielen notifications@github.com wrote:

The mere presence of a provider is not a problem. Also you would never
lazy-load UI Router. You'd always need it upfront.

I think his point is that ui-router states are declared by registering
them with the ui-router state provider, which obviously must happen in the
config block. Any lazy loaded user modules that register states with
ui-router would have to be reworked to do so at runtime.


Reply to this email directly or view it on GitHub
#11015 (comment)
.

@raul-arabaolaza
Copy link

Right, but lazy loading was never officially supported. Backwards
compatibility with existing hacks is not one of my goals here

Fair enough, no more complaints on my part then

@ocombe
Copy link
Contributor

ocombe commented Mar 29, 2015

you need the config block if the lazy loaded module defines a decorator, but I'm not sure if there is a way to do that

@geddski
Copy link
Contributor

geddski commented Apr 6, 2015

@btford do you have a current implementation of this I can start playing with? I've followed this up to this point.

@gautelo
Copy link

gautelo commented Apr 6, 2015

Eyes @btford
Eyes OP - Loading new code section and placeholder syntax
Eyes Counter proposal 1
😃

@andrezero
Copy link

I feel like this thread is going in circles.

@btford on the contrary, it has never been so clear.

If one takes the time to read it all.

I believe the doubts expressed by @raul-arabaolaza are very representative of the community anxiety towards AngularJS progress when it comes to maintaining large, complex systems.

Thank you both for the exercise.

@btford can you please improve your original post so that it incorporates the enlightened discussion?

Under "Run and config blocks":

  • that it does not throw if a module with config is lazy loaded (despite your comment it should throw if you try to add a module with config blocks)
  • Any library you lazily load that requires reconfiguration at run-time must be re-written just for those services that require configuration.
  • [constants] they'll still be useable post-config phase.

You might also rethink the // this example is silly, maybe present a more real world one.

@thelgevold
Copy link

Question about the new router in 1.4:
Why can't the lazy loading be convention based and follow the structure under the components folder?

Meaning, if you navigate using the router and activate a component, can't you just load the top level controller for the component at that point? Ideally you shouldn't have to reference the top level controller using a script tag. Upon instantiating the controller it would be ideal if all the downstream dependencies would be loaded and instantiated at this point as well (services, directives,etc). Again without the need for script tags in index.html. Given how Angular 1.x DI works I suppose this solution would require all downstream resources to be combined into a single file though. I was hoping the new router would function more like Durandal. The router config seems almost identical to Durandal routing, so it seem like it would be possible to support a more seamless RequireJS-esq DI here. It seems a bit odd to me to specify placeholders etc.

@Narretz
Copy link
Contributor

Narretz commented Sep 27, 2017

The option to load new modules into the injector will be part of 1.6.7. However, it's slightly different from the original proposal. Read more here: 34237f9

@Narretz Narretz closed this as completed Sep 27, 2017
@ocombe
Copy link
Contributor

ocombe commented Sep 27, 2017

Nice! It's funny to see that it doesn't take much code to do that when it's inside of the framework :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests