Introduction
There are many articles on how to implement AngularJS. After developing a web application using AngularJS, it is a common need for the application to respond faster. Lazy Loading is one such method.
The main objective of this article is to implement lazy loading [Load dependency modules and files (css or js) only when we need it] in our application. This article will a give basic idea of how to use RequireJS and ocLazyLoad.
The reader is assumed to have basic knowledge on AngularJS. This article will only help you implement lazyloading.
Background
AngularJS is one of the right choices to implement Single page application. When we start developing an application, we put all the css and js files in the index.html page and load the application.
This is an easy and good way to start with. But our application may not stay small for long. This will grow as time passes (scalability).
Copying all the files in the index.html page will take more time for landing page itself. So when the user comes to our page, she/he may get tired of waiting and may leave without even seeing our landing page :-(.
It is known that AngularJS developer team is in the process of developing a native lazy load implementation.
But even now, this can be achieved by using the following two plugins:
RequireJS vs OcLazyLoad
When I start to implementing Lazy Loading, I had a great confusion on which one to choose, whether Requirejs or OcLazyLoad or both.
After viewing more videos and text tutorials, I've concluded that we should walk by holding both their hands. This was the right idea.
Reason
RequireJS
- RequireJS is mainly used to load the dependency js files. We can call it a "good organiser".
- We can't load the CSS files (We can but we can't load as we do as Js files. Need to do some trick)
- We can't inject the AngularJS modules on the fly. This loads files only when needed.
OcLazyLoad
- OcLazyLoad is used to load the dependency files and also to inject the AngularJS modules on the fly.
- Loading Js and Css files are effortless.
- We can't maintain the dependency files structure.
From the above points:
I have had a great confusion here. Implementing and learning is very easy in OcLazyLoad and this is doing the same thing as RequireJs (Loading dependency files) and even inject angular module. Then, why should I choose RequireJs too? The answer is to maintain the dependency files and reuse the code.
Implementation
RequireJS
Using RequireJS, we define a single entry point on our Index.html page. In RequireJS, we maintain the codes as modules which helps them have a single reponsibility.
Now we need to configure our dependency file structure. This will help us to load a file and their dependencies. The main thing is after configure the dependency, the worries to remember the dependencies for all the file will be vanished.
For example,
Consider any of the AngularJS plugin which will depend on the angular.js. Here we will have the confusion. For example, Login page is our landing page. When we come to the landing page, the angular.js file will be loaded. Then assume we are going to dashboard page. So here we no need to load the angular.js file again, since we have already loaded. But if we refresh the browser in dashboard page(all the script files will be vanished), the files related to the dashboard page only will be loaded. For all those files, the dependency is angular.js file. Then now what will be our destination? angular.js, right?
This is the simple example. So here we may think what is the big effort we did for this? :-) Obviously not (Only here). We can satisfy with this simple application but our client won't. While keep on loading or if we depend on many AngularJs plugin, then there is lot of chance to forget the dependency chain.
Now we will come to the implementation. The data-main attribute on our single script tag tells Require.js to load the script mentioned in data-main path.
<script src="scripts/require.js" data-main="app/main.js"></script>
Here main.js is our configuration file. This is not necessary that the file name should be main.js. After loading the main.js, RequireJS is automatically depends on the js file. When mentioning the file name, we no need to mention the file extension.
The following is the main.js file structure.
- urlArgs - This is to mention the versioning
- paths - This is the place where we need to mention the file path
- shim - Shim is the one where we need to mention the dependencies
- deps - This will kick start our application
- require - This will load any other script we need to run. This will load the necessary file before our main app load.
require.config({
urlArgs: "bust=" + (new Date()).getTime(),
paths: {
'jquery': '../../scripts/jquery-1.9.1.min',
'angular': '../../scripts/angular.min',
'angular_aria': '../../scripts/angular-aria',
'angular_route': '../../scripts/angular-route.min',
'angular_cookie': '../../scripts/angular-cookies',
'angular_translate': '../../scripts/angular-translate/AngularTranslate',
'angular_translate_cookie': '../../scripts/angular-translate-storage-cookie.min',
'angular_animate': '../../scripts/angular-animate.min',
'angular-local-storage': '../../scripts/angular-local-storage',
'angular_ui_bootstrap': '../../scripts/angular-ui/ui-bootstrap-tpls.min',
'angular_ocLazyLoad': '../../scripts/ocLazyLoad/ocLazyLoad',
'HomeController': 'home/homeController',
'LandingModule': 'Landing/landingController',
'indexController': 'indexController',
'Page1Controller': 'Page1/page1Controller',
'Page2Controller': 'Page2/page2Controller',
'Page2Service': 'Page2/page2Service'
},
shim: {
'angular': {
exports: 'angular'
},
'jquery': {
exports: "$"
},
'angular_ocLazyLoad': {
deps: ['angular']
},
'angular_route': {
deps: ['angular']
},
'angular_cookie': {
deps: ['angular']
},
'angular_ui_bootstrap': {
deps: ['angular']
},
'app': {
deps:['angular_route', 'jquery', 'angular_ocLazyLoad', 'angular_ui_bootstrap']
},
'HomeController': {
deps: ['app']
},
'Page1Controller': {
deps: ['app']
},
'Page2Controller': {
deps: ['Page1Controller', 'Page2Service']
},
'indexController': {
deps: ['app']
},
},
deps:['app']
});
require(['indexController'], function () {
angular.bootstrap(document, ['SampleApp']);
});
Here important thing is requirejs will look all the files inside the app directory. For example, when we try to load homecontroller file then based on the path, the RequireJS will load like App/home/homecontroller.js. If we want to change config path, then we need to use the settings. After setting this config RequireJS will look into the following path. That is the RequireJS assume the homecontroller.js file is residing in Project/home/homecontroller.js or we can use ../../ to bypass them.
baseUrl:'Project'
- Here in this initial app kick start, we need to set our main app. (i.e) we need to load inject the necessary module to our main app. For that, we have to load the necessary files.
- Our main app, depends on the modules 'ngRoute', 'ui.bootstrap', 'oc.lazyLoad'. So we have to load the following files
- angular.js, jquery.js, ngRoute.js, OcLazyLoad.js and UiBootstrap.js. In the shim, you can see app. Here we mentioned all the depedency for the main app.
- I've loaded indexController.js file with the main app. The reason is, I've defined IndexController as parent controller for all the controller.
app.js
The following is our app.js Structure.
define(['require'], function (require) {
var SampleApp = angular.module('SampleApp', ['ngRoute', 'ui.bootstrap', 'oc.lazyLoad']);
SampleApp.config(['$routeProvider', '$httpProvider', '$controllerProvider', '$provide',
function ($routeProvider, $httpProvider, $controllerProvider, $provide) {
SampleApp.registerController = $controllerProvider.register;
SampleApp.$register = $provide;
var version = "?bust=" + (new Date()).getTime();
$routeProvider
.when('/Page1', {
title: 'Page1',
templateUrl: 'App/Page1/Page1.html' + version,
controller: 'page1Controller',
caseInsensitiveMatch: true,
resolve: {
loadModule: ['$ocLazyLoad', '$q', function ($ocLazyLoad, $q) {
debugger
var deferred = $q.defer();
require(["Page1Controller"], function () {
$ocLazyLoad.inject('SampleApp.Pages');
deferred.resolve();
});
return deferred.promise;
}]
}
})
.when('/Landing', {
title: 'Landing',
templateUrl: 'App/Landing/Landing.html' + version,
controller: 'landingController',
caseInsensitiveMatch: true,
resolve: {
loadModule: ['$ocLazyLoad', '$q', function ($ocLazyLoad, $q) {
debugger
var deferred = $q.defer();
require(["LandingModule"], function () { deferred.resolve(); });
return deferred.promise;
}]
}
})
.when('/home', {
title: 'home',
templateUrl: 'App/Home/Home.html' + version,
controller: 'homeController',
caseInsensitiveMatch: true,
resolve: {
loadModule: ['$ocLazyLoad', '$q', function ($ocLazyLoad, $q) {
debugger
var deferred = $q.defer();
require(["HomeController"], function () {
$ocLazyLoad.inject('SampleApp.Home');
deferred.resolve();
});
return deferred.promise;
}]
}
}).otherwise({
title: 'Landing',
redirectTo: '/Landing'
});
$httpProvider.interceptors.push(
['$q', '$location',
function ($q, $location) {
return {
request: function (config) {
return config;
},
response: function (result) {
return result;
},
responseError: function (rejection) {
console.log('Failed with', rejection.status, 'status');
return $q.reject(rejection);
}
}
}]);
}]);
})
RequireJS expect AMD approach from us. Asynchronous module definition (AMD) is a JavaScript specification that defines an API for defining code modules and their dependencies, and loading them asynchronously if desired.
Here SampleApp is our main module.
Except indexController, we are loading other controllers (Landing, home, page1 and page2) lazily. These controller will not bind to the main module. When loading the controller lazily, then these should be available for us.
So we need register them with the module while loading lazily.
We can register controller, factory, services using the following.
- $controllerProvider
- $provide
In the app configuration, we have to use those to for future use.
SampleApp.registerController = $controllerProvider.register;
SampleApp.$register = $provide;
Here we have attached the register service with main app variable called registerController and $register.
LoadingController.js
define(['angular'], function (angular) {
var SampleApp = angular.module('SampleApp');
debugger
SampleApp.$register.factory('landingService', ['$http', '$rootScope', function ($http, $rootScope) {
debugger
return {
GetQuestions: function () {
return $http({
url: ""
});
}
}
}]);
SampleApp.registerController('landingController',
['$scope', '$http', 'landingService', function (scope, http, landingService) {
scope.GetQuestions = function () {
landingService.GetQuestions().success(function (data) {
}).
error(function (data) {
});
}
}]);
});
This is our landing controller. Here we have defined landing controller and service file in AMD manner. We have called the main app and registered the landingController and their services.
Ok. Here we have defined the landing controller file in AMD manner and registered the controller to the main app. Then how do we load this js file only on the routing landing?
We need to use resolve property on the routing. On the resolve use promises with requirejs, to load the file as below.
resolve: { loadModule: ['$ocLazyLoad', '$q', function ($ocLazyLoad, $q) {
var deferred = $q.defer();
require(["LandingModule"], function () { deferred.resolve(); });
return deferred.promise;
}]
}
Necessity of OcLazyLoad
Before look in to the use of OcLazyLoad, we will go for the short discussion on modularization.
Modularization:
Modularization is logically dividing our module in to smaller sub modules or write modules and combine them to our main module which will help the main module logically.
In future our app will grow as much as we do improvement. As we discussed till now, we are registering our controllers to main module.
So in the starting stage itself doing modularization, we'll help us not to rewrite the whole code.
The main purpose of the modularizing our application is to make it easier to reuse, configure and test the components in your application.
Modularization will help us for
- Splitting the code into logical modules
- Splitting the code into multiple files
Loading the files lazily will be done by the RequireJs. Consider the plugin NgDialog which is used for modal pop up (dialog box). We may not need in all the page except one or two page.
Suppose if I need this only for page1, then I can load this file easily through RequireJs. So after I loading this file, will this work? Surely not.
Why? What was happened here? Here we loaded the NgDialog file. But our app doesn't know what is NgDialog. Unless inject this module to our main app, main app doesn't know about NgDialog.
Dynamically injecting through RequireJs is not possible. So we are using OcLazyLoad to inject the module.
In our case, lets take a simple example.
We planned to think logically modularize our app. From the config file, we may understand that we will have multiple pages. So here I want to create a new module called SampleApp.Pages.
define(['angular'], function (angular) {
var SampleApp = angular.module('SampleApp.Pages', []);
SampleApp.config(['$routeProvider', '$httpProvider', '$controllerProvider', '$provide',
function ($routeProvider, $httpProvider, $controllerProvider, $provide) {
SampleApp.registerController = $controllerProvider.register;
SampleApp.$register = $provide;
var version = "?bust=" + (new Date()).getTime();
$routeProvider
.when('/Page2', {
title: 'Page2',
templateUrl: 'App/Page2/Page2.html' + version,
controller: 'page2Controller',
caseInsensitiveMatch: true,
resolve: {
loadModule: ['$ocLazyLoad', '$q', function ($ocLazyLoad, $q) {
var deferred = $q.defer();
require(["Page2Controller"], function () {
deferred.resolve();
});
return deferred.promise;
}]
}
});
}]);
SampleApp.controller('page1Controller',
['$scope', '$http', 'page1Service', function (scope, http, page1Service) {
scope.GetQuestions = function () {
page1Service.GetQuestions().success(function (data) {
}).
error(function (data) {
});
}
}]);
return SampleApp;
});
On the page "page1" routing, we will load this file. So after loading the file our main app doesn't know about the new module. So we need to inject it to main module. Refer the below code which is written in the config file at page1 routing
resolve: {
loadModule: ['$ocLazyLoad', '$q', function ($ocLazyLoad, $q) {
debugger
var deferred = $q.defer();
require(["Page1Controller"], function () {
$ocLazyLoad.inject('SampleApp.Pages');
deferred.resolve();
});
return deferred.promise;
}]
}
Note: For experimental purpose, I've tried one thing. Here you can notice something that page2 routing is mentioned in SampleApp.Pages. Is it possible? Yes it is possible. SampleApp.Pages is separate which will not come under the SampleApp. We are calling this SampleApp.Pages as submodule of main module. Right? Then how this will not come under the Main app? Yes, here SampleApp.Pages is the submodule not in the syntactic way but in the logical way. So as syntax, this is the separate module. This submodule will have separate routing. For experimental purpose I've done this. On refreshing the browser in page2, this will not render the page2. This will go to the Landing page as routing cannot be matched with route provider directory.
How can we solve this?
We can modularize our app. After modularization, we can separate the modularization file from the controller and service file. After separation, we need to load the modularization file where we mentioned the config and routing and should load the files when main app load and should inject those separated module. We can load the controller and services later based on our need.
Are we using OcLazyLoad only to inject the module lazily? Not at all. We can use OcLazyLoad to load the css file too.
References
http://www.sitepoint.com/understanding-requirejs-for-effective-javascript-module-loading/
https://cdnjs.com/libraries/backbone.js/tutorials/organizing-backbone-using-modules
http://requirejs.org/
https://en.wikipedia.org/wiki/Asynchronous_module_definition
Modularization
https://www.safaribooksonline.com/blog/2014/03/27/13-step-guide-angularjs-modularization/
http://tutorials.jenkov.com/angularjs/dependency-injection.html
http://henriquat.re/modularizing-angularjs/modularizing-angular-applications/modularizing-angular-applications.html
http://clintberry.com/2013/modular-angularjs-application-design/
Points of Interest
In this article, we've discussed how to implement lazy loading implementation using RequireJS with OcLazyLoad.
My intention is that this will be helpful for developers who are confused about how to start with lazy loading.
Here I've attached the working demo till what have we discussed.
Note: You have to configure the code in IIS to work in proper way. Don't run it from the physical path.
History
- 15th October, 2015 - First version
- 19th October, 2015 - Second version
- 01st November, 2015 - Third version
- 20th November, 2015 - Fourth version