Thursday, February 11, 2016

AngularJS + RequireJS

The trick to working with an AngularJS application in RequireJS modules is to get a handle on two frameworks performing dependency injection at two different stages of the application.

So why bother? First of all, even though some of the AngularJS tutorials demonstrate putting your application module, controllers, and services all in one file that really is not the best way to organize your work. Follow the rule of coding one "type" per file and you will not have to put all of your work at risk every time you open up a file to make a change.

Second, once I have modularized my application into multiple source files the number of scripts that get loaded in the document escalates horribly. RequireJS allows each script to declare its own dependencies and with one script load in the document to bootstrap it they all take care of themselves. Plus, when I am looking at a module I know what the dependencies are, they are declared right at the top.

But should I deploy the application with a monolithic JavaScript file? That really depends on your circumstances, there is not one right answer. With a big application dynamic loading can help get it kicked off faster, especially if controllers and views are lazy-loaded. But sometimes lazy-loading could cause "stuttering" in the application and making the user wait at the start may be a better option. Unlike Bowerify which always delivers a monolithic file and requires a compilation step before viewing the page during development, RequireJS can deliver a monolithic file for deployment while dynamically loading modules during development.

RequireJS

RequireJS uses AMD (Asynchronous Module Definition) to define modules with dependency injection. Basically it does a similar thing to what you are familiar with in AngularJS, but the syntax is slightly different and the modules are loaded from files on the remotely.

There are three entry-points that you need to know: requirejs.config, require, and define. The require function allows you to execute something after the dependencies have been met, the define function allows you to define a module that requires dependencies, and requirejs.config accepts an object with configuration information for the framework.

The entry point is a script tag in the HTML page where RequireJS is loaded. The data-main attribute defines the script that will get everything started:

1
<script src="components/requirejs/require.js" data-main="main.js"></script>

To create a scope I wrapped the code with an iife (pronounced iffy), a self-calling function. Technically it is referred to as an "immediately invoked function expression." Typically there is a require call in the main script, which injects dependencies and then executes a function. In this case the function bootstraps the angular application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// main
//

(function () {
    
    require([ 'angular', 'modules/app', 'modules/app-config' ], function (angular) {
        
        angular.element(document).ready(function() {
            angular.bootstrap(document, ['angularJsRequireJs'], { strictDi: true });
        });
    });
    
}).call(this);

RequireJS defaults to looking for modules relative to the path of the bootstrap file. Modules do not specify the .js extension. So "app" and "app-config" would be in the modules folder relative to the location of main.js. This is what the application folder structure looks for this project. It is not what the whole project structure looks like, there is Node stuff in the folder above this. This is just the app folder containing the client-side application:

If you want to change the default path for modules add the baseUrl property to the configuration options, which are set through requirejs.config:

1
2
3
4
requirejs.config({
  
    baseUrl: "/modules"
});

While this example explains how to do it, I am going to back that out in the real code because I want RequireJS to load AMD modules from a bunch of folders: components, controllers, modules, etc.

Just FYI, RequireJS is not manageable as a Bower module. If you peak in the components folder I placed it there manually alongside of the other third-party components that Bower installed.

So the first thing that I need to do to get this example to work is to shim AngularJS because it is not an AMD module.

Configuring RequireJS

Configuration

AngularJS is not delivered as an AMD compliant module. So we need to "shim" it, which involves configuring RequireJS to treat it as an AMD module during dependency injection. This is the whole main script with the configuration, and the first section of the config object defines a map of names and module paths. We use that to establish short-names for the modules that may be frequently used, or modules like those that Bower brings in with long pathnames.

 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
31
32
33
34
35
36
37
38
39
// main
//

(function () {

    requirejs.config({
            
        paths: {
            'angular' : 'components/angular/angular',
            'angular-route': 'components/angular-route/angular-route',
            'assign': 'components/object-assign-shim'
        },
        
        shim: {
            'angular': {
                exports: 'angular'
            },
            'angular-route': {
                deps: [ 'angular' ],
                init: function () { return {}; }
            },
        },
        
        packages: [
            {
                name: 'assign',
                main: 'index.js'
            }
        ]
    });
    
    require([ 'angular', 'modules/app', 'modules/app-config' ], function (angular) {
        
        angular.element(document).ready(function() {
            angular.bootstrap(document, ['angularJsRequireJs'], { strictDi: true });
        });
    });
    
}).call(this);

The shim defines what to do for individual modules that are not AMD compliant. Angular creates a global object named "angular," so the shim simply tells RequireJS to export that object as the module object.

I added angular-route to this application because it shows how to add a non-AMD module that is dependent on another module. Angluar-route performs its own initialization, so we provide an empty "init" stub for RequireJS that returns an empty object as the module object.

I also added the Bower package object-assign-shim to the application, which adds the ECMAScript 2015 Object.assign function if it does not already exist in the JavaScript engine (ECMA-262 6th Edition). I am going to use that in a moment to define the controller module. While the object-assign-shim module is AMD compliant, we do have to tell RequireJS which script file in the module folder needs to be loaded: index.js.

Dependency Injection

Looking back at the call to require it is easier to see how the dependency injection works now.  The first argument to require is an array of the dependency modules. Each of these modules will be loaded asynchronously, but the require callback will not be executed until they have all been loaded. Modules are "singletons," the are loaded only once, and shared wherever something depends on them.

The only module that needs to be referenced in the require callback is the angular module, so that one is named in the function parameter list. The app and app-config need to be loaded before the bootstrap so the require is dependent on them, but they are not referenced in the function so there is no need to inject them. The best (and only) practice is to make the injected modules first in the dependency list, and the non-injected modules last. The order is not important, the individual dependencies have their own dependencies declared and RequireJS will figure out the load order.

Defining AMD Modules and Managing Dual Dependency Injection

The last piece is defining new modules, which we want to do in order to wrap the AngularJS code. The return value from the define callback function is a reference to the module object. In the case of the AngularJS application module we will return the module itself so it can be injected into other code.

1
2
3
4
5
6
7
// app
//

define( [ 'angular', 'angular-route' ], function (angular) {
    
    return angular.module('angularJsRequireJs', [ 'ngRoute' ]);
});

Defining the other modules to contain AngularJS constructs is similar. This config code depends on the application module as the place to register it. I could just use the angular.module function to retrieve the AngularJS module, but then I would need to make sure I had the module name right everywhere I used it. It is easy to make a mistake with that, so the best-practice is to use RequireJS injection to get it.

Also, RequireJS has added a constraint on the structure of the AngularJS application at this point. The AngularJS configuration in this example ends up being dependent on MathController. The MathController (defined below) is dependent on the application module. If I put the configuration code in the app.js file with the module definition, then that module would be dependent on MathController which is dependent on the application module. Circular dependencies are not a good thing and need to be avoided.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// app-config
//

define( [ 'modules/app', 'controllers/MathController' ], function (app) {
    
    app.config([ '$routeProvider', function () {
        
    }]);
    
    app.run([ function () {
        
    }]);
});

About that dependency on the MathController: I figure that the controllers will declare their dependency on services so I do not have to worry about those. But what is the best-practice for loading the controllers? There are three choices: eager-load all of the controllers (this example), lazy-load the controllers in the configuration of Angular-Route using the resolve property, or lazy-load the controllers after bootstrapping but before they are referenced. They are all viable choices, but it is a topic that I will discuss at another time.

The following controller is built using prototypal inheritance, which is the best-practice. I have a whole post on that subject here: Building Angular Applications using Inheritance. Controllers may be instantiated over and over again by AngularJS, and prototypes are created once while closures are re-created for every instance. If you are wondering how the forward reference works on line six, JavaScript actually compiles the function first so the forward reference works.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// MathController
//

define([ 'modules/app' ], function (app) {
    
    app.controller('MathController', [ '$scope', MathController ]);
    
    function MathController($scope) {
        
        this.$scope = $scope;
        this.$scope.value1 = 0;
        this.$scope.value2 = 0;
    }
    
    MathController.prototype = Object.assign(Object.create(Object.prototype), {
        
        addNumbers: function () {
            
            return parseFloat(this.$scope.value1) + parseFloat(this.$scope.value2);
        }
    });
    
    return MathController;
});

Pay attention to the dual dependency injection in this example. RequireJS is injecting the angular dependency, and then the AngularJS code is using it. Angular performs its own dependency injection for $scope in the controller.

Wrap-up

That is really everything that has to be done. Just extend this to create modules for each of the controllers, services, providers, and whatever else makes up your application. You can download the working project for this post at http://askjoelit.com/download/AngularJsAndRequireJs.zip. The project has a node application to serve the files, so make sure that you run "npm install" first to bring in the dependent Node modules and "bower install" to bring in the client dependencies before launching the service with "npm start."

References

See the references page.


No comments:

Post a Comment