For a better reading experience, read the article where it was originally posted: https://aymericbeaumet.com/properly-shim-angularjs-applications-using-browserify
Edit (March 5th 2015): the frontend community has evolved in the last few months and tends to be less hostile to the CommonsJS style (e.g.: Angular is now available on npm). This article has been rewritten accordingly.
AngularJS is a frontend framework developed by folks at Google. It allows to build advanced web applications in a modular way by splitting the code base into small components. This brings a lot of advantages like a clean separation of concerns and an easier testability.
Browserify is a tool which allows to benefit from the CommonJS modules in theoretically any JavaScript environments. Given an entry point, it computes a dependency tree, resolves it and produces a single output file. By consuming the package.json
, Browserify enables to require node_modules
in the build. This allows to rely on npm as a package manager for frontend dependencies, where Bower or Component would have been usually used.
When I first heard about Browserify, I immediately thought the modularity it brings would be really nice to build AngularJS applications. And it actually is. However they are not a perfect match by now, and some drawbacks need to be fixed.
This article presents a solution to structure an AngularJS application using Browserify. It covers the use of non-CommonJS modules as dependencies.
A sample application
Let’s consider a sample application which sets an entry in the local storage, and uses it to react differently whether it’s the first time a user is loading the page. It will be used as a support to identify the trouble cases and detail how to tackle them. So create an empty folder and let’s get started!
First, create an app.js
file which will be the entry point:
var angular = require("angular");
var app = angular.module("app", [require("angular-local-storage")]);
app.run(function (localStorageService) {
if (!localStorageService.get("lastVisit")) {
angular.element("body").text("Hello stranger!");
} else {
angular.element("body").text("Welcome back!");
}
localStorageService.set("lastVisit", Date.now());
});
Neat, isn’t it? If you come from a Node.js background I’m sure you do like it.
Now let’s generate the package.json
:
npm init
npm install --save angular angular-local-storage jquery
npm install -D browserify
Then compile the application:
./node_modules/.bin/browserify app.js -o bundle.js
And finally create a basic index.html
to load it:
<html>
<head>
<script src="bundle.js"></script>
</head>
<body ng-app="app"></body>
</html>
Pretty simple huh? But as you might expect, this doesn’t work. Open index.html
in a web browser and take a look at the console to find a nice JavaScript error.
Explanations
There are several issues occurring here.
Modules not defining an entry point
When a module is required, Browserify looks in the node_modules
folder to find its package.json
and loads it. Browserify expects it to have a main
property which contains the relative path to the file which should be loaded.
It is usual that this property is missing, that it points to the wrong file or that the package.json
itself is missing. In such case, you can override the file to load in the own project’s package.json
:
{
"browser": {
"angular": "./node_modules/angular/angular.js",
"angular-local-storage": "./node_modules/angular-local-storage/dist/angular-local-storage.js"
}
}
Modules not properly exporting their content
When Browserify requires the file indicated by the main
property in the module’s package.json
, it is usual that this file does not properly export its content in the CommonJS style. Also, we expect AngularJS modules to export their names so that we could directly require them in our AngularJS modules definitions.
To this end, browserify-shim can be used.
First install it:
npm install -D browserify-shim
Then, update the package.json
:
{
"browserify-shim": {
"angular": {
"exports": "angular"
},
"angular-local-storage": {
"exports": "angular.module('LocalStorageModule').name"
}
},
"browserify": {
"transform": ["browserify-shim"]
}
}
-
angular
exports the global variableangular
-
angular-local-storage
exports its AngularJS module nameLocalStorageModule
Modules fetching their dependencies on the global context
Some frontend modules relies on the fact their dependencies will be exposed on the global object (generally the window
object in most browsers). This is an anti-pattern of the CommonJS architecture which favors separation of concerns.
For example, AngularJS expects jQuery to be exposed at the window.jQuery
property.
One solution is to wrap the module into a function which exposes a global object with the appropriate properties. Hopefully, this could also easily done via browserify-shim (see above for the installation).
Then, update the package.json
:
{
"browserify-shim": {
"angular": {
"depends": "jquery:jQuery"
},
"angular-local-storage": {
"depends": "angular"
}
}
}
-
angular
will be provided thejquery
module on a fake window object at the propertyjQuery
-
angular-local-storage
will be provided theangular
module the same way
Wrap up
You should have added the following content to your package.json
:
{
"browser": {
"angular": "./node_modules/angular/angular.js",
"angular-local-storage": "./node_modules/angular-local-storage/dist/angular-local-storage.js"
},
"browserify-shim": {
"angular": {
"depends": "jquery:jQuery",
"exports": "angular"
},
"angular-local-storage": {
"depends": "angular",
"exports": "angular.module('LocalStorageModule').name"
}
},
"browserify": {
"transform": ["browserify-shim"]
}
}
Once you’ve made those corrections, you should be able to compile and run the sample application:
./node_modules/.bin/browserify app.js -o bundle.js
Check it really works by opening index.html
.
A working example can be found attached to this post.
Side notes
Some related facts which can save you time when dealing with Browserify:
- modules not published to npm can be installed by giving their git repository URL:
npm install git+https://github.com/angular-ui/ui-router.git
. Note that it is also possible to directly pass a GitHub repository:npm install angular-ui/ui-router
. A version can be specified by appending#tag
. See the documentation for more information - modules not containing a
package.json
cannot be installed using npm (even by directly providing a git repository). To tackle this issue, a third party installer like napa has to be used -
napa
will directly install the dependencies in thenode_modules
directory - the
postinstall
script is equivalent toinstall
. It is executed after the modules installation. However, I find it to be more explicit - you should place
napa
into thepostinstall
script - the paths provided in the
browser
field needs to be relative to the rootpackage.json
path. It is mandatory to start each entry by./
- it is possible to indicate files in another repository than
node_modules
when filling the browser field (e.g.:bower_components
) - centralizing Browserify options in the
package.json
is a good practice since it allows to keep a constant behavior when invoking it in different ways (CLI, API)
Conclusion
Even though there is a little more code to write to setup a project, this solution provides a beautiful way to organize its code in logical bricks. It totally matches the way AngularJS has been thought.
Also, managing all the dependencies in a single place is really useful. Especially when developing full-stack JavaScript applications, hence potentially sharing dependencies between the frontend and the backend (e.g.: Async.js, Bluebird, Moment.js, highlight.js, …). Using a CommonJS loader allows to rely on the exact same versions of the modules both client-side and server-side.