📜 ⬆️ ⬇️

Module loading on demand to AngularJS

If you are in a hurry: then yes, deferred loading of modules into AngularJS is possible, and you can see the code needed for this below.

Does AngularJS really not support deferred loading in any way?


AngularJS is one of the best templates for front end development, but it is still young, and does not have several important features (who said a good router?).
While most of these features can be added in the form of modules that can be found on google or on specialized web sites , there are some features that cannot be added in this way.
Currently, many require asynchronous loading of modules, and it seems Google is going to implement it in the second version of the framework, but who knows when it will be ...

I'm looking for a way to do this now, because I want to optimize my application and speed up the time it loads.
I found two very interesting articles (in English): deferred loading in AngularJS and loading components of AngularJS after starting the application using RequireJS .
But both of them explain the delayed loading of controllers, services, filters, directives, but not the delayed loading of modules.
I decided to study the angular source code for loading modules (you can see it here), and I noticed that the registration of modules occurs after initialization, but the new loaded modules and their dependencies simply do not connect to the application and are not initialized.

Wait, can't you do it yourself?


Well, yes, of course it can be done! All we need to do is make sure that the module we are adding has not been previously added to the application, since we have no desire to rewrite the already existing code. Imagine you need a service that has already been loaded and configured before, it will very quickly stop working as needed if you rewrite it.
So, we need a list of previously loaded modules, which should be easy to find, right?
Well ... yes ... actually ... no.
If you look at the source code, you will find there an internal variable called modules . This variable is used to store a list of all loaded modules, and it is not accessible from the outside.
')

Therefore, we can not get a list of modules?


No, we can not. But we can recreate it again.
We can use angular.module('moduleName') at any time to get an existing module. If you output its result to the log, you will notice the property: _invokeQueue . This is the list of its dependencies.
Since modules can only be loaded at startup and the application can only be started using the ng-app directive, if you can find the application module, you can get the entire list of loaded modules and their dependencies.
Translator's note: in fact, the application can be started without the ng-app directive, but in this case it does not matter.
You can do this with the following code:
 function init(element) { var elements = [element], appElement, module, names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; function append(elm) { return (elm && elements.push(elm)); } angular.forEach(names, function(name) { names[name] = true; append(document.getElementById(name)); name = name.replace(':', '\\:'); if(element.querySelectorAll) { angular.forEach(element.querySelectorAll('.' + name), append); angular.forEach(element.querySelectorAll('.' + name + '\\:'), append); angular.forEach(element.querySelectorAll('[' + name + ']'), append); } }); angular.forEach(elements, function(elm) { if(!appElement) { var className = ' ' + element.className + ' '; var match = NG_APP_CLASS_REGEXP.exec(className); if(match) { appElement = elm; module = (match[2] || '').replace(/\s+/g, ','); } else { angular.forEach(elm.attributes, function(attr) { if(!appElement && names[attr.name]) { appElement = elm; module = attr.value; } }); } } }); if(appElement) { (function addReg(module) { if(regModules.indexOf(module) === -1) { regModules.push(module); var mainModule = angular.module(module); angular.forEach(mainModule.requires, addReg); } })(module); } } 

Registration of new modules


Now, when we have a list of previously loaded modules, we can add new modules (only those that have not been loaded before).
To do this, we need to understand how modules and dependencies are invoked in angular.
As soon as a module is registered, all its dependencies are loaded (in the “initialization” phase), which are added to the module as a result of initialization. If any dependency already exists in memory, then the initialization phase will be skipped, and a link to the existing dependency will be added to the module.
A module can go through configuration and execution phases.
The configuration phase is performed before the dependencies are downloaded.
The execution phase begins only after the configuration phase and the complete loading of all the required dependencies. You can see the code for the execution phase in the _runBlocks parameter.
To start the execution phase, we will use the invoke function of the $injector service.
So, in the end, we will make a list of all dependencies, track the configuration and execution phases, and call them in the correct order.
In fact, we will reproduce the way in which angular loads its modules, which you can explore from source codes .
The result is shown in the following function:
 function register(providers, registerModules, $log) { var i, ii, k, invokeQueue, moduleName, moduleFn, invokeArgs, provider; if(registerModules) { var runBlocks = []; for(k = registerModules.length - 1; k >= 0; k--) { moduleName = registerModules[k]; regModules.push(moduleName); moduleFn = angular.module(moduleName); runBlocks = runBlocks.concat(moduleFn._runBlocks); try { for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { invokeArgs = invokeQueue[i]; if(providers.hasOwnProperty(invokeArgs[0])) { provider = providers[invokeArgs[0]]; } else { return $log.error("unsupported provider " + invokeArgs[0]); } provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } } catch(e) { if(e.message) { e.message += ' from ' + moduleName; } $log.error(e.message); throw e; } registerModules.pop(); } angular.forEach(runBlocks, function(fn) { providers.$injector.invoke(fn); }); } return null; } 

But be careful, when registering dependencies, we need to use the correct provider. We do not want to use the service provider when registering the directive.

Let's write our service


Each provider and injector are available in configuration and initialization phases. We need to keep a link to them if we want to use them later.
We also need to keep track of loaded modules in order to prevent them from re-loading.
I don’t want to write my own asynchronous loader, as there are quite a lot of them on the market (requireJS, script.js ...) and it would be stupid to reinvent your own bike, so you’ll have to figure this out yourself using the configuration.
You can use any bootloader that supports the following syntax:
 loader([urls], function callback() {}); 

This way you can load any resources that you need (only js files, css, etc.).
In this example, I will use script.js .
 var modules = {}, asyncLoader, providers = { $controllerProvider: $controllerProvider, $compileProvider: $compileProvider, $filterProvider: $filterProvider, $provide: $provide, // other things $injector: $injector }; 

This is a fairly easy part of the configuration, we need to define an asynchronous loader and a list of modules that we could load using a directive.
We also run the initialization script, which was previously defined, to fill in the list of modules for initial initialization.
 this.config = function(config) { if(typeof config.asyncLoader === 'undefined') { throw('You need to define an async loader such as requireJS or script.js'); } asyncLoader = config.asyncLoader; init(angular.element(window.document)); if(typeof config.modules !== 'undefined') { if(angular.isArray(config.modules)) { angular.forEach(config.modules, function(moduleConfig) { modules[moduleConfig.name] = moduleConfig; }); } else { modules[config.modules.name] = config.modules; } } }; 

To set up a provider, simply add this code to your application:
 angular.module('app').config(['$ocLazyLoadProvider', function($ocLazyLoadProvider) { $ocLazyLoadProvider.config({ modules: [ { name: 'TestModule', files: ['js/testModule.js'], template: 'partials/testLazyLoad.html' } ], asyncLoader: $script }); }]); 

As we write the provider, the only place where we can use component injection is the $get property. That that will return this property, and will be available in your service.
We will define getters and setters for configuring modules and a function for loading them.
We will also add a getter for the list of modules, since it must be available if someone wants to write a plugin.
Getters and setters are easily identified:
 getModuleConfig: function(name) { if(!modules[name]) { return null; } return modules[name]; }, setModuleConfig: function(module) { modules[module.name] = module; return module; }, getModules: function() { return regModules; } 

Now let's take a closer look at the download function. We need to implement the loading of modules by name or configuration.
The configuration object contains the name of the module, a list of files (scripts, css ...) and an optional template.
If we load a module using a directive, the template will be used to replace its code.
We will also maintain a list of module dependencies so that they can be registered.
The download function will return a promise, which will simplify further development.
 load: function(name, callback) { var self = this, config, moduleCache = [], deferred = $q.defer(); if(typeof name === 'string') { config = self.getModuleConfig(name); } else if(typeof name === 'object' && typeof name.name !== 'undefined') { config = self.setModuleConfig(name); name = name.name; } moduleCache.push = function(value) { if(this.indexOf(value) === -1) { Array.prototype.push.apply(this, arguments); } }; if(!config) { var errorText = 'Module "' + name + '" not configured'; $log.error(errorText); throw errorText; } } 

We need a function to get the dependencies of the module:
 function getRequires(module) { var requires = []; angular.forEach(module.requires, function(requireModule) { if(regModules.indexOf(requireModule) === -1) { requires.push(requireModule); } }); return requires; } 

We also need a function to check whether a specific module was previously loaded, in case we missed something from the moment of initialization to the present. There is no “clean” way to do this, so you have to use the “dirty” one:
 function moduleExists(moduleName) { try { angular.module(moduleName); } catch(e) { if(/No module/.test(e) || (e.message.indexOf('$injector:nomod') > -1)) { return false; } } return true; } 

Now we can write a function that will load the dependencies of the new module. It will immediately return control if the module has been loaded before or fill in the variable moduleCache to get a list of new modules and their dependencies for registration.
 function loadDependencies(moduleName, allDependencyLoad) { if(regModules.indexOf(moduleName) > -1) { return allDependencyLoad(); } var loadedModule = angular.module(moduleName), requires = getRequires(loadedModule); function onModuleLoad(moduleLoaded) { if(moduleLoaded) { var index = requires.indexOf(moduleLoaded); if(index > -1) { requires.splice(index, 1); } } if(requires.length === 0) { $timeout(function() { allDependencyLoad(moduleName); }); } } var requireNeeded = getRequires(loadedModule); angular.forEach(requireNeeded, function(requireModule) { moduleCache.push(requireModule); if(moduleExists(requireModule)) { return onModuleLoad(requireModule); } var requireModuleConfig = self.getConfig(requireModule); if(requireModuleConfig && (typeof requireModuleConfig.files !== 'undefined')) { asyncLoader(requireModuleConfig.files, function() { loadDependencies(requireModule, function requireModuleLoaded(name) { onModuleLoad(name); }); }); } else { $log.warn('module "' + requireModule + "' not loaded and not configured"); onModuleLoad(requireModule); } return null; }); if(requireNeeded.length === 0) { onModuleLoad(); } return null; } 

In the end, we need to call an asynchronous loader that loads the dependencies and registers them.
 asyncLoader(config.files, function() { moduleCache.push(name); loadDependencies(name, function() { register(providers, moduleCache, $log); $timeout(function() { deferred.resolve(config); }); }); }); 

We made it, now it is possible to load modules on demand !!!
 $ocLazyLoad.load({ name: 'TestModule', files: ['js/testModule.js'] }).then(function() { console.log('done!'); }); 

Use directive


We should be able to load the module through a directive. To do this, we will use the template parameter that we mentioned earlier. This template will replace the directive.
We will use the $templateCache service to prevent the loading of templates that already exist in the cache of our application.
The directive will be called as follows:
 <div oc-lazy-load="{name: 'TestModule', files: ['js/testModule.js'], template: 'partials/testLazyLoad.html'}"></div> 

If we have defined the configuration of the TestModule module in the provider settings, we can call our directive as follows:
 <div oc-lazy-load="'TestModule'"></div> 

Writing a directive is not the purpose of this article, so I’ll skip its description. An important part of the directive is to download a new template by its url, or from the cache, if it was loaded earlier:
 ocLazyLoad.directive('ocLazyLoad', ['$http', '$log', '$ocLazyLoad', '$compile', '$timeout', '$templateCache', function($http, $log, $ocLazyLoad, $compile, $timeout, $templateCache) { return { link: function(scope, element, attr) { var childScope; /** * Destroy the current scope of this element and empty the html */ function clearContent() { if(childScope) { childScope.$destroy(); childScope = null; } element.html(''); } /** * Load a template from cache or url * @param url * @param callback */ function loadTemplate(url, callback) { scope.$apply(function() { var view; if(typeof(view = $templateCache.get(url)) !== 'undefined') { scope.$evalAsync(function() { callback(view); }); } else { $http.get(url) .success(function(data) { $templateCache.put('view:' + url, data); scope.$evalAsync(function() { callback(data); }); }) .error(function(data) { $log.error('Error load template "' + url + "': " + data); }); } }); } scope.$watch(attr.ocLazyLoad, function(moduleName) { if(moduleName) { $ocLazyLoad.load(moduleName).then(function(moduleConfig) { if(!moduleConfig.template) { return; } loadTemplate(moduleConfig.template, function(template) { childScope = scope.$new(); element.html(template); var content = element.contents(); var linkFn = $compile(content); $timeout(function() { linkFn(childScope); }); }); }); } else { clearContent(); } }); } }; }]); 

Integration of our service with ui-router


Delayed loading of modules usually occurs when you load a new route. Let's see how you can do it with ui-router (but it will work with ng-route ).
Since we can load our module using a service or a directive, we can use two options: use the resolve object or use a template.
Using the service requires the use of the resolve object. The resolve object allows you to define some parameters for your route, and is called before loading the template. This is important, the template can use a controller that performs deferred loading.
Each parameter to the resolve function allows the promise to determine how it should be allowed. Since our loading function returns a promise, we can simply use it. Here the views part is mandatory, this is just for this example.
 $stateProvider.state('index', { url: "/", // root route views: { "lazyLoadView": { templateUrl: 'partials/testLazyLoad.html' } }, resolve: { test: ['$ocLazyLoad', function($ocLazyLoad) { return $ocLazyLoad.load({ name: 'TestModule', files: ['js/testModule.js'] }); }] } }); 

Using the directive is also simple:
 $stateProvider.state('index', { url: "/", views: { "lazyLoadView": { template: '<div oc-lazy-load="{name: \'TestModule\', files: [\'js/testModule.js\'], template: \'partials/testLazyLoad.html\'}"></div>' } } }); 

I think this is a bit less optimal than using the function resolve , since we added a complex layer, but this can be very useful in some cases.
Here we are done. I hope you find this delayed bootloader useful!
Fully working example you can look at Plunkr .
You can also look at all the code and an example on github .
I used this angular module as a base for my project, but I greatly improved it by adding new features that I needed.

Source: https://habr.com/ru/post/208164/


All Articles