
On-demand module loading in AngularJS
- Transfer
If you are in a hurry: yes, delayed loading of modules is
AngularJS is one of the best templates for front end development, but it is still young and lacks 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 websites , there are some features that cannot be added in this way.
Currently, many people need 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 it now because I want to optimize my application and speed up its loading time.
I found two very interesting articles (in English):deferred loading in AngularJS and loading AngularJS components 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 dedicated to loading modules (you can see it here), and I noticed that the registration of modules occurs after initialization, but new loaded modules and their dependencies simply do not connect to the application and do not initialize.
Well yes, of course it can be done! All we need to do is make sure that the module we added was not previously added to the application, since we don’t want to rewrite the existing code. Imagine that you need a service that has already been downloaded and configured earlier, it will very quickly stop working as it should 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 into the source code, you will find there an internal variable with a name
No, we can’t. But we can recreate it again.
We can use
Since the modules can only be loaded at the start and the application can only be launched using the directive
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:
Now that 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 called in angular.
As soon as the 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, the initialization phase will be skipped and a link to the existing dependency will be added to the module.
A module can go through the configuration and execution phases.
The configuration phase is performed before loading the dependencies.
The execution phase begins only after the configuration phase and the full load of all the required dependencies. You can see the execution phase code in the parameter
To start the execution phase, we will use the invoke function of the service
So, in the end, we will make a list of all the dependencies, we will track the configuration and execution phases, and call them in the correct order.
In fact, we will reproduce the way angular loads its modules, which you can learn from the source code .
The result is shown in the following function:
But be careful, when registering a dependency, we need to use the correct provider. We do not want to use the service provider when registering the directive.
Each provider and injector is available in the configuration and initialization phases. We need to keep a link to them if we want to use them later.
We also need to track loaded modules in order to prevent them from reloading.
I don’t want to write my own asynchronous bootloader, since there are quite a lot of them on the market (requireJS, script.js ...) and it would be foolish to invent your own bike, so you have to determine this yourself using the configuration.
You can use any bootloader that supports the following syntax:
In this way, you can load any resources that you require (only js files, css, etc.).
In this example, I will use script.js .
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 the directive.
We also run the initialization script, which was defined earlier, to populate the list of modules for initial initialization.
To configure the provider, simply add this code to your application:
Since we are writing a provider, the only place we can use component injection is this property
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, as it should be available if anyone wants to write a plugin.
Getters and setters are easily defined:
Now let's take a close look at the download function. We need to implement loading modules by name or by configuration.
The configuration object contains the module name, a list of files (scripts, css ...) and an optional template.
If we load the module using the 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 load function will return a promise, which will simplify further development.
We need a function to get the dependencies of the module:
We also need a function to check whether a certain module has been previously loaded, in case we missed something from the moment of initialization to the present moment. There is no “clean” way to do this, so you have to use the “dirty” one:
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 previously, or fill out a variable
In the end, we need to call the asynchronous loader, which will load the dependencies and register them.
We did this, now you can load the modules on demand !!!
We should be able to load the module through the directive. To do this, we will use the parameter
We will use the service
The directive will be invoked in the following way:
If we defined the module configuration
Writing a directive is not the purpose of this article, so I will skip its description. An important part of the directive is to load a new template by its url, or from the cache if it was loaded earlier:
Delayed loading of modules usually occurs when you load a new route. Let's see how you can do this with ui-router (but this will work with
Since we can load our module using a service or directive, we can use two options: use an object
Using a service requires using an object
Each function parameter
Using the directive is also simple:
I think this is slightly less optimal than using the function
So we have done everything. I hope you find this delayed bootloader useful!
A fully working example you can take a look at Plunkr .
You can also look at all the code and the example on github .
I used this angular module as the base for my project, but I greatly improved it by adding new features that I needed.
AngularJS
possible, and you can see the code necessary for this below.Does AngularJS not support lazy loading in any way?
AngularJS is one of the best templates for front end development, but it is still young and lacks 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 websites , there are some features that cannot be added in this way.
Currently, many people need 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 it now because I want to optimize my application and speed up its loading time.
I found two very interesting articles (in English):deferred loading in AngularJS and loading AngularJS components 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 dedicated to loading modules (you can see it here), and I noticed that the registration of modules occurs after initialization, but new loaded modules and their dependencies simply do not connect to the application and do not initialize.
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 added was not previously added to the application, since we don’t want to rewrite the existing code. Imagine that you need a service that has already been downloaded and configured earlier, it will very quickly stop working as it should 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 into the source code, you will find there an internal variable with a name
modules
. This variable is used to store a list of all loaded modules, and it is not accessible from outside.So we can’t get a list of modules?
No, we can’t. But we can recreate it again.
We can use
angular.module('moduleName')
at any time to get an existing module. If you print the result in the log, you will see the property: _invokeQueue
. This is a list of its dependencies. Since the modules can only be loaded at the start and the application can only be launched using the directive
ng-app
, 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 that 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 called in angular.
As soon as the 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, the initialization phase will be skipped and a link to the existing dependency will be added to the module.
A module can go through the configuration and execution phases.
The configuration phase is performed before loading the dependencies.
The execution phase begins only after the configuration phase and the full load of all the required dependencies. You can see the execution phase code in the parameter
_runBlocks
. To start the execution phase, we will use the invoke function of the service
$injector
. So, in the end, we will make a list of all the dependencies, we will track the configuration and execution phases, and call them in the correct order.
In fact, we will reproduce the way angular loads its modules, which you can learn from the source code .
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 a dependency, 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 is available in the configuration and initialization phases. We need to keep a link to them if we want to use them later.
We also need to track loaded modules in order to prevent them from reloading.
I don’t want to write my own asynchronous bootloader, since there are quite a lot of them on the market (requireJS, script.js ...) and it would be foolish to invent your own bike, so you have to determine this yourself using the configuration.
You can use any bootloader that supports the following syntax:
loader([urls], function callback() {});
In this way, you can load any resources that you require (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 the directive.
We also run the initialization script, which was defined earlier, to populate 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 configure the 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
});
}]);
Since we are writing a provider, the only place we can use component injection is this property
$get
. What 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, as it should be available if anyone wants to write a plugin.
Getters and setters are easily defined:
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 close look at the download function. We need to implement loading modules by name or by configuration.
The configuration object contains the module name, a list of files (scripts, css ...) and an optional template.
If we load the module using the 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 load 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 certain module has been previously loaded, in case we missed something from the moment of initialization to the present moment. 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 previously, or fill out a 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 the asynchronous loader, which will load the dependencies and register them.
asyncLoader(config.files, function() {
moduleCache.push(name);
loadDependencies(name, function() {
register(providers, moduleCache, $log);
$timeout(function() {
deferred.resolve(config);
});
});
});
We did this, now you can load the modules on demand !!!
$ocLazyLoad.load({
name: 'TestModule',
files: ['js/testModule.js']
}).then(function() {
console.log('done!');
});
Using the directive
We should be able to load the module through the directive. To do this, we will use the parameter
template
that we mentioned earlier. This template will replace the directive. We will use the service
$templateCache
to prevent the loading of templates that already exist in the cache of our application. The directive will be invoked in the following way:
If we defined the module configuration
TestModule
in the provider settings, we can call our directive as follows:
Writing a directive is not the purpose of this article, so I will skip its description. An important part of the directive is to load 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 this with ui-router (but this will work with
ng-route
). Since we can load our module using a service or directive, we can use two options: use an object
resolve
or use a template. Using a service requires using an object
resolve
. The object resolve
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 function parameter
resolve
enables promise to determine how it should be resolved. Since our load function returns a promise, we can just use it. Here the part views
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: ''
}
}
});
I think this is slightly less optimal than using the function
resolve
, since we added a complex layer, but it can be very useful in some cases. So we have done everything. I hope you find this delayed bootloader useful!
A fully working example you can take a look at Plunkr .
You can also look at all the code and the example on github .
I used this angular module as the base for my project, but I greatly improved it by adding new features that I needed.