Untangle Your JavaScript with Browserify
We've all been there. You're working on an project with a lot of JavaScript, and you need to add a new widget that depends on some libraries. You have a complex template structure and you don't know which libraries have been added as <script>
tags already, or where they might be. You can't just add them to the template you're working on, because that would add redundant HTTP requests. Furthermore, you might end up overwriting a library that had plugins added to it, breaking other widgets that relied on those plugins. So, you end up putting all your <script>
tags in your base template in a monolithic block and making sure you have them listed in the correct order.
Some time later, you realize you need to clean up your script block, but you have no idea which ones are still being used and which aren't. You remove some tags you think are unneeded and the site seems fine when you click around, but later you get a bug report about a broken widget that was actually using that library.
It doesn't have to be this way
As we do increasingly amazing things with the web, the size and complexity of our JavaScript code has exploded! In many cases we're not building "sites" any more, we are truly building "apps" - highly interactive and responsive tools that look less and less like the hyperlinked pages of the original web. To move forward in this environment it's vital to keep your code clean, well structured, and maintainable.
It's time to embrace modularity in our client-side code. Instead of writing tightly integrated code that depends on everything being in the global scope, we should strive to create decoupled, discrete components that clearly define their dependencies and the functionality that they export. There are many tools to help with this, but two of the most popular are Browserify and RequireJS.
Browserify and transforms
Though we used RequireJS briefly, in the end we chose Browserify for its simplicity, easy extension through transforms, and its focus on npm and the Node.js module system. It is an astoundingly flexible system that implements a "require" function on the browser and cleanly encapsulates modules so that they don't pollute the global scope.
Transforms allow Browserify to become incredibly versatile. You can consume AMD packages with deAMDify, or use browserify-shim to consume libraries that attach themselves to the window. You can take advantage of alternate package management systems, like Bower with debowerify or Component with decomponentify. You can smoothly handle code that needs to be precompiled, like CoffeeScript and JSX. You can even precompile things like Handlebars templates so that you can "require" them in your modules.
Let's get to work!
So, enough talk about why. Let's move on to how! Browserify is built with Node.js, so you will need to have it installed on your system. To take your first steps with Browserify, you'll probably want to install it globally so that you can use it as a command-line script.
$ npm install -g browserify
Writing modules
Now, let's write a simple module that requires something:
'use strict';
var _ = require('underscore');
var logUnderscoreVersion = function() {
console.log(_.VERSION);
}
module.exports = logUnderscoreVersion;
There are a few things you'll notice here. First, some of you will immediately point out that I'm using 'use strict';
outside of a function, and chastise me because that will apply strict mode to the entire global scope and break all the things! Thankfully, that's not the case here. Browserify encapsulates every module in it's own scope, so strict mode will only apply to the current module.
To use the Underscore library, I'm calling "require" and assigning it to the familiar "_" variable. At the moment, however, this will fail because we haven't installed it yet. Remedy this with npm:
$ npm install underscore
By calling the "install" command without the "-g" flag, you're telling npm to install your dependency in the local "node_modules" folder, which it will create for you if needed. Browserify will use that folder to find Underscore and make it available to your module.
Finally, at the end of the module I'm "exporting" the function that I defined. This means that I am making that function available outside of my module. When another module requires my module, the result will be whatever I assigned to "module.exports". This is how Node.js modules work. Anything I don't export stays private to my module.
Building a bundle
Now, let's use the command-line script to build a bundle for the browser. This will include all the required modules in one file. If you saved your module above as "logunderscore.js", browserify it like this:
$ browserify logunderscore.js > bundle.js
Now you can include bundle.js in an HTML file using a single script tag, and you're ready to use your JavaScript! Code that is outside of a function will be executed immediately, so a common pattern is to use a "main.js" or an "index.js" as an entry point that requires whatever you need to initialize your app and kicks it off immediately.
Requiring your own modules
When you need to require one of your own modules, use the relative path. You don't need to add the ".js" at the end of the path.
var logUnderscoreVersion = require('./logunderscore');
logUnderscoreVersion();
Exporting multiple things
If you need to export multiple functions or objects, you can use the "exports" shortcut from Node.js.
'use strict';
var logDate = function() {
console.log(new Date().getDate());
}
var logMonth() {
console.log(new Date().getMonth());
}
exports.logDate = logDate;
exports.logMonth = logMonth;
Then, you can use it like this:
var dateUtils = require('./dateutils');
dateUtils.logDate();
dateUtils.logMonth();
Or like this:
var logDate = require('./dateutils').logDate;
logDate();
Integrating Browserify with build tools
Once you're comfortable with Browserify, you'll probably want to integrate it with your favorite build tool.
Browserify and Grunt
In Grunt, you'll use grunt-browserify. Here's a config snippet that builds the bundle, and then watches for changes:
'browserify': {
options: {
debug: true,
transform: ['reactify'],
extensions: ['.jsx'],
},
dev: {
options: {
alias: ['react:'] // Make React available externally for dev tools
},
src: ['client/index.js'],
dest: 'build/bundle.js'
},
production: {
options: {
debug: false
},
src: '<%= browserify.dev.src %>',
dest: 'build/bundle.js'
}
},
This config takes advantage of several features, some of which I haven't mentioned yet. It's using the reactify transform to precompile JSX files for use with React. It instructs browserify to look for ".jsx" extensions so that you don't have to include them in your require path. It sets the debug flag so that Browserify will generate source maps for effective debugging in development, but overrides that flag in the production target to keep the build lean.
The "alias" option makes a reqirement available through a global "require" function. This allows you to work with multiple bundles, if you'd like. Here, though, it's being done so that the React dev tools extension can find React and enable a tab in Chrome. The "alias" setting in the Grunt plugin uses the bundle.require()
method from Browserify's API, which is also available with the "-r" flag on the command-line script.
Browserify and Gulp
The gulp-browserify plugin is currently a bit more minimal than its Grunt counterpart, but you can still do everything that you'd like to do by listening for the "prebundle" event and interacting with the bundler API directly.
var browserify = require('gulp-browserify');
var gulp = require('gulp');
var gutil = require('gulp-util');
var rename = require('gulp-rename');
gulp.task('browserify', function() {
var production = gutil.env.type === 'production';
gulp.src(['index.js'], {read: false})
// Browserify, and add source maps if this isn't a production build
.pipe(browserify({
debug: !production,
transform: ['reactify'],
extensions: ['.jsx']
}))
.on('prebundle', function(bundler) {
// Make React available externally for dev tools
bundler.require('react');
})
// Rename the destination file
.pipe(rename('bundle.js'))
// Output to the build directory
.pipe(gulp.dest('build/'));
});
You, too, can Browserify today!
Hopefully this guide has illustrated the usefulness of Browserify and helped you get it up and running yourself. If you've got questions or comments, let me know below or find me on Twitter @bkonkle. Happy coding!