Hi and welcome!

AngularJS and RequireJS for Java developers 31-12-2015

AngularJSRequireJS

In this post I will show how to use AngularJS (1.4.8) with RequireJS (2.1.22) and how to integrate them into typical SpringBoot (1.3.1) application development and build processes.

Before we will dive into code a little disclaimer. I’m not a full time client side programmer. Mostly I do a server side development in Java. I heard about NPM, Grunt and Bower but didn’t use them. I have Java and Maven and I want to use these tools for my client side work. If you are hardcore JS guy then this post isn’t for you.

I encourage you to check out GitHub project I created as example for this blog post.

Project structure

As you can see this is typical Maven project with SpringBoot application. You can run the application with command:

make run

This will build an application and will run it on port 8888.

App in dev mode

Dependencies

The first problem you will notice after starting to work with AngularJS - you have a lot of JS files. You place your modules in separate directories and controllers, directives, services, etc. in separate files. And this is a problem for production. You need somehow concatenate all these files.

Angular provides dependency management on instances level. Controller can declare a dependency to a service, and Angular will inject an instance of the service to your controller during creation. But Angular does nothing with files dependencies. Angular do not know and do not care in which file your service is located. He just assumes that service should be already loaded in JS context. If not - it will fail.

In simplest case you can just concatenate all JS files and you probably will be fine. But in more complicated applications (multi page applications) you will have a dozen of modules, which depends on each other, and you will want to load only modules you need.

Also you will want to control the order in which resources are loaded. If application A depends on module M, then before bootstrapping application A you will want to be sure that file with module M is loaded.

This can be solved with RequireJS - a JavaScript module and file loader. RequireJS operates with modules (these modules are not the same modules in Angular). To create a RequireJS module you need to wrap your JS into define() function.

RequireJS maps modules to files. So if your add a dependency to another module, RequireJS knows in which file to find this module. And if you have a complex modules dependency tree, then RequireJS can understand which files and in which order to load to satisfy all dependencies and bootstrap an application in most efficient way.

So, to use RequireJS we need to convert our Angular staff into RequireJS modules.

For example, see common-services.js

define([
    'angular',
    'common/common-module'
], function(angular) {

    angular.module('common')
    .factory('commonService', [function() {
        return {
            sayHello: function(name) {
                return "Hello, " + name + "!";
            }
        }
    }]);

});

Here we define an Angular service commonService in common module and wrap it with define() function.

By default RequireJS module ID maps one to one to file. In example above, common-services module depends on common/common-module. And this means that RequireJS will load this dependency from file common-module.js inside common directory.

But it is also possible to define RequireJS modules in special configuration file. See config.js:

requirejs.config({
    baseUrl: 'js',
    paths: {

        // 3rd party
        angular: 'lib/angular',

        // vendor
        vendor: 'lib/vendor',

        // common app
        commonApp: 'common/common-app',

        // example app
        exampleApp: 'example/example-app'

    },
    // Angular is not distributed as AMD module, so we need to wrap it with define().
    // See http://requirejs.org/docs/api.html#config-shim for more details.
    shim: {
        'angular': {
            exports: 'angular'
        }
    }
});

As you can see module angular is mapped to file lib/angular.js. Now in common-services module we can specify a dependency to angular module by simply using module ID - angular.

RequireJS quickly gets tricky to configure. You have different possibilities to declare modules, you have shim for non AMD third party JS pieces, you have bundles which allow you to group modules into one file, etc…. And it may be difficult to get it all together.

After many experiments I found the following solution to handle this configuration complexity:

  • In config.js we define only top level modules - I called them applications.

  • For inner module dependencies we use default file based module ids. So during optimization we can move all inner modules to one application file. For example: all pieces located in directory common can be grouped and placed into one file common/common-app.

  • Applications can set dependencies to each other only using module ids defined in common.js. To define dependency to common inside exampleApp we should use commonApp module id. And we can’t directly add dependency to, for example, common/common-services module. See example-module.js:

    define([
        'angular',
        'commonApp'
    ], function(angular) {
    
        angular.module('example', ['common']);
    
    });

    Now, in common-controller we can use commonService as follows:

    define([
        'angular',
        'example/example-module'
    ], function(angular) {
    
        angular.module('example')
        .controller('exampleController', ['$scope', 'commonService', function($scope, commonService) {
    
            $scope.greeting = commonService.sayHello("Sergey");
    
        }]);
    
    });
  • Third party dependencies are defined as vendor module and in PROD environment are grouped into vendor bundle (we will see it later).

Using these rules it is fairly easy to manage dependencies inside Angular application.

Bootstrapping Angular application

Now the only missing part is - how to bootstrap an Angular application. See index.html:

 jQuery.ajax({
	 url: "/js/lib/requirejs-2.1.22.js",
	 type: "GET",
	 dataType: "script",
	 cache: true,
	 async: false,
	 global: false,
	 "throws": true,
	 success: function () {
		 require(["/js/config.js"], function () {
			 require(["exampleApp"]);
		 });
	 }
 });

Here I used jQuery to fetch requirejs JS file. After requirejs is downloaded it handles the load of other parts of application. As you can see, RequireJS loads config.js file and after that exampleApp module. And inside example-app.js happens Angular application bootstrapping:

angular.bootstrap(document.getElementById('example-app'), ['example'], {
	strictDi: true
});

Note, that you can load requirejs using ordinal <script> tag (see RequireJS documentation for details). And add JQuery to vendor bundle as separate module. I cheated a little bit to keep it simple.

Optimization

Next thing is optimization of resources. We want to concatenate everything related to each Angular module into one application file. Plus do minification. RequireJS can help with that too.

To perform optimization you need to use separate JS library downloadable from RequireJS site.

There are different ways to run optimization, but we, as true Java devs, will use Nashorn JavaScript runtime which is available in standard JDK 8 distribution. Check out pom.xml for plugin, which will do that:

<plugin>
	<groupId>org.codehaus.mojo</groupId>
	<artifactId>exec-maven-plugin</artifactId>
	<version>1.4.0</version>
	<executions>
		<execution>
			<phase>compile</phase>
			<goals>
				<goal>exec</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<executable>${java.home}/bin/${jjs}</executable>
		<commandlineArgs>-scripting ${project.basedir}/requirejs/r-2.1.22.js -- -o ${project.basedir}/requirejs/build.js</commandlineArgs>
	</configuration>
</plugin>

The optimization process is controlled by special build.js file. See RequireJS documentation for full list of available options in build.js.

In build.js we specify which modules to concatenate and minify. In our example as result of build process we should have 3 output files containing vendor, commonApp and exampleApp modules.

As you remember, to define inner module dependencies we used default file based module ids. How this will work if we move all parts to one application file? This will work, because at the time when RequireJS will try to find modules like common/common-service, these modules already will be loaded into JS context and RequireJS will not try to load them from files.

To perform optimized build fire following command:

make build_optimized

In Maven’s output you will see something like:

[INFO] --- exec-maven-plugin:1.4.0:exec (default) @ angularjs-requirejs-example ---

Tracing dependencies for: vendor

Tracing dependencies for: commonApp

Tracing dependencies for: exampleApp

lib/vendor.js
----------------
lib/angular.js
lib/vendor.js

common/common-app.js
----------------
common/common-module.js
common/common-services.js
common/common-app.js

example/example-app.js
----------------
example/example-module.js
example/example-controller.js
example/example-app.js

RequireJS traced down all dependencies for our modules and grouped them.

Now if you run optimized application:

make run_optimized

, you will see that we serve optimized and concatanated resources.

App in prod mode

One important note here. For optimized build we replaced config.js from src/main/resources with slightly modified version from requirejs/config.js.

In this file instead of shim we define a bundle and use urlArgs option to control versions of resources and force resource reload in browser after each build.

Conclusions

  • RequireJS and AngularJS plays nicely together. Although configuration can be difficult.

  • RequireJS optimizations run by Nashorn are slow and increase build time a lot.

  • It is possible to make further optimizations by grouping RequireJS modules used on one page into bundles. But, probably, for most projects optimization level we achieved in this post will be good enough.

  • We still have a problem with views optimization. I didn’t touched this problem in this post, but in short - if you use views intensivelly and don’t do any optimizations, then each view will be separate HTTP request.

  • It would be nice to see file level dependencies built in Anglular.