AngularJS And Browserify — The Dream Team For Single Page Applications

18/08/14 by Bastian Krol

18 Comments

Let’s face it, structuring a large JavaScript project is not trivial. The tool Browserify enables you to use CommonJS modules for your frontend JavaScript, even though browsers do not support CommonJS modules natively. Browserify transforms all modules and their dependencies and creates one big lump of JavaScript which works in the browser — the Browserify bundle. Can we also use Browserify in an AngularJS project? Does it even make sense? Yes, and yes, definitely. This post shows how that is supposed to work.

Browserify — Why?

The biggest advantage when using Browserify is the modularization of the code base. Up to and including version ECMAScript 5, JavaScript does not offer a native module concept. Thus, Browserify/CommonJS is a direct competitor to AMD/RequireJS and also to ECMAScript 6 transpilers like Traceur (ES6 offers native modules). In my opinion, Browserify offers a number of advantages.

Including packages from npm directly is one of these advantages, maybe the most important one. The npm registry contains a multitude of JavaScript modules (close to 90,000 modules at the time of writing). And npm is no longer a package manager for server side JavaScript only. The npm registry also contains a lot of packages for the browser. Classics like jQuery, jQuery UI and es5-shim are already available via npm. Each day, more frontend projects make their releases available on npm, so the numbers are growing. Even packages which were not intended to be used in the browser originally can often be used in frontend projects, thanks to Browserify.

Thus, Browserify is a very good alternative to other frontend project setups (like RequireJS or a bunch of script tags).

Browserify — How?

To transform the sources, Browserify starts at one CommonJS module (the entry point) and follows all require statements in this module. The entry point and all dependencies are transferred into the bundle. The require statements in the dependencies are also resolved and included into the bundle. This process is continued recursively until all require statements have been processed and the bundle is complete.

AngularJS and Browserify

Using Browserify means using CommonJS modules and the CommonJS constructs exports/module.exports and require. When you are working with AngularJS at the same time, we need to decide how to to combine CommonJS modules with AngularJS’ dependency system. There are a few proposals how to do this. The most interesting in this respect is probably a presentation by Ben Clinkinbeard from ng-conf 2014. Some of his ideas have been used in this post.

Code

The example repository for this post is available on GitHub. If you would rather read code instead of prose, head over there. The example app is a todo app, as usual. The example repository can also be used as a project template for your next AngularJS project with Browserify.

Include AngularJS

Update: When this post was written, there were no official releases from the AngularJS teams on npm, let alone support for CommonJS. Since then, quite a bit has a changed and the AngularJS team now publishes CommonJS compliant releases to the npm registry. This makes this whole affair a lot easier. The post and the example repo have been updated accordingly.

To install the AngularJS libraries CommonJS format, simply do:

npm install --save angular angular-route

Now angular core as well as angular-route are available. In our app, we just need to require angular (or angular-route) like this:

var angular = require('angular');

Integrating AngularJS With CommonJS — A Play In Three Acts

In an ordinary AngularJS project (without Browserify) each file usually contains one AngularJS entity, be it a controller, a service, a provider, etc. The typical AngularJS boilerplate code to start the declaration of a controller might look like this:

app/js/controller/todo.js:

(function() { 'use strict'; angular .module('todoApp') .controller('TodoCtrl', function($scope, TodoService) { ... }); })();

In the simplest possible project setup, all JavaScript files are referenced in the HTML by a bunch of script tags.

app/index.html:

... <script src="/app/js/service/todos.js" type="text/javascript"></script> <script src="/app/js/service/imprint.js" type="text/javascript"></script> <script src="/app/js/controller/edit_todo.js" type="text/javascript"></script> <script src="/app/js/controller/todo.js" type="text/javascript"></script> <script src="/app/js/controller/todo_list.js" type="text/javascript"></script> <script src="/app/js/controller/imprint.js" type="text/javascript"></script> <script src="/app/js/controller/footer.js" type="text/javascript"></script> // more script tags ...

The Naive Approach

We could do something quite similar when using Browserify. A CommonJS module to define a controller would look like that:

app/js/controller/todo.js:

'use strict'; var angular = require('angular');   angular .module('todoApp') .controller('TodoCtrl', function($scope, TodoService) { ... });

The only difference then is that we omit the IIFE — by definition, CommonJS modules always have their own scope and do not access the global scope. This is also why we need the statement var angular = require(‘angular’);, there is no global variable angular.

Since Browserify merges all CommonJS module into a single file, the script tags in index.html will be replaced by a single script tag for the Browserify bundle. This lengthy enumeration now moves to a JavaScript file, for example to our entry point. The file that declares the AngularJS module is a reasonable choice for the entry point.

app/js/app.js:

'use strict';   var angular = require('angular'); var app = angular.module('todoApp', [ 'ngRoute' ]);   require('./service/todos'); require('./service/imprint'); require('./controller/edit_todo'); require('./controller/todo'); require('./controller/todo_list'); require('./controller/imprint'); require('./controller/footer'); // ... more require statements, one per file

Right now using Browserify does not yet really help to structure the code. We ought to do better than that.

One index.js Per Source Directory

The first flaw we would like to get rid of is the long list of require statements in app.js. app.js will only list the directories from which we import modules.

app/js/app.js:

'use strict';   var angular = require('angular'); var app = angular.module('todoApp', []);   // one require statement per sub directory instead of one per file require('./service'); require('./controller');

If we pass a directory instead of a file as the argument to the require function, it will automatically look for a file named index.js in that directory and require this file. To make use of this feature, we create an index.js in each directory that defines which files from this directory will be required:

app/js/controller/index.js:

'use strict';   require('./edit_todo'); require('./footer'); require('./todo'); require('./todo_list'); require('./imprint');

A little aside: Reasonable directory structures for AngularJS projects have been discussed a number of times. The suggestions of this post are independent of the directory structure, it does not matter if the directories are created along a technical axis (controller, service, directive, …) or along the business domain.

With this change the code already has become a bit cleaner. But we are still using Browserify primarily as an oversized tool for script concatenation — not entirely satisfying.

Where To Put The AngularJS Boilerplate Code?

Our next optimization takes care of the usual AngularJS boilerplate code that is needed to define an AngularJS entity (controllers, services, …). Instead of putting this code in each CommonJS module, we can also put this into the index.js files:

app/js/controller/index.js:

'use strict'; var app = require('angular').module('todoApp');   app.controller('EditTodoCtrl', require('./edit_todo')); app.controller('FooterCtrl', require('./footer')); app.controller('TodoCtrl', require('./todo')); app.controller('TodoListCtrl', require('./todo_list')); app.controller('ImprintCtrl', require('./imprint'));

Now the individual CommonJS modules for each controller and service are independent of AngularJS:

app/js/controller/todo.js:

'use strict';   module.exports = function($scope, TodoService) { ... };

All in all the code is now considerably cleaner. An added bonus of this approach is improved testability for the AngularJS entities (see next section).

Unit Tests

The fact that the individual CommonJS modules are now comprised only of one single function which does not depend on AngularJS, has a distinct advantage: We can write unit tests independently of AngularJS, so we can use any test framework we like. Here is an example with Mocha and Chai:

test/unit/service/todos.js:

'use strict';   var chai = require('chai') , expect = chai.expect;   var TodoServiceModule = require('../../../app/js/service/todos.js');   describe('The TodoService', function() { var TodoService;   beforeEach(function() { TodoService = new TodoServiceModule(); });   it('should have some todos initially', function() { var todos = TodoService.getTodos(); expect(todos.length).to.equal(4); expect(todos[0].title).to.equal('Buy milk'); }); });

These tests can be executed without AngularJS and, in particular, without a browser. For example, we can simply execute them with Mocha in Node.js. This allows for rapid feedback and can easily be integrated into CI builds (because it is headless).

Additionally we should execute the unit tests in real browsers once in while, since there can be minute differences between Node.js and browser JavaScript runtimes. That’s very easy with Mocha, we just need to put the Mocha library and the tests in a small HTML file. Since the tests contain require statements, we need to process them with Browserify first (see below).

Karma is another possibility to execute tests in the browser. Karma also supports Mocha test suites and it is possible to trigger Karma from a task runner like Gulp or Grunt, so this can be used in a CI environment.

Command Line Tools: Browserify & Watchify

No matter how nicely structured our code is, it’s all for naught when it does not work in the browser. We need to process our sources with Browserify to turn a bunch of CommonJS modules into something that the browser understands:

browserify --entry app/js/app.js --outfile app/dist/app.js

It would be quite annoying for the development workflow if we had to run this manually after every change. That’s the reason for Watchify. Watchify is a companion tool to Browserify and keeps running in the background, watching the source files. As soon as one changes, Watchify recreates the Browserify bundle. It supports the same command line parameters as Browserify, thus the command to start it is:

watchify --entry app/js/app.js --outfile app/dist/app.js.

To browserify the tests (to execute them with Karma, for example) the following command can be used:

browserify test/unit/controller/*.js test/unit/service/*.js --outfile test/browserified/browserified_tests.js
watchify test/unit/controller/*.js test/unit/service/*.js --outfile test/browserified/browserified_tests.js

There’s a directory named bin in the example repository, which has some useful shell scripts to start Browserify and Watchify.

Gulp Build, Live Reload, Karma, Protractor And All That

Doing things with a build system might be even more convenient then the command line tools Browserify and Watchify. gulp.js is a good choice here. The example repository contains a gulpfile.js with all the stuff that you would expect from a decent build configuration:

  • linting the JavaScript code with ESlint
  • run unit tests with Mocha (in Node.js),
  • process the code with Browserify (to create a non-minified bunde),
  • ngAnnotate & uglify (to create a minified Browserify bundle),
  • bundle the unit tests with Browserify and execute them in the Browser via Karma,
  • execute end-to-end tests with Protractor,
  • a server for static assets (gulp-connect),
  • browser live reload.

During development we can simply have gulp watch running in the background all the time. Every time we change a source file, the Browserify bundle will be updated and the browser automatically loads the new bundle so that the change is there immediately.

Conclusion

AngularJS projects profit from Browserify, too. It offers some nifty advantages:

  • modularizing with CommonJS
  • simple use of npm packages
  • improved testability, especially for unit testing
  • very mature tooling (Browserify command line tool, Watchify, integration with Gulp and Grunt)

And here we haven’t even tapped the advanced features of Browserify:

  • transforms
  • using Node.js core modules in the browser
  • source maps
  • cloud based cross browser tests with SauceLabs

Therefore, the recommendation for today is: Try Browserify in your next project! And then, never again go without it.

This is a translation of a German article published first at AngularJS.de, the leading German site for everything related to AngularJS.


Originally published at blog.codecentric.de on August 18, 2014.