-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Lazy-loading modules #146
Comments
Thanks, @nateabele! We're going to need to be careful about the word module. I already got confused in reading Nate's post. I think I was thinking he meant angular modules when he really meant AMD modules... but I'm still not sure. Just to be clear -- loading angular modules is the one thing that couchPotato doesn't do (yet)... rather it enables loading and registering components (services/controllers/directives/filters) using AMD modules, which can therefore be done in the context of resolving promises during transition to a state. My next task was to look at how you load angular modules in your gist (that is what it does, I hope), and to try incorporate that, as well. For the rest of this post, I'm referring to AMD modules, unless otherwise indicated. couchPotato currently works with the stock router, and with ui-router by means of a "dummy" property within the resolve property of the state config. detour (which is the one based on ui-router) integrates and wraps couchPotato, providing a "dependencies" property that is used to invoke couchPotato for download and registration. I'll be pushing the detour code that integrates couchPotato later today. However, the real purpose (as yet unrealized) of detour is about lazy-loading and editing routes themselves, which I consider to be a whole different animal (even though it begs support for the lazy loading and registration of the components on which the states depend). I'm pretty sure that that is not in the scope of this issue, so we can probably steer clear of thinking about it too much in this context. Back to lazy-loading angular components/angular modules for a state: I guess the second approach with having registered handlers sounds like a nice abstraction -- if that means that someone can choose to use couchPotato or some other method as long as it ultimately deliveres a promise. I would think whichever direction is taken, you'd want the references (URL paths in AMD) to the set of components upon which a state relies to remain with the definition of that state -- right? In other words, you wouldn't want to bury the list of dependencies for state XYZ in a monolithic application-wide run function. Could you explain your concerns about multiple simultaneous loading from multiple handlers? Since we're single-threaded, I figured that at least with AMD we know that the first call to a given AMD module will be the one that runs the module, and the rest just get it's result.... but I suppose if you actually use two different methods to register components, you could be in a bit of a hurt. On another note, I was thinking this morning about how it might be possible to avoid leaning on AMD. If anyone has ideas, I'd be excited to discuss that. Right now, what it brings are (a) just-in-time code download and (b) easily avoids re-download/re-registration and (c) path-based pointers to the code. Also, it has no trouble working with sources from different domains. For what it's worth, couchPotato is just a formalization of and extension of a bunch of other examples out there. Functionally the only thing it adds is that it allows/requires the AMD modules to do the registering of the components they define, thereby allowing for dependencies of dependencies to automatically be downloaded/registered without having to express the entire set in the state definition, as fits with the AMD paradigm. |
Hey, sorry for not being clear. When I said 'modules', I did, in fact, mean Angular modules. Angular's module implementation provides an adequate encapsulation mechanism, so unless I'm missing something here (entirely possible), I don't think we need to care about the AMD paradigm. For purposes of what I had in mind, all we really need to care about is mapping a module name to one or more script URLs (i.e. if a module is split across multiple files, or if a module depends on another file that's just generic JavaScript code). Angular itself, combined with simple module -> URL(s) hash maps should provide all the benefits and flexibility of AMD.
I think this will work itself out naturally once we figure out a state traversal / manipulation API.
Personally, I think that states should only list their module dependencies, not the URLs for those dependencies. The point of
If you have two separate handlers from two separate modules, both of which depend on the same (third) module, it's conceivable that they could attempt to load the same module at the same time, since the promises would be loading in parallel. Strictly speaking, yes, these things happen synchronously, but loading the same script twice would attempt to register the same module twice, which would cause Angular to throw an error. |
So if we're talking specifically about modules and not individual angular components, then neither couchPotato nor detour do anything about that, yet, and many of my comments above should be read with the assumption that some of what you were discussing was in part about AMD modules. I do want to do lazy angular module loading, but that's not in couchPotato or detour as of now.
AFAIK, if you use AMD you don't need to worry about this as the second caller will get the result of the AMD-definition-module without running it. I'm sure the same functionality could be implemented without AMD, too, and I guess that was your point. I think its worth separating some different but overlapping goals that one might have: lazy loading: essentially, you don't want to incur network traffic or processor time/RAM usage until you need to. Big sites/apps can benefit from this. loose coupling: you don't want one part of your site/app to require intimate knowledge or intricate dependencies on others where not strictly necessary. Sites/apps that are managed by larger teams, and sites/apps built from pieces developed by different teams can benefit from this, and modularization of site/app architecture can have benefits in use cases such as a CMS framework where "components" (using the term loosely here) should be pluggable. dependency management: you want to specify dependencies where they are needed to help you keep your code understandable and manageable, including cases where different teams are responsible for different pieces. For example, couchPotato doesn't do too much for you on its own with loose coupling. Whatever dependencies you load/register need to work together and with the specified templates and in the part of the app where they are invoked. But it does provide the lazy benefit, and it allows the dependencies to be referenced where they are needed (in the definition of the states that they are needed). For dependency management, it does provide that the path is specified in the definition of the state, therefore it doesn't require any master URL map to be updated. I suppose that expressing dependencies between modules (each of which is composed of a single component) is similar to expressing them with AMD. But I'm trying to wrap my head around where you would specify the URLs for everything. I have a feeling I'm missing a piece of the puzzle here. Is one of your goals to specifically avoid AMD? Please correct me if I'm wrong... It sounds like you're thinking about a replacement for AMD that lives in angular-land such that you can specify the URLs somewhere near the top of the food chain, and thus not worry about them when specifying the actual dependencies within the application -- in other words, you'd use names of components/modules in the app code, and have a centralized store mapping the URLs to those components/modules. If that is so, it worries me from a loose-coupling standpoint. If a large organization has one group (call it Technical Services) that owns a big web site's/web app's overall config, does the Technical Services group need to modify a master list in order for a smaller group in the organization (call it Widget Sales) to use a little component in one of the states/routes/components that they own? Don't get me wrong -- I'd be happy not to "require" requirejs... we may just be thinking of different use cases with respect to dependency management? I'm sure we could build something that serves "centralized URL map use casees" and "decentralized" versions as well. |
one more thing I neglected to mention... When I think of loose coupling, I imagine widgets being loaded in a container and the container not caring about the specificities of the widgets (just that they support the widget interface) and the widgets not knowing anything at all about the container (just that it supports the container interface). Loose coupling is the goal that seems truly challenging -- the dependency management stuff and lazy loading stuff should really just be a matter of making some decisions and ensuring sufficient flexibility for things like having both centralized and decentralized dependency management. |
Sorry -- forgot to actually make my key point. -- that I want to recognize |
In terms of how $state interacts with this, wouldn't it be enough to be able to use
I.e. the controller simply gets loaded as part of the normal resolve process; all $state cares about is that it's there when resolve finishes. In fact once $resolver is factored out into it's own service, your lazy loading solution should simply be able to decorate $resolver to be able to do fancy stuff like allowing services to be lazy loaded as well. Just as a wild idea, you when you ask for a service 'foo.bar' it could ensure module 'foo' is loaded, create a private $injector for the module that inherits from the app injector, and then resolve 'bar' within that injector, and finally return the service just like plain old $resolver would have done. |
Are there any subtle tradeoffs between registering controllers and referencing them by registered name vs. assigning them directly to a controller property? I can't think of anything serious as long as the lazy loader can give the already-loaded copy of FooController the next time it's called for. I gravitated to the pattern where the routers don't need to care what the dependencies are. In the case of ui-router, putting the loader in the resolve list with a "dummy" property gets the job done because the dependencies register themselves in the module. This works today, but has no concept of modules. So it's lazy and supports dependency management within the main module only (unless the AMD module definition were to load code from another module). I'm kind of behind on that portion of the conversation because I still haven't had time to look at Nate's runtime module dependency loader. Back on the subject of where to define URLs... it would be nice to come up with something hierarchical that starts in a local place (perhaps the state definition itself) and then gradually looks upward toward a root in order to resolve references to modules and components.... something like, perhaps, the state objects themselves (with the final stop being the root state/state service)? I think that would be a pretty decent way to allow for both centralized and decentralized mapping (and of course the path up the states already exists). I suppose one could make the case that this would clutter the router with non-router issues, but if you consider the router to be the place where you define what's going on for a given app URL (or state), then where else would it really belong? If a local state needs a different version of a component, it need only override the URL that it would naturally have gotten from its ancestor. |
somewhat related -- https://github.com/afterglowtech/angular-detour now supports configuration through json as well as allowing for services to be specified by name to resolve locals and to act as template providers. Specifying named services is necessary because javascript functions can't travel in json. I had to move the lazy registration of dependencies to a higher level in the transitionTo function in order to get them registered prior to invoking the templateService (the parameter used to specify a service that returns a template). So, for example, the detail state from the sample looks like this in json (there's an abbreviated syntax so that a real server-sent json string would be much shorter):
Looking at this now, "resolveServices" is misleading and needs to be renamed. "resolveByService" is more true, but that doesn't sound very good. :) |
Also check out Future States: http://christopherthielen.github.io/ui-router-extras. I'm closing this because at this time people can use these third party options. |
Interesting... I wasn't aware of all this work that had been done on lazy loading states. @stu-salsbury are you still using couch potato or detour? |
What about make the template and controller accept functions that returning promises? like: .state("contacts.item", {
url: "/:id",
views: {
main: {
template: function() { return System.import('some.html'); },
controller: function() { return System.import('some.js'); }
}
}
}) |
I believe promises are already supported by |
@nateabele thanks for pointing me out |
@nateabele I can't make that work,
got this error : Argument 'fn' is not a function, got Promise |
Ah, you're right, I forgot... |
@nateabele With some help with "resolve", I actually made it work:
a little ugly |
I'm struggling with how to lazy loading directives now, any idea? |
- Retry URL sync by matching state*.url - Make registration of new states (by the lazyload hook) optional - Reuse lazy load promise, if one is in progress - Refactor so ng1-to-ng2 UIRouter provider is implicit - When state has a .lazyLoad, decorate state.name with `.**` wildcard Closes #146 Closes #2739
I'm opening this as a place to continue discussion around lazy-loading of modules from different states (continued from #123).
Rationale
The
ui.state
module provides an architecture within which it is possible to cleanly assemble very large applications. Asui.state
allows users to navigate whole applications without page reloads, it is necessary to be efficient about script loading.Resources
@stu-salsbury has done a decent bit of work to enable lazy module loading, loosely based on
ui.state
, and I wrote a stupid gist that we can probably throw out: :-)API / Syntax
State definitions can supply a
depends
key, that lists the modules that they (or their child states) depend on, i.e.:Because module management is not within the domain of
ui.state
, it is important not to go too far down the path of explicitly handling it. Instead, we can provide hooks for users to do it themselves. For example:Here, modules could register handlers with
$state
($stateProvider
would work, too, if we made the handlers injectable and had them return a function, similar to$httpProvider.responseInterceptors
), and when a state is transitioned to, those handlers receive an array of the combined list of modules required by a state (i.e. that state and any parents) that are not currently defined.A handler that "knows about" one or more modules in the list can return a promise that resolves when all the modules that handler can load are loaded. The combined list of promises can then be thrown into
$q.all()
, the resolution of which triggers theresolve
for the state transition itself.Advantages
Caveats & Alternatives
One potential caveat of this approach is that multiple handlers may attempt to load the same module at the same time. Possible workarounds:
Alternatively, it is possible that the whole concept of module-loading is simply too far outside the domain of state management, such that it should be handled entirely by a separate module.
If that's the case, we should come up with a system of interceptors/decorators sufficient to allow a third-party module to easily intercept and defer state transitions while module loading takes place, as well as a way to get the current state and traverse upwards from it, in order gather all necessary dependencies.
The text was updated successfully, but these errors were encountered: