Skip to content

How to add angular require lazy to a project

nikospara edited this page Oct 27, 2014 · 2 revisions

How to add angular-require-lazy to a project

Goal

This document is a guide for including angular-require-lazy in a project. It approaches this goal in a step-by-step fashion, setting up a hypothetical project. Since no two projects are the same, emphasis will be given to the whys for each step. For the same reason you are encouraged to be creative with this setup and make it fit your needs, rather than adapting your needs to it or the setup of the "Expenses" sample application.

The final project will have angular-require-lazy, Grunt as the build system, Bower for web package management.

State of this document

This is still draft; information provided is mostly correct, but not double checked. Additionally, there are still parts missing, most importantly the demo server, the testing configuration and dynamic modules.

Assumptions

  1. The client-side code resides in its own project, or at least is sufficiently isolated from the server-side code.

    This is not the year 2004 where Javascript was a nice to have, but optional, enhancement to a web application. The client-side code is an application of its own and ought to be treated as such.

    The setup described here is agnostic to the server-side technology. A simplistic Express application will be used for the needs of this guide, just to have something to work with. The setup will include grunt-contrib-connect to serve the client-side code and grunt-connect-proxy to proxy the REST services provided by the server-side application.

  2. Several directories will be used. For this guide we will assume a directory structure familiar to Maven users. This structure is fairly detailed and, with the help of the directory map of the next section, you should be able to adapt it to any structure that suits your needs.

    A summary for those unfamiliar with Maven: All sources go in various directories under src; all artifacts go to a subdirectory of or directly under target. Sources for the main application are placed under src/main, for tests under src/tests. Traditionally, web content is placed under src/main/webapp.

Directory map

Variable Meaning Example value (relevant to this guide)
$BASE The root directory of the project
$JSSRC Javascript sources for the client application src/main/javascript
$WEBCONTENT Web content (images, stylesheets etc) src/main/webapp
$BOWERROOT Where Bower places the web libraries src/bower
$WEBSCRIPTS The directory where the Javascripts will be placed under $WEBCONTENT scripts
$VENDORDIR The directory where vendor files will be placed under $WEBSCRIPTS lib
$BUILDDIR The directory that holds the web contents to be served for development target/build
$COMPILEDIR The directory that holds the final web contents target/compiled

Prerequisites

  1. The node and npm executables in the PATH.
  2. The $BASE directory and the directory structure under it has been created.
  3. Familiarity with the tools: Node, npm, Grunt, Bower

Setup steps

Create the package.json

Create the package.json for your project (see the reference). Add all the following devDependencies:

  • grunt
  • require-lazy-grunt
  • grunt-contrib-clean
  • grunt-contrib-copy
  • grunt-contrib-connect
  • grunt-connect-proxy
  • grunt-contrib-concat
  • grunt-contrib-watch
  • grunt-karma
  • bower
  • karma
  • karma-jasmine
  • dateformat: Version ~1.0.6
  • karma-requirejs
  • karma-phantomjs-launcher
  • istanbul

Run npm install. You will also need the grunt-cli package; you can either install it globally with npm install -g grunt-cli or locally (ommit the -g switch). The rest of the guide assumes that grunt is accessible from the command line.

HINT: You may also want to add ./node_modules/.bin to your PATH.

Create the Bower configuration

Create the file .bowerrc with the following contents (DO NOT FORGET TO REPLACE THE VARIABLE $BOWERROOT):

{
	"directory": "$BOWERROOT",
	"json": "bower.json"
}

Now add the file bower.json. Add the following to dependencies, based on angular-require-lazy:

  • angular: At least version 1.3.x

  • angular-route: With a version matching that of angular

    This library is not strictly required, but a lot of the work for angular-require-lazy has been done to support it, so you will probably want it. Unfortunately, angular-ui-router is not supported yet.

    If your application does not have routing, then you may want to reconsider if the overhead of adding angular-require-lazy is worth it.

  • requirejs

  • requirejs-text

  • almond

  • require-lazy

Add the following to devDependencies:

  • angular-mocks: With a version matching that of angular above

NOTE: If $BOWERROOT is under src, like in this guide, do not forget to exclude everything under it from your version control.

Start configuring Grunt

Create a build.config.js file with configuration customizations (idea I first saw in ngbp):

module.exports = {
	
};

Create a simple Gruntfile.js, at this point containing only the clean task:

module.exports = function(grunt) {
	var userConfig, taskConfig

	grunt.loadNpmTasks("grunt-contrib-clean");

	userConfig = require("./build.config.js"),

	taskConfig = {
		clean: ["target"]
	};

	grunt.initConfig(grunt.util._.extend(taskConfig, userConfig));
};

We will be adding code to the appropriate sections, i.e. under grunt.loadNpmTasks(...), inside the taskConfig = {...} and at the end of the file.

Starting with a build configuration

With our sources in various locations ($JSSRC, $WEBCONTENT, $BOWERROOT, etc), we need a script to gather them in a directory that can be served to the browser.

Change build.config.js to:

module.exports = {
	JSSRC: "src/main/javascript",
	WEBCONTENT: "src/main/webapp",
	BOWERROOT: "src/bower",

	WEBSCRIPTS: "scripts",
	VENDORDIR: "lib",

	BUILDDIR: "target/build",
	COMPILEDIR: "target/compiled",

	vendor_files: {
		js: [
			"almond/almond.js",
			"angular/angular.js",
			"angular-mocks/angular-mocks.js",
			"angular-route/angular-route.js",
			"require-lazy/lazy*.js",
			"requirejs/require.js",
			"requirejs-text/text.js"
		]
	}
};

Most of it are configuration options from the directory map table. The vendor_files are the vendor files to be copied to the build dir. We could copy the entire $BOWERROOT, but it is quite big (currently and at this stage ~1.6 MB, but it will probably get much bigger) and will delay our builds. On the other hand, there is the overhead of double configuration: when you add a web library, you need to add it to bower.json and the vendor_files.

Add these to the appropriate sections of the Gruntfile:

grunt.loadNpmTasks("grunt-contrib-copy");

taskConfig = {
	...
	copy: {
		build: {
			files: [{
				expand: true,
				cwd: "<%= WEBCONTENT %>",
				src: ["**/*"],
				dest: "<%= BUILDDIR %>"
			},{
				expand: true,
				cwd: "<%= JSSRC %>",
				src: ["**/*"],
				dest: "<%= BUILDDIR %>/<%= WEBSCRIPTS %>"
			},{
				expand: true,
				cwd: "<%= BOWERROOT %>",
				src: ["<%= vendor_files.js %>"],
				dest: "<%= BUILDDIR %>/<%= WEBSCRIPTS %>/<%= VENDORDIR %>"
			}]
		}
	}
};

Running grunt copy:build will copy the files to the web content directory.

Add the web server and proxy

Add the following to the appropriate sections of the Gruntfile:

grunt.loadNpmTasks("grunt-contrib-connect");
grunt.loadNpmTasks("grunt-connect-proxy");

taskConfig = {
	...
	connect: {
		options: {
			port: 8080
		},
		serve: {
			options: {
				base: "<%= BUILDDIR %>",
				keepalive: true,
				middleware: require("./src/build/grunt-connect-proxy-middleware")
			},
			proxies: [{
				context: "/api",
				host: "localhost",
				port: 8110
			}]
		}
	}
};

grunt.registerTask("server", ["configureProxies:serve","connect:serve"]);

The configuration for connect.serve.proxies reads as:: Requests under context will be forwarded to host:port, i.e. for this case the request to http://localhost:8080/api/user/login will be forwarded to http://localhost:8110/api/user/login. More details here.

I have externalized the middleware to another file in order to keep the Gruntfile as clean as possible. The file src/build/grunt-connect-proxy-middleware.js reads:

module.exports = function(connect, options) {
	var proxy, base, middlewares = [];
	proxy = require("grunt-connect-proxy/lib/utils").proxyRequest;
	middlewares.push(proxy);
	// see https://github.com/drewzboto/grunt-connect-proxy/issues/65
	base = Array.isArray(options.base) ? options.base : [options.base];
	base.forEach(function(b) {
		middlewares.push(connect.static(b));
	});
	return middlewares;
};

Running grunt server will start the server and the proxy.

Adding angular-require-lazy

The views

Let us add some simple sources for 2 views, view1 and view2. They will be lazilly loaded.

src/main/javascript/app/view1/view1.tpl.html

The template of this view:

<h2>View 1</h2>
Go to <a href="#view2">view 2</a>.
src/main/javascript/app/view1/controller.js

The controller of this view:

define(["currentModule", "templateCache!./view1.tpl.html"], function(currentModule) {
	"use strict";

	currentModule.controller("View1Ctrl", ["$scope", function($scope) {

	}]);
});

The currentModule abstracts the exact module that is related with this Angular artifact (service, controller, directive etc). It allows this artifact to be loaded lazilly. The currentModule provides the same API as angular.module("moduleName") - with the exception of config() functions that are not supported at this time.

The templateCache RequireJS plugin loads a template file (using the text! plugin behind the scenes) and adds it to Angular's templateCache with the full path, i.e. here app/view1/view1.tpl.html. It returns an object with 2 members, the full path and the text of the file (what text! would have returned).

src/main/javascript/app/view1/main.js
define(["angular", "currentModule", "./controller"], function(angular, currentModule) {
	"use strict";

	return angular.module("view1", currentModule.combineDependencies([]));
});

The main file for each view just creates an Angular module and returns it. There are a few details:

  • The angular variable (both the global and the one provided by RequireJS) are intercepted by angular-require-lazy code to support laziness transparently. The required bootstrapping procedure will be covered in a next section.

  • angular-require-lazy has a mechanism to allow each controller, directive, etc to specify its own Angular module dependencies. For this to work, the Angular module dependencies must be combined using currentModule.combineDependencies([]). If a dependency was to be specified at this level, e.g. dependency on ngResource, the file would be:

      define(["angular", "currentModule", "./controller", "path/to/ngResource"], function(angular, currentModule) {
          return angular.module("view1", currentModule.combineDependencies(["ngResource"]));
      });
    
  • The main file for each module needs to require all its dependencies; so it requires the controller.js.

view2/*

Add similar files for the second view. Just change "view1" to "view2" where appropriate.

Adding the scripts from angular-require-lazy

For the time being, angular-require-lazy is not packaged for Bower. So, you will need to copy the files manually.

Copy all the files from https://github.com/nikospara/angular-require-lazy/tree/master/WebContent/scripts/util/lib/angular-require-lazy to $JSSRC/lib/angular-require-lazy. This location ensures that, when angular-require-lazy is published for Bower all you will need to do is to remove the source folder and add the Bower dependency.

src/main/javascript/require-cfg.js

We want to be able to customize the RequireJS configuration, so we use the var require=... variant and take care to include this file before require.js.

var require = {
	baseUrl: "$WEBSCRIPTS", // NOTE: REPLACE THIS WITH VALUE!!!!!
	
	paths: {
		"angular": "lib/angular/angular"
	},
	
	map: {
		"*": {
			"text": "lib/requirejs-text/text",
			"lazy": "lib/require-lazy/lazy",
			"lazy-builder": "lib/require-lazy/lazy-builder",
			"promise-adaptor": "lib/angular-require-lazy/promiseAdaptorAngular",
			"currentModule": "lib/angular-require-lazy/currentModule",
			"templateCache": "lib/angular-require-lazy/templateCache",
			"ngLazy": "lib/angular-require-lazy/ngLazy",
			"ngLazyBuilder": "lib/angular-require-lazy/ngLazyBuilder"
		}
	},
	
	shim: {
		"angular": {
			exports: "angular"
		},
		"lib/angular-route/angular-route": {
			deps: ["angular"]
		}
	}
};

src/main/javascript/app/main.js

define(["angular", "lib/angular-require-lazy/bootstrap", "lib/angular-require-lazy/routeConfig",
	"lazy!app/view1/main", "lazy!app/view2/main", "lib/angular-route/angular-route"],
function(angular, bootstrap, routeConfig, view1, view2) {
	"use strict";
	
	var main = angular.module("main", ["ngRoute"]);
	
	main.config(["$routeProvider", function($routeProvider) {
		$routeProvider
			.when("/view1", routeConfig.fromAmdModule({controller:"View1Ctrl", template:"app/view1/view1.tpl.html"}, view1))
			.when("/view2", routeConfig.fromAmdModule({controller:"View2Ctrl", template:"app/view2/view2.tpl.html"}, view2))
			.otherwise("/view1");
	}]);
	
	bootstrap(document, main);
	
	return main;
});

In this simple case, we lazy! require all the views and register them through the routeConfig facility from angular-require-lazy. In more dynamic applications, you could employ the lazy-registry - the expenses example application does that.

The bootstrap facility hides all the steps required to configure Angular and angular-require-lazy. Just remember that as far as Angular is concerned this is manual bootstrapping, so do not include the ng-app directive in the HTML.

src/main/webapp/index.html

All is ready for the application page:

<!DOCTYPE html>
<html>
<head>
	<title>My application</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
	<div x-ng-view></div>
	
	<script src="scripts/require-cfg.js"></script>
	<script src="scripts/lib/requirejs/require.js" data-main="app/main"></script>
</body>
</html>

Run it!

This simple setup should be runnable. To summarize the build steps (Grunt):

  • Clean the output directory
  • Copy the files
  • Run the server

Or:

grunt clean copy:build server

Navigate to http://localhost:8080/index.html and Angular should redirect you to view1.

Currently we have not configured Grunt to watch for changes, so for any change to take effect you have to stop the server and rerun the command above.

Bundling with r.js

Having an application that is working in non-built mode, it is time to configure bundling. This still has some rough edges for the first-time configuration.

The Gruntfile

Load the require-lazy configuration

Add these vars:

var ..., options, config;

options = require("./src/build/options.js");
config = require("./src/build/app.build.json");

The src/build/app.build.json is the configuration file for r.js, documented in detail here. For the needs of this application, enter the following contents:

{
	"baseUrl": "$WEBSCRIPTS",
	"mainConfigFile": "$JSSRC/require-cfg.js",
	"inlineText": true,
	"name": "app/main",
	"optimize": "none",
	"normalizeDirDefines": true,
	
	"paths": {
		"angular": "empty:"
	}
}

And notes:

  • Make sure you substitute all the $XXX variables.
  • The name option is the main AMD module, substitute as appropriate.
  • "optimize": "none" is used for debugging; you will probably need "optimize": "uglify2", unless you plan to include a separate optimization (minification) step.
  • "angular": "empty:" is included because Angular 1.3 has some problems with r.js and we want to include it separately.

The src/build/options.js contains require-lazy-specific configuration:

var
	path = require("path"),
	fs = require("fs");

module.exports = {
	// this must match the map['*'].lazy property in require-cfg.js
	libLazy: "lib/require-lazy/lazy",
	// must point to the $BUILDDIR in a platform-specific way (thus path.normalize)
	// NOTE: For this guide we are using the preprocessed files as source; in the
	//       expenses example, this is pointing to the actual $JSSRC.
	basePath: path.normalize(path.join(__dirname, "..", "..", "target", "build")),
	// must point to the $COMPILEDIR in a platform-specific way (thus path.normalize)
	outputBaseDir: path.normalize(path.join(__dirname, "..", "..", "target", "compiled")),
	// Function to retrieve metadata for a given module. This is a facility of require-lazy,
	// not used in this guide. It IS used in the expenses example application to dynamically
	// build Angular's routes and the menu.
	retrieveMetaForModule: retrieveMetaForModule,
	// Make a path relative to the project directory
	makeBuildRelativePath: function(x) {
		return path.normalize(path.join(__dirname, "..", "..", x));
	}
};

/** Retrieve module metadata from a file called MODULE.metadata.json, if it exists. */
function retrieveMetaForModule(moduleName) {
	var
		 stat = null,
		 filename = path.normalize(path.join(__dirname, "../../target/build/scripts/", moduleName + ".metadata.json")),
		 ret = null;
	try {
		stat = fs.statSync(filename);
	}
	catch(ignore) {}
	if( stat !== null && stat.isFile() ) {
		try {
			ret = fs.readFileSync(filename);
			ret = JSON.parse(ret);
		}
		catch(e) {
			ret = null;
			console.log(e);
		}
	}
	return ret;
}
Load the required plugins
grunt.loadNpmTasks("require-lazy-grunt");
grunt.loadNpmTasks("grunt-contrib-concat");
Handle the index.html for the compiled mode

In the compiled mode we use Almond and LazyLoad instead of Require to save space. This leads to a slightly different setup. Create the file index-compile.html under $WEBCONTENT with the following contents:

<!DOCTYPE html>
<html>
<head>
	<title>My application</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
	<div x-ng-view></div>
	
	<script src="scripts/require-cfg.js"></script>
	<!-- BOOTSTRAP APPLICATION AND CALL APP/MAIN -->
	<script>
		define("angular",[],function() {
			return angular;
		});
		
		LazyLoad.js(["scripts/app/main-built.js"], function() {
			require(["app/main"]);
		});
	</script>
</body>
</html>

The compiled require-cfg.js will contain all manually concatenated files and the configuration.

This file will be the index.html in compiled mode, so add the build:compile Grunt task:

taskConfig = {
	...
	copy: {
		build: {
			...
		},
		compile: {
			files: [{
				src: "<%= BUILDDIR %>/index-compile.html",
				dest: "<%= COMPILEDIR %>/index.html"
			}]
		}
	},
	...
};

This simple project has no other web files. A real project would have. You have two options: copy everything from $WEBCONTENT to $COMPILEDIR, except the index-compile.html which is handled as above; or copy everything except index-compile.html and $WEBSCRIPTS from $BUILDDIR to $COMPILEDIR. I'd suggest the second option, so that you don't have to repeat the configuration or processing of other web resources (e.g. LESS files).

Add a connect server for the compiled mode

This is the same as the development server, only it serves the $COMPILEDIR directory.

taskConfig = {
	...
	connect: {
		options: {
			port: 8080
		},
		serve: {
			...
		},
		serveCompiled: {
			options: {
				base: "<%= COMPILEDIR %>",
				keepalive: true,
				middleware: require("./src/build/grunt-connect-proxy-middleware")
			},
			proxies: [{
				context: "/api",
				host: "localhost",
				port: 8110
			}]
		}
	},
	...
}
Add the angular-require-lazy configuration

This is simple, since we have externalized it in src/build/app.build.json and src/build/options.js:

taskConfig = {
	...
	require_lazy_grunt: {
		options: {
			buildOptions: options,
			config: config
		}
	},
	...
}
Add the manual concatenation task

Like mentioned before, Angular 1.3 does not cooperate well with r.js, so we have to include it manually together with some other scripts. The task to do this is:

taskConfig = {
	...
	concat: {
		compile: {
			src: [
				"<%= BUILDDIR %>/<%= WEBSCRIPTS %>/require-cfg.js",
				"src/build/compiled/almond-trick.js",
				"<%= BUILDDIR %>/<%= WEBSCRIPTS %>/<%= VENDORDIR %>/almond/almond.js",
				"src/build/compiled/require-cfg-almond.js",
				"<%= BUILDDIR %>/<%= WEBSCRIPTS %>/<%= VENDORDIR %>/angular/angular.js",
				"src/build/compiled/lazyload.js"
			],
			dest: "<%= COMPILEDIR %>/<%= WEBSCRIPTS %>/require-cfg.js",
			nonull: true
		}
	}
	...
}

This needs 3 more files:

  1. lazyload.js: Get it from here - it is not in Bower

  2. src/build/compiled/almond-trick.js:

     var baseUrl = require.baseUrl;
     // Almond suggests it is best not to use the `var require = ...` configuration.
     // Since it is convenient I keep ot so, but for the built application I keep the
     // original config in rcfg, delete require, then call `require.config()` after
     // almond is loaded.
     // If not for this workaround, the `config.map` option does not work.
     var rcfg = require;
     delete require;
    
  3. src/build/compiled/require-cfg-almond.js:

     require.config(rcfg);
    

If you wanted to include other scripts manually, this is the place. Specifically for jQuery, it has to be included before Angular, if angular.element is to use the full jQuery implementation. You have two options: load it separately (so just add a <script> before the others in index-compile.html), or bundle it (so specify paths.jquery: "empty:" in app.build.json and include it in the concat task above).

Add the conveniency tasks
grunt.registerTask("compile", ["clean", "copy:build", "require_lazy_grunt", "copy:compile", "concat:compile"]);
grunt.registerTask("server-compiled", ["configureProxies:serveCompiled","connect:serveCompiled"]);

Now grunt compile will build the compiled code and grunt server-compiled will serve it. You can combine both in one command:

grunt compile serve-compiled

Development mode: watch

When developing it is convenient to watch the source files for changes and rebuild. This can easily be accomplished by adding the following to the appropriate sections of the Gruntfile:

grunt.loadNpmTasks("grunt-contrib-watch");

taskConfig = {
	...
	connect: {
		...
		serve: {
			options: {
				...
				// CHANGE THE keepalive SETTING!!!
				keepalive: false
				...
			},
			...
		},
		...
	},
	watch: {
		development: {
			files: [
				"<%= JSSRC %>/**/*",
				"<%= WEBCONTENT %>/**/*"
			],
			tasks: ["clean", "copy:build"]
		}
	}
};

grunt.registerTask("dev", ["clean", "copy:build", "server", "watch:development"]);

Now grunt dev will make the $BUILDDIR, start the development server and watch the files; on change, the $BUILDDIR will be created again.