Introduction to Gulp.js 5: Bundling JavaScript with Browserify

Introduction to Gulp.js 5: Bundling JavaScript with Browserify

This is the 5th part of my series, Introduction to Gulp.js. Today I will show how to use Browserify to bundle your JavaScript and use CommonJS modules to run node modules in the Browser.

Browserify

This task is more complex because I use Browserify to bundle my JavaScript. If this is too complex for your needs, you may use gulp-concat to concatenate all your JavaScript files into one file.

Browserify is a wonderful tool, which allows you to use node modules in your browser. Over 70% of the node modules will run! And it will bundle up all of your dependencies. If you want to find out more about writing CommonJS modules for Browserify, have a look at the documentation.

This task I saw in the gulp-starter blendid. It’s long but clever. It allows the creation of multiple files with Browserify. I create two files. One file is loaded in the head of my website containing Modernizr and one file with the rest of my JavaScript at the bottom.

Creating JavaScript files with Browserify

Install the node modules needed for this task:

$ npm install --save-dev browserify@11.2.0 vinyl-source-stream@1.0.0 watchify@3.4.0 gulp-util@3.0.1 pretty-hrtime@1.0.1 gulp-notify@2.0.0

Create the entry in the config.js file:

gulp/config.js

browserify: {
  // Enable source maps
  debug: true,
  // Additional file extensions to make optional
  extensions: ['.coffee', '.hbs'],
  // A separate bundle will be generated for each
  // bundle config in the list below
  bundleConfigs: [{
    entries:    './' + srcAssets + '/javascripts/application.js',
    dest:       developmentAssets + '/js',
    outputName: 'application.js'
  }, {
    entries:    './' + srcAssets + '/javascripts/head.js',
    dest:       developmentAssets + '/js',
    outputName: 'head.js'
  }]
}

gulp/tasks/development/scripts.js

var gulp = require("gulp");
var browsersync = require("browser-sync");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var watchify = require("watchify");
var bundleLogger = require("../../util/bundleLogger");
var handleErrors = require("../../util/handleErrors");
var config = require("../../config").browserify;

/**
 * Run JavaScript through Browserify
 */
gulp.task("scripts", function (callback) {
  browsersync.notify("Compiling JavaScript");

  var bundleQueue = config.bundleConfigs.length;

  var browserifyThis = function (bundleConfig) {
    var bundler = browserify({
      // Required watchify args
      cache: {},
      packageCache: {},
      fullPaths: false,
      // Specify the entry point of your app
      entries: bundleConfig.entries,
      // Add file extensions to make optional in your requires
      extensions: config.extensions,
      // Enable source maps!
      debug: config.debug,
    });

    var bundle = function () {
      // Log when bundling starts
      bundleLogger.start(bundleConfig.outputName);

      return (
        bundler
          .bundle()
          // Report compile errors
          .on("error", handleErrors)
          // Use vinyl-source-stream to make the
          // stream gulp compatible. Specify the
          // desired output filename here.
          .pipe(source(bundleConfig.outputName))
          // Specify the output destination
          .pipe(gulp.dest(bundleConfig.dest))
          .on("end", reportFinished)
      );
    };

    if (global.isWatching) {
      // Wrap with watchify and rebundle on changes
      bundler = watchify(bundler);
      // Rebundle on update
      bundler.on("update", bundle);
    }

    var reportFinished = function () {
      // Log when bundling completes
      bundleLogger.end(bundleConfig.outputName);

      if (bundleQueue) {
        bundleQueue--;
        if (bundleQueue === 0) {
          // If queue is empty, tell gulp the task is complete.
          // https://github.com/gulpjs/gulp/blob/master/docs/API.md#accept-a-callback
          callback();
        }
      }
    };

    return bundle();
  };

  // Start bundling with Browserify for each bundleConfig specified
  config.bundleConfigs.forEach(browserifyThis);
});

This task has additional utilities for handling errors and logging the bundling process. Put these into a util folder in your gulp folder:

gulp/util/bundleLogger.js

/* bundleLogger
   ------------
   Provides gulp style logs to the bundle method in browserify.js
*/

var gutil = require("gulp-util");
var prettyHrtime = require("pretty-hrtime");
var startTime;

module.exports = {
  start: function (filepath) {
    startTime = process.hrtime();
    gutil.log("Bundling", gutil.colors.green(filepath));
  },

  end: function (filepath) {
    var taskTime = process.hrtime(startTime);
    var prettyTime = prettyHrtime(taskTime);
    gutil.log("Bundled", gutil.colors.green(filepath), "in", gutil.colors.magenta(prettyTime));
  },
};

gulp/util/handleErrors.js

var notify = require("gulp-notify");

module.exports = function () {
  var args = Array.prototype.slice.call(arguments);

  // Send error to notification center with gulp-notify
  notify
    .onError({
      title: "Compile Error",
      message: "<%= error.message %>",
    })
    .apply(this, args);

  // Keep gulp from hanging on to this task
  this.emit("end");
};

Using CommonJS Modules

Writing CommonJS modules is nice. You export your function, object, string, or integer, you like to export as a module or individually:

math.js

exports.add = function() {
  var sum = 0, i = 0, args = arguments, 1 = args.length;
  while (i < 1) {
    sum += args[i++];
  }
  return sum;
};
module.exports = {
  toggleNavigation: function() {
    ...
  }
};

Later, you import your modules and use them:

increment.js

var add = require("./math").add;

exports.increment = function (val) {
  return add(val, 1);
};

application.js

var navigation = require("./navigation");
var triggerNavigation = document.querySelector(".toggle-navigation");

document.addEventListener("DOMContentLoaded", function () {
  triggerNavigation.addEventListener("click", navigation.toggleNavigation);
});

Loading non-CommonJS files

But one problem remains: How do I use JavaScript files, which aren’t written in CommonJS syntax? Like Modernizr or jQuery?

I need to install browserify-shim:

$ npm install --save-dev browserify-shim@3.8.0

I open my package.json file and need to add a few lines:

package.json

{
  "...": "...",
  "browser": {
    "modernizr": "./app/_bower_components/modernizr/modernizr.js",
    "jquery": "./app/_bower_components/jquery/dist/jquery.js"
  },
  "browserify-shim": {
    "modernizr": "Modernizr",
    "jquery": "$"
  },
  "browserify": {
    "transform": ["browserify-shim"]
  },
  "devDependencies": {
    "...": "..."
  }
}

In the section "browser" you point browserify-shim to the asset you want to shim. I use Bower and have installed my packages into app/_bower_components/. The name you choose is the name you have to require later in your JavaScript.

Within "browerify-shim" you decide where to map this require to. To include jQuery or Modernizr later you would write:

app/assets/javascripts/head.js

require("modernizr");

app/_assets/javascripts/application.js

require("jquery");

$(function () {
  console.log("jQuery and Modernizr loaded");
});

You have to run npm install once you added a new entry to your package.json file.

Conclusion

This concludes the 5th part of my series, Introduction to Gulp.js. We learned how to use Browserify to bundle JavaScript files, how to use CommonJS modules to run Node in your Browser, and how to use non-CommonJS JavaScript files.