Lazy Load with RequireJS and AngularJS

Lazy Load Angular Components with RequireJS

Lazy Loading in Angular and Integrating AMDs

Click Here for TL;DR

Angular’s Dependency Injection

Dependency injection (DI) as defined by Angular… DI in Angular allows you to use modules, services, controllers, directives, and filters that can depend on one another to operate.

By $injecting these dependencies into each other they can reference each others public APIs and operate accordingly.  This concept of DI loads all files upfront so that they are available for use whenever necessary (which may be never for certain use cases).

Require JS

From RequireJS site: “RequireJS is a JavaScript file and module loader. ” RequireJS has two main functionalities. require() and define(). require() allows you to depend on a component while define() allows you to create those components.

Having RequireJS lazy load dependencies (we’ll take controllers for example) is complicated and still depends on an initial “definition” js file to load.  For example:

1
2
3
4
5
6
7
define(['angular'], function (angular) {
  return angular.module('myApp.controllers', [])
    .controller('MyCtrl1', ['$scope', '$injector', function ($scope, $injector) {
      require(['controllers/MyCtrl1'], function(MyCtrl1) {
        $injector.invoke(MyCtrl1, this, {'$scope': $scope});
      }]);
});

This example above is the would-be contents of an initial controller.js file that would load on bootstrap giving your application access to the controller names.  When one of the controllers is called upon it would then load the independent controller file as listed in the require() block. The independent controller file controller/myCtrl.js would look like:

1
2
3
4
5
6
define([], function() {
  return ['$scope', function($scope) {
    $scope.message = "Hello World";
    $scope.$apply();
  }];
});

Notice how we have to call $scope.$apply() at the end of the function so Angular can run a digest cycle and be aware of all your controller’s models.

This way is complicated, messy, and unrealistic in the scope of large projects. So what other tools are out there?

Create Something Custom

Using Angular’s built in providers you can initiate your different components.

1
2
3
4
5
$controllerProvider.register;
$compileProvider.directive;
$filterProvider.register;
$provide.factory;
$provide.service;

Here is an example of a lazy load provider using this technique combined with RequireJS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
define(['myApp'], function (module) {
    function LoaderProvider(){
        this.controller = null;
        this.directive = null;
        this.filter = null;
        this.factory = null;
        this.service = null;

        //service factory definition
        this.$get = ['$q', '$rootScope', function($q, $rootScope){
            return {
                load: function (path) {
                    var q = $q.defer();
                    require([path], function () {
                        $rootScope.$apply(function () {
                            q.resolve();
                        });
                    });
                    return q.promise;
                 }
             };
        }];
    }

    var container = new LoaderProvider();
    module.provider('Loader', function(){
        return container;
    });
    return container;
});

The above example creates a factory called Loader and it’s associated provider (LoaderProvider).  You can then set the configuration of this provider in your apps config phase and lazy load your components in your route definitions (more about Providers, config phase, and run phase).  Here is an example of that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
define(['angular', 'uiRouter'], function(angular){
    return angular.module('myApp', ['ui.router'])
        .config(function (LoaderProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
            LoaderProvider.controller = $controllerProvider.register;
            LoaderProvider.directive = $compileProvider.directive;
            LoaderProvider.filter = $filterProvider.register;
            LoaderProvider.factory = $provide.factory;
            LoaderProvider.service = $provide.service;
        })
        .config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
            $urlRouterProvider.otherwise('/');
            $stateProvider
                .state('root', {
                    url:'/',
                    views: {
                        "content"  : {
                            templateUrl: 'views/index.html',
                            resolve : {
                                load : ['Loader', function(Loader){
                                    return Loader.load('controllers/mainCtrl');
                                }]
                            }
                        }
                    }
                })
        }]);
});

Using this method is much more simple.  In the Loader.load() you can insert an array with your controller and any dependencies it may have.  You can then write your controller’s code without worry of manually triggering digest cycles, injecting $scope, or any of the other concerns that come along with RequireJS. Below is an example of a controller using RequireJS and the custom lazy loading provider:

1
2
3
4
5
define([ 'providers/LoadProvider'], function (Loader) {
    Loader.controller('myCtrl', ['$scope', function ($scope) {
        $scope.message = "Hello World";
    }]);
});

RequireJS is a very useful AMD to use in general but with AngularJS as well, specifically for managing the loading of your external resources (jQuery, Firebase, etc…).  As mentioned above Angular’s DI is not an AMD.

Angular modules solve the problem of removing global state from the application and provide a way of configuring the injector. As opposed to AMD or require.js modules, Angular modules don’t try to solve the problem of script load ordering or lazy script fetching. These goals are totally independent and both module systems can live side by side and fulfill their goals. (Source)

Libraries That Do This For You

There are a few libraries that help take the complicated nature of Angular + RequireJS marriage out of the picture:

Angular 2.0

Angular 2.0 has many many feature updates and additions (A whole post can be written about this).  One of those features is lazy loading component with the new router, this new router is even being supported in 1.3! Angular Router

In The End

Integrating AMDs like RequireJS with Angular allow you to build powerful and large apps that can load quickly in the browser giving your user a better experience.  Lazy loading Angular components can be complicated with RequireJS but there are solutions out there to make this easier.

Pro Tip: Write Unit Tests!

Writing tests for your application is one of the most useful tools you can use to ensure your code has less bugs (I can’t in good conscious say any code is bug free), ensure a smooth user experience, protect against the dangers of large teams working on the same source, and much more. The benefits of testing are endless and I think it’s something everyone should educate themselves on!

Leave a Reply

Your email address will not be published. Required fields are marked *