Overview

AngularJS And Browserify — The Dream Team For Single Page Applications

18 Comments

Browserify Your AngularJS App

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

or

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.

Kommentare

  • shriek

    22. August 2014 von shriek

    I really think that this is how web-apps should be made now that we have all the tooling we need. The beauty of this approach is that it cleverly uses the CommonJS module system of node which is seriously lacking in browser. It will be interesting how people will module in ES6 and this approach but so far I’m really impressed how it keeps everything so clean and modularized.
    Good job on the post too. I was having hard trying to find any resource regarding this. It makes perfect sense now.

  • Andrew Del Prete

    I really found this article very insightful! especially the separation of functionality from AngularJS. Do you have any examples of testing Directives with this type of methodology? Since a Directive is just a function, is it also possible to unit test without pulling in Angular?

    • Bastian Krol

      It is probably possible, although I didn’t try.

    • Avi

      I was just thinking the same thing, but I don’t see how it is possible. The very purpose of a directive is to modify the DOM. That is hard to do without, at the very least, a synthetic DOM. I guess you could use jsdom https://github.com/tmpvar/jsdom but you are already getting pretty close to PhantomJS/ZombieJS. At least jsdom is entirely native JS, so easier to install and run cross-platform.

      Similarly for services, they often rely on various other services and providers, like $http. I guess you could mock them?

  • Ram

    Great article on integrating Browserify with Angular. Saved me a tonne of time working out how to use Browserify myself. Thanks for the github repo too. I use gulp all the time and it was nice to have a working example to learn from 🙂

  • Nelson Omuto

    23. September 2014 von Nelson Omuto

    This is a very clean way of doing things.

  • Pyjac

    Clean and elegant. Thanks

  • damionjn

    12. October 2014 von damionjn

    Thanks for the good post, loving the classy modularity 🙂

    By the way, you can install github repos directly using npm:
    npm i -S user/repo

    And using napa, you can install repos without an npm package.json:
    npm i -SD napa
    npm i napa -S angular/bower-angular angular/bower-angular-route

    Then in your app.js:
    require(‘angular/angular’)
    require(‘angular-route/angular-route’)

  • Andre

    Great article, well written (and translated) and I love the fact that unit testing with Mocha and Chai is included. Cheers

  • Geoffrey Cox

    Nice work on the blog entry! As an alternative, I applied some of your ideas to grunt and karma: https://github.com/redgeoff/angular-browserify-seed . I hope this is helpful to other people out there!

  • gsanta

    Great article!

    I would have one question.
    I created an index.js inside my services folder, just as you explained here.
    But what if one of my services uses another service?
    Should I require in that service (which uses the other) the whole services folder? Does not cause it any trouble (for example does not run the content of services/index.js multiple times, when I require it multiple times?)

    • Paul Everitt

      19. January 2015 von Paul Everitt

      I don’t believe you can replace DI with require(). First, you need an instance, not just the implementation. You of course can make an instance of the service, but that likely won’t work in several ways.

      Foremost, you won’t get the config/run phases of wiring up the service (though you might not need this.) Second, your instance might need to be defined in a certain “place”, e.g. a ui-router parent state.

  • Mateus

    Good article, but we already have the angular in official npm, then we no longer need napa for this case.
    However it was good to know the napa now is easy to use modules on github =D

    By google tranlator

  • HYA

    Very nice article, have already started using it in a project but I have one point in this approach…
    In each of these files app/Controllers/index.ts, app/Services/index.ts, app/Filters/index.ts
    have to write some thing very similar contents as
    var angular: ng.IAngularStatic = require(‘../Scripts/Angular’);
    var myApp = angular.module(‘MyApplication’, [‘angularLocalStorage’, ‘ui.bootstrap’, ‘dialogs’,…..]);
    how it would work with directives case ?

    • Bastian Krol

      That the require statements need to be repeated in each CommonJS module is a direct implication of the fact that these are indeed separate modules. The alternative would be to have angular as a variable in global scope. I would not recommend doing that just to save some characters.

      I did not understand your question about directives.

      • HYA

        Hi Bastian, the situation is a bit complex … let me try to give more precise example. I have created two test controller ‘Calculator’ and ‘Person’ in ./Controllers directory and a filter ‘cityNameFilter’ in ./Filters direcotry.
        so the ./Controllers/index.ts looks as
        var angular = require(‘../Scripts/Angular/’);
        require(‘../Scripts/AngularCookies/’);
        require(‘../libs/angularLocalStorage/’);
        var vwApp = angular.module(‘vw5Application’, [‘angularLocalStorage’]);

        vwApp.controller(‘Calculator’, [require(‘./Calculator’)]);
        vwApp.controller(‘Person’, [‘$scope’, require(‘./Person’)]);

        while ./Filters/index.ts looks like as
        var angular = require(‘../Scripts/Angular/’);
        require(‘../Scripts/AngularCookies/’);
        require(‘../libs/angularLocalStorage/’);
        var vwApp = angular.module(‘vw5Application’, [‘angularLocalStorage’]);

        vwApp.filter(‘cityNameFilter’, [require(‘./CityNameFilter’)]);

        but then both Controllers are not available and angular can not inject them until I put them again in ./Filters/index.ts as
        var angular = require(‘../Scripts/Angular/’);
        require(‘../Scripts/AngularCookies/’);
        require(‘../libs/angularLocalStorage/’);
        var vwApp = angular.module(‘vw5Application’, [‘angularLocalStorage’]);

        vwApp.controller(‘Calculator’, [require(‘../Controllers/Calculator’)]);
        vwApp.controller(‘Person’ , [ ‘$scope’, require(‘../Controllers/Person’)]);

        vwApp.filter(‘cityNameFilter’, [require(‘./CityNameFilter’)]);
        So it looks ./controllers/index.ts and ./Filters/index.ts both overwrite each other vwApp, not complement each other’s vwApp… So every controller and filter have to be defined in both index files and also in ./services/index.ts.

        • Bastian Krol

          The problem you see is because you create a new module named “vw5Application” every time you try to access it, overwriting the existing module every time. This is a bug in your code.

          If you write something like this

          var vwApp = angular.module(‘vw5Application’, [‘angularLocalStorage’]);

          (with an array as the second parameter) then you create a new module.

          If only want to retrieve the existing module to declare an other controller, service, directive or filter for it, you need to omit the second parameter (the array), like this:


          var vwApp = angular.module(‘vw5Application’);

          This only has one parameter, the module name, it assumes that the module has already been created and just retrieves it without overwriting it.

          The first syntax (with the array) must only exist in one place, only in one of your source files (and it needs to come first). All other places need to use the second syntax (without the array).

          If you look at my example code, you’ll notice that only
          app/js/app.js creates the module (array as second parameter):

          var app = angular.module('todoApp', [ 'ngRoute' ]);

          Anywhere else it is just:

          angular.module('todoApp');

          Also have a look at the section “Creation versus Retrieval” in the AngularJS module guide where this is explained.

          • HYA

            Thanks Bastian, that is right… let me try to understand your article one more time…

Comment

Your email address will not be published. Required fields are marked *