Description
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. As ui.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: :-)
- https://github.com/afterglowtech/angular-detour
- https://github.com/afterglowtech/angular-couchPotato
- https://gist.github.com/nateabele/5610305
API / Syntax
State definitions can supply a depends
key, that lists the modules that they (or their child states) depend on, i.e.:
/* ... */
.state("contacts.item", {
url: "/:id",
views: {
main: {
templateUrl: "contacts/item.html",
controller: function($scope, $stateParams, $resource, editor) {
/* ... */
}
}
},
depends: ['ngResource', 'customEditor']
})
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:
/* ... */
.run(function($state, $q) {
$state.dependencyLoaders.push(function(depends) {
var notify = $q.defer();
var toPaths = moduleSpecificMethodToConvertDependsToPaths;
var paths = toPaths(depends);
if (!paths || !paths.length) {
// This module doesn't know anything about any of these dependencies
return;
}
// Example: use require.js to load dependencies:
require(toPaths(depends), function() {
notify.resolve();
});
return notify.promise;
});
})
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 the resolve
for the state transition itself.
Advantages
- Makes it easier to divide sections of an app into separate modules
- Logical sections and sub-sections of apps can be self-contained, as states associated with a section/module can identify & communicate their own dependencies, as well as how to load them
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:
- Valid handler return values may be annotated in some way with the modules being loaded, such that those modules are not passed to other handlers
- Evaluate loaded scripts inside a try/catch block
- Document it as a user-land responsibility
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.