Fabrizio Fabrizio - 3 months ago 46
AngularJS Question

Require pattern Browserify / Angular

I'm working in a project with angular and browserify, this is the first time for me to use this two tools together, so I would like some advice on which is the way to

require
files with browserify.

We may import those files in different ways, Until now I experimented this way:

Angular App:

app
_follow
- followController.js
- followDirective.js
- followService.js
- require.js
- app.js


For each folder with in the files for a plugin I created an
require.js
file and in it I require all the files of that folder. Like so:

var mnm = require('angular').module('mnm');

mnm.factory('FollowService', ['Restangular',require('./followService')]);
mnm.controller('FollowController',['$scope','FollowService',require('./followController')])
mnm.directive('mnmFollowers', ['FollowService',require('./followDirective')]);


and then require all
require.js
files in a unique file called
app.js
that will generate the
bundle.js


Question:

This way to require the files can be a good structure, or it will have some problem when I need to test? I would like to see your way to achieve good structure with angular and browserify

Answer

AngularJS and browserify aren't, sadly, a great match. Certainly not like React and browserify, but I digress.

What has worked for me is having each file as an AngularJS module (because each file is already a CommonJS module) and having the files export their AngularJS module name.

So your example would look like this:

app/
  app.js
  follow/
    controllers.js
    directives.js
    services.js
    index.js

The app.js would look something like this:

var angular = require('angular');
var app = angular.module('mnm', [
  require('./follow')
]);
// more code here
angular.bootstrap(document.body, ['mnm']);

The follow/index.js would look something like this:

var angular = require('angular');
var app = angular.module('mnm.follow', [
  require('./controllers'),
  require('./directives'),
  require('./services')
]);
module.exports = app.name;

The follow/controllers.js would look something like this:

var angular = require('angular');
var app = angular.module('mnm.follow.controllers', [
  require('./services'), // internal dependency
  'ui.router' // external dependency from earlier require or <script/>
  // more dependencies ...
]);
app.controller('FollowController', ['$scope', 'FollowService', function ...]);
// more code here
module.exports = app.name;

And so on.

The advantage of this approach is that you keep your dependencies as explicit as possible (i.e. inside the CommonJS module that actually needs them) and the one-to-one mapping between CommonJS module paths and AngularJS module names prevents nasty surprises.

The most obvious problem with your approach is that you're keeping the actual dependencies that will be injected separate from the function that expects them, so if a function's dependencies change, you have to touch two files instead of one. This is a code smell (i.e. a bad thing).

For testability either approach should work as Angular's module system is essentially a giant blob and importing two modules that both define the same name will override each other.


EDIT (two years later): Some other people (both here and elsewhere) have suggested alternative approaches so I should probably address them and what the trade-offs are:

  1. Have one global AngularJS module for your entire app and just do requires for side-effects (i.e. don't have the sub-modules export anything but manipulate the global angular object).

    This seems to be the most common solution but kind of flies in the face of having modules at all. This seems to be the most pragmatic approach however and if you're using AngularJS you're already polluting globals so I guess having purely side-effect based modules is the least of your architectural problems.

  2. Concatenate your AngularJS app code before passing it to Browserify.

    This is the most literal solution to "let's combine AngularJS and Browserify". It's a valid approach if you're starting from the traditional "just blindly concatenate your app files" position of AngularJS and want to add Browserify for third-party libs, so I guess that makes it valid.

    As far as your app structure goes this doesn't really improve anything by adding Browserify, though.

  3. Like 1 but with each index.js defining its own AngularJS sub-module.

    This is the boilerplate approach suggested by Brian Ogden. This suffers from all the drawbacks of 1 but creates some semblance of hierarchy within AngularJS in that at least you have more than one AngularJS module and the AngularJS module names actually correspond to your directory structure.

    However the major drawback is that you now have two sets of namespaces to worry about (your actual modules and your AngularJS modules) but nothing enforcing consistency between them. Not only do you have to remember to import the right modules (which again purely rely on side-effects) but you also have to remember to add them to all the right lists and add the same boilerplate to every new file. This makes refactoring incredibly unwieldy and makes this the worst option in my opinion.

If I had to chose today, I would go with 2 because it gives up all pretense of AngularJS and Browserify being able to be unified and just leaves both to do their own thing. Plus if you already have an AngularJS build system it literally just means adding an extra step for Browserify.

If you're not inheriting an AngularJS code base and want to know which approach works best for starting a new project instead: don't start a new project in AngularJS. Either pick Angular2 which supports a real module system out of the box, or switch to React or Ember which don't suffer from this problem.