Using ES6 modules with Browserify, Babel and Grunt
I recently tackled the task of converting our traditional multi-page web app’s JavaScript over to ES6 format modules. After initially being overwhelmed with the amount of options out there, I did a bit of research and landed on a solution that worked well for our project. This setup consists of Browserify, Babel and Grunt. Along with a couple of Browserify plugins to generate multiple bundles ready for production. And as ES6 modules are the future of JavaScript, this is the module format I chose.
These choices were influenced by the current technology being used in the project. We were already using Grunt so I stuck with that to avoid having to re-implement the rest of our asset pipeline in something like Gulp for example. In this article I’ll be guiding you through the steps to get this up and running for your project.
Converting scripts to ES6 module format
Before diving into the tooling, I’ll start off by going through some actual JavaScript. Our project wasn’t utilising any single-page application framework like Angular or React. It had a custom namespacing implementation and followed the revealing module pattern. Here is a simple example of this:
MyApp.Home = (function () {
var initialise = function () {
$(".button").on("click", function () {
MyApp.Messages.generateMessage(greetingMessage);
});
};
return {
initialise: initialise
};
})();
I wanted to move away from this manual approach of managing dependencies and creating namespaces, and start taking advantage of the new features available in ES6. So before setting up Browserify I had to go through all of the JavaScript files and convert them into the ES6 module format. This was fairly straightforward and was mostly just a change of syntax. The other thing I had to take care in changing is the way we handled third-party scripts. Rather than including them in the HTML with a script tag, we are now importing them in as necessary from npm. For more information on ES6 modules check out this great article by Wes Bos. Here is the code from above after being converted over to ES6 module format:
import $ from "jquery";
import * as messages from "./messages";
var initialise = function () {
$(".button").on("click", function () {
messages.generateMessage(greetingMessage);
});
};
export { initialise };
There are a few things to take note of here:
- We no longer have to create the namespace manually.
- There is no need to wrap the code in an immediately invoked function expression, which means there is no global variable being created.
- We are importing our dependencies.
- Instead of returning our initialise function we make use of the ES6 export feature.
Installing the packages
Let’s start setting up our tooling. Firstly, we’ll set up Browserify and Babel for Grunt. We are going to use Babelify (which is a Browserify plug-in) to transpile our ES6 JavaScript into ES5 compatible syntax. Make sure you have Node.js installed on your machine before continuing - you can download it here. Install the required packages with this command:
npm install grunt grunt-cli grunt-browserify babelify babel-preset-es2015 --save-dev
We are installing a few different packages here, so it may take a little while. As we’re working with a multi-page app we want to have separate bundles for scripts that are only required on a certain page, rather than having all scripts bundled together like you usually would for a single-page app. To do this with Browserify we need to install the factor-bundle plug-in:
npm install factor-bundle --save-dev
Factor-bundle splits browserify output into multiple bundle targets based on an entry-point. For each entry-point, an entry-specific output file is built. A really nice feature of factor-bundle is that it performs a bit of extra magic with files that are required by two or more of the entry files, and factors them out into a common bundle. No more manually creating and maintaining these bundles!
Setting up the Grunt task
Now it’s time to get Browserify to do something with our files. Here is the grunt configuration for our set up:
module.exports = function (grunt) {
grunt.initConfig({
browserify: {
development: {
src: [
"./js/main-home.js",
"./js/main-products.js"
],
dest: './dist/js/common.js',
options: {
browserifyOptions: { debug: true },
transform: [["babelify", { "presets": ["es2015"] }]],
plugin: [
["factor-bundle", { outputs: [
"./dist/js/main-home.js",
"./dist/js/main-products.js"
] }]
]
}
}
}
});
grunt.loadNpmTasks('grunt-browserify');
};
Okay so there’s a little bit going on here. Along with the usual grunt configuration code, we’re creating the browserify:development task with the following configuration:
- src: specifies our entry points. These are the entry points for each bundle we would like to create.
- dest: specifies where the scripts that are required for more than one bundle should be extracted to.
- options:browserifyOptions: we’ve specified debug: true, so that Browserify will generate source maps, which are useful for debugging.
- options:transform: specifies that our scripts are to be transpiled into ES5 compatible syntax. We are using the es2015 preset, which means that our ES6 module syntax will be converted into CommonJS format modules for Browserify.
- options:plugin: is where we’re specifying that factor-bundle should output multiple bundles based on the src entry points.
Let’s run Browserify with the following command:
grunt browserify
We now have our bundled ES5 compatible script files ready to use.
Automating
The next to step is to automate this process so that the script bundles get updated whenever we make a change to our JavaScript. To do this we simply add the following configuration to our Browserify grunt task:
watch: true,
keepAlive: true
Add these properties to the options object, just after the plugin property. Now whenever we make a JavaScript change, the task will transform our code to ES5 syntax and re-generate the bundles as required. What’s great about this feature is that it only transforms the code that we’ve changed, rather than transforming all of our JavaScript each time we make a change. Which means it’s lightning fast! And with that our Browserify setup is fully configured for development.
Setting up a production task
There is one final step; we need to minify our code for production. To accomplish this we will create a production sub-task for Browserify and utilise the Minifyify plugin. Minifyify is a browserify plugin that, as you may have guessed, minifies your code. Which is what we want to do before we deploy our scripts to production. Go ahead and install minifyify with this command:
npm install minifyify --save-dev
And here is the configuration for our production Browserify task:
production: {
src: [
"./js/main-home.js",
"./js/main-products.js"
],
dest: './dist/js/common.min.js',
options: {
browserifyOptions: { debug: false },
transform: [["babelify", { "presets": ["es2015"] }]],
plugin: [
["factor-bundle", { outputs: [
"./dist/js/main-home.min.js",
"./dist/js/main-products.min.js"
] }],
["minifyify", { map: false }]
]
}
}
Our scripts are now minified and ready for production with great browser support (IE9 and up). The last thing we will do is add some NPM scripts to the project just for convenience. Add the following to your package.json
file.
"scripts": {
"start": "grunt buildDev",
"build": "grunt buildProd"
}
Now we can start the project in development mode by running npm start
from the project’s root directory. And npm run build
to build for production. That’s it! I’ve created a github repository with an example set up based on this article.