
AngularJs is a great web application development framework. The development of the business logic of the application is completely separated from the attendant fuss around the DOM. Angular modular is great, but is also the source of the problem. The number of modules is growing rapidly. And if directives can still be packaged in separate packages of the angular-ui type, then with business logic controllers it is more and more difficult. Things get even worse when security requirements basically prohibit loading business controllers onto a client that are not available to the current user. With an advanced role-based access system to the application, the scale of the problem becomes obvious.
In Angular, in principle, there is no system for loading modules on demand. Nevertheless, you can independently develop such a module, which will load the javascript file. But there is a problem. When calling the function angular.module with which any Angular module starts, it does not add functionality to the internal structures of Angular. And this was done intentionally, so that you can specify script tags in a random order without observing the dependencies between the modules. The final loading of the modules will be done after the html-document is fully loaded. Actually, this is the function of the angular.bootstrap, which creates an instance of injector and initializes the internal structures of the framework.
So, the problem arises:
- Provide module loading using a directive. This will give the opportunity to load the module exactly when it is really needed.
- Provide dependency resolution. Those. if the module has dependencies, check if they are all satisfied. And if not, then initiate the procedure for loading modules that satisfy the dependency.
- The directive must also ensure the loading of the specified template, since directives in the template may have a dependency on the loadable module (for example, an indication of the controller) and the module must be loaded earlier, and only then the template is applied.
- And, of course, compiling and linking the loaded template.
Let's get started
An example of a directive, the appearance of which in the code will initiate the loading of the home module:
<div load-on-demand="'home'"></div>
In addition to the load-on-demand directive itself, there is the name of the loadable module. This option is selected for greater flexibility in configuring loadable modules. Configuration is usually done by calling the module.config function.
Example of function call:
var app = angular.module('app', ['loadOnDemand']); app.config(['$loadOnDemandProvider', function ($loadOnDemandProvider) { var modules = [ { name: 'home', script: 'js/home.js' } ]; $loadOnDemandProvider.config(modules, []); }]);
We now turn directly to the directive. In our case, we do not need to fine-tune the directive, so we return only the linking function (linkFunction), which does everything necessary. Pseudo-code that demonstrates the algorithm:
var aModule = angular.module('loadOnDemand', []); aModule.directive('loadOnDemand', ['$loadOnDemand', '$compile', function ($loadOnDemand, $compile) { return { link: function (scope, element, attr) { var moduleName = scope.$eval(attr.loadOnDemand);
')
The key point here is the call to the $ loadOnDemand.load () function. All the functionality for configuring and loading the script is in the $ loadOnDemand provider. Open it up. I deliberately hide implementation details so as not to clutter up the code.
aModule.provider('$loadOnDemand', function(){ this.$get = [function(){
Each provider must provide a $ get function that should return a service object. This service will be used by the inspector when it is required. In addition to the $ get function, our privyder provides the config function — it is used to configure the module loader (app.config above). The fact is that the module.config function provides only providers, so it is necessary to separate the provider configuration logic from the service it provides.
The service itself has two functions: getConfig - used for ease of obtaining a configuration object and, in fact, the main function of the service is load, which loads the module. Low-level script loading is done using document.createScript — such loading is more user-friendly for the IDE debugger.
And water would be all that needs to be done. But, this will not work. The reason is stated above - after the script is loaded and executed, the module's functionality will not be placed in the angular infrastructure. So, immersed in angular.bootstrap.
After the DOM is loaded, the angular initialization procedure starts. She is looking for a directive ng-app with the name of the main module of the application. After that, an instructor is created and the DOM is compiled into an angular template. In this chain, we are most interested in the creation of the vector, since it is this call that starts the module loading procedure - the loadModules function. loadModules gets a Module object in which there is a queue of commands for the vector - _invokeQueue. This queue is created by calling angular.module. Each element of this queue is given to the corresponding provider, which does all the work of adding functionality.
We just need to repeat this algorithm using existing providers. We get them using the inspector.
aModule.provider('$loadOnDemand', ['$controllerProvider', '$provide', '$compileProvider', '$filterProvider', function ($controllerProvider, $provide, $compileProvider, $filterProvider) { . . . loadScript(moduleName, function(){ register(moduleName); }); . . . }]);
Registration function of the register module.
moduleFn = angular.module(moduleName); for (invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { invokeArgs = invokeQueue[i]; provider = providers[invokeArgs[0]]; provider[invokeArgs[1]].apply(provider, invokeArgs[2]); }
In invokeArgs [0] is the name of the provider, invokeArgs [1] is its method of registering a new service. invokeArgs [2] - parameters that are passed to the registration method (list of injections and the function constructor of the service).
That's probably all, it remains only to load the dependencies that are in moduleFn.requires as a simple array of module names. After connecting such a module to your project, the main page will look something like this:
<!DOCTYPE html> <html ng-app="app"> <head> </head> <body> <div ng-view></div> <script src="js/angular.js"></script> <script src="js/loadOnDemand.js"></script> </body> </html>
And the main module of the application, something like this:
(function(){ var app = angular.module('app', ['loadOnDemand']); app.config(['$routeProvider', function ($routeProvider) { . . . }; app.config(['$loadOnDemandProvider', function ($loadOnDemandProvider) { . . . }; })();
The project is on
github with a demo