-
Notifications
You must be signed in to change notification settings - Fork 24
How to add angular require lazy to a project
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.
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.
-
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.
-
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 undertarget
. Sources for the main application are placed undersrc/main
, for tests undersrc/tests
. Traditionally, web content is placed undersrc/main/webapp
.
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 |
- The
node
andnpm
executables in thePATH
. - The
$BASE
directory and the directory structure under it has been created. - Familiarity with the tools: Node, npm, Grunt, Bower
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 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.
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.
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 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.
Let us add some simple sources for 2 views, view1 and view2. They will be lazilly loaded.
The template of this view:
<h2>View 1</h2>
Go to <a href="#view2">view 2</a>.
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).
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 onngResource
, 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
.
Add similar files for the second view. Just change "view1" to "view2" where appropriate.
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.
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"]
}
}
};
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.
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>
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.
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.
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;
}
grunt.loadNpmTasks("require-lazy-grunt");
grunt.loadNpmTasks("grunt-contrib-concat");
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).
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
}]
}
},
...
}
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
}
},
...
}
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:
-
lazyload.js
: Get it from here - it is not in Bower -
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;
-
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).
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
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.