Require.js packages for building large scale angular applications

javascript Sep 10, 2014

Require.js is one of my favorite ways to work with large JavaScript applications. It allows you to build AMD (asynchronous module definition) style modules to break up your application into small reusable modules. This is a desirable feature in large code bases.

It's important to know when and why you may consider require.js. If you're working on a small application that doesn't have a need for many files and little logic, you may or may not need the overhead of require.js. However, you still can get value out of it's organization and structure it provides you.

On the other hand there is the use case of the large scale application with many files, lots of logic, tons of code, etc. This type of application is where require.js shines greatly. You'll spend a bit of time getting it set up initially, but the structure, organization, and added maintainability working with require gives you is great for large apps.

AMD

A typical AMD module looks like...

define(['angular'], function() {
	return {
    	/* API for this module */
    };
});

Or you can use the commonjs style AMD module..

define(function(require) {
	var angular = require('angular');
    
	return {
    	/* API for this module */
    };
});

This pattern ends up helping you create a dependency tree so you know what modules depend on what other modules.

Packages

One of the things available in the require.js library is the ability to create packages. A require.js package looks almost like what a typical commonjs, node package would look like. This means you'll have an entry module, like index.js or in the case of require, you'll have main.js for your package.

The main.js is responsible for loading in the other modules for the package. Then in your requirejs config call you can simply ask for a package...

NOTE: Don't forget that angular is NOT an AMD module, so you have to shim it and any other angular plugin you use

require.config({
	packages: ["chat"],
    paths: {
    	angular: "/app/javascripts/vendor/angular/angular",
        ngRoute: "/app/javascripts/vendor/angular-route/angular-route"
    },
    shim: {
    	angular: {
        	exports: "angular"
        },
        ngRoute: {
        	deps: ["angular"]
        }
    }
});

Then you'll have a folder structure like this...

/app
/app/javascripts
/app/javascripts/main.js
/app/javascripts/app.js
/app/javascripts/chat
/app/javascripts/chat/main.js
/app/javascripts/chat/chatModule.js
/app/javascripts/chat/chatCtrl.js

And the chat/main.js just loads in the modules it needs...

define(function (require) {
    var chatCtrl = require("./chatCtrl");
});

Notice the ./ here, that tells require.js to load the module relative to the current package.

Angular.js modules

Angular.js also has a concept of modules that allow you to break up your application.

If you combine angular modules with require.js packages, you have a nice system for creating reusable modules.

Each package needs to have a main.js to load the packages modules, and also it needs to have some module.js file for angular.

In the chat example, there would be a chatModule.js...

define(function (require) {
    var angular = require("angular"); 
    return angular.module("my.chat", []);
});

Now in your chatCtrl module above, you use that module to define your controller...

define(function(require) {
    var chat = require("./chatModule");

    function ChatCtrl() {
        /* Chat controller */
    }

    return chat.controller("ChatCtrl", ChatCtrl);
});

Now if I wanted to add a Chat service to the package, you simply add ChatService.js to the chat folder...

define(function(require) {
    var chat = require("./chatModule"),
        io = require("socketio");

    function Chat() {
        var socket = io("/"),
            messages = [];

        return {
            messages: messages,
            send: function(msg) {
               	/* Send messages with socketio */
            }
        };
    }

    return chat.factory("Chat", Chat);
});

Then don't forget to go back and add your new service to the chat/main.js...

define(function (require) {
    var chatCtrl = require("./chatCtrl");
    var chatService = require("./chatService");
});

Use the package

Once you define the package as done above in the application's main.js, wherever you create your application's module in say, app.js, you simply require in the package...

define(function(require) {
	var angular = require("angular");

	require("ngRoute");
	require("chat");
    
    var app = angular.module("app", [
      "ngRoute",
      "my.chat" ]);
});

The last thing you have to remember to do, and this is with any angular app using a module loader, is remember to manually bootstrap your application. You have to do this because all the JS files load in asynchronously, so Angular won't know when to start the application.

You can do this in your main.js

require(['angular', 'app/app', function(angular) {
	angular.bootstrap(document.documentElement, ["app"]);
});

Conclusion

It's important to have a solid architecture when working with large scale JavaScript applications. AMD with require.js is one of many ways of accomplishing this. I have really liked working with this style of app, and created a starter application for anyone to get started working with it...

https://github.com/jcreamer898/requirejs-angular-starter

Feel free to fork, clone, and have fun!

Tags