browserify

Browserify in-depth

February 14th, 2015 | browserify, gulp, highcharts, javascript |

I was seeing this name Browserify everywhere but I always found difficult to understand how Browserify work when I was reading blogs or others articles. It was never clear or it was using things I don’t wanted (I wanted to use gulp for instance to build my project, no grunt, no bower, and I knew gulp-browserify was blacklisted). I decided to step into and learn from the beginning what is browserify, and how it works.

You just need to know at least Node and npm to read further.

So, what I only knew at the beginning was that Browserify is used to create a bundle of js files (on which you can apply transforms such as uglify to minify and reactify to convert React .jsx files), and the html just need to reference one file and then you don’t care anymore of what component to include in the page, how to handle dependencies (A needs B, B should be included before A, this kind of stuff).

That what is, all I knew. Now, let’s start from scratch and let’s discover what’s inside.

We will start by using the browserify command line to check how that’s work and we’ll finish with examples having html and differents third party libs such as Highcharts.

Node is smart

If you are used to Node, you are often writing this kind of code without thinking twice:

var _ = require('lodash');
var arr = [3, 43, 24, 10 ];
console.log(_.find(arr, function(item) { return item > 10; }));

That works if you have at least install the lodash package locally (npm install lodash), if you don’t, it will failed at resolving the dependency and will tell you : Error: Cannot find module 'lodash'.
Browserify allow us to recreate this behavior (the working one!) by building the Javascript file with all dependencies inside needed to run our app on a browser with a single

To create this bundle.js, I need to do something like:

browserify -e index.js -o bundle.js

We can’t build right now, browserify doesn’t know where to find the jquery module. To do that, install the jquery npm package : npm install --save jquery. Execute browserify, check index.html, it’s working. We are lucky because jquery has its npm package. We will see what to do after when a lib you want to use is not in the repository.

Custom lib

But first, let’s try with our own library helper.js :

var $ = require('jquery');
var counter = 0;

module = module.exports = function() {
    return $('
').text("you called me " + counter++); };

It’s a simple function that returns a new div (using jquery lib again) and counts how many times you have called it. Because you are coding in a modular way, don’t forget to export your module in a CommonJS way (module = module.exports = …).

We modify our index.js to play with it :

var $ = require('jquery');
var help = require('./helper.js');

$('body').append(help());
$('body').append(typeof counter);
$('body').append(help());

That renders:

you called me 0
undefined
you called me 1

Our helper is called, and we can see that counter is not known, because each module is in its own world (its own function). Let’s check bundle.js :

...({1:[function(require,module,exports){
var $ = require('jquery');
var counter = 0;

module = module.exports = function() {
    return $('
').text("you called me " + counter++); }; },{"jquery":3}],2:[function(require,module,exports){ var $ = require('jquery'); var myhelper = require('./helper.js'); $('body').append(myhelper()); $('body').append(typeof counter); $('body').append(myhelper()); },{"./helper.js":1,"jquery":3}],3:[function(require,module,exports){ /*! * jQuery JavaScript Library v2.1.3 ...

Each module is encapsulated in function(require,module,exports){ which is very handy when it comes to modularity, no global variables all over the place.

Now, let’s add a third-party library that is not in the npm repository.

A compliant CommonJS lib you have locally

If you have a lib locally (in your ./libs/) and it contains a line such as (d3 source example):

if (typeof module === "object" && module.exports) module.exports = d3;

You can import it normally with require() by specifying the path.

var d3 = require('./libs/d3.v3.js');
d3.select("body").selectAll("p")
    .data([4, 8, 15, 16, 23, 42])
    .enter().append("p")
      .text(function(d) { return "I’m number " + d + "!"; });

No module.exports but (function(w) { … })(window)

Let’s try to add this colors.js into our project. We downloaded the .min.js and added it in our libs/ folder. It does not have any module.exports references but it’s encapsulated like that :

(function(window) {
  var Colors = {};
  Colors.rand = function() { ... }
  ...
  window.Colors = Colors;
}(window));

So, it adds a new object called Colors into the given ‘window’ object.

Let’s update our index.js and use it (we remove the jquery dependency for the sake of simplicity) :

var cc = require('colors');
document.body.innerHTML = cc.rand();

Now, how do we tell browserify where to find this ‘colors’ package ? We need somehow to have somewhere module.exports = Colors. We don’t want to use “window.Colors” expecting it exists (we would need to include it manually in our HTML before bundle.js, we don’t want to), we want require() to handle the dependency and to return an instance that we can use under the alias ‘cc’. To do that, we would need to pass a fake item ‘window’ to colors.js for it to set its property Colors onto it, then we would grab the reference from it.

The package browserify-shim does that for you.

That allows you to require() CommonJS incompatible libs (module.exports =). Take a look here https://github.com/thlorenz/browserify-shim for more info.

To resolve my situation, we npm install it and we update package.json :

    "browserify": {
        "transform": ["browserify-shim"]
    },

    "browserify-shim": {
        "./libs/colors.min.js": {
            "exports": "Colors"
        }
    }
  • “browserify” : needed for “browserify-shim” to be used by browserify when it does its job.
  • “browserify-shim” : declares that our colors lib exports a variable named “Colors” and this is what we want require() to return

I want a highcharts chart

Let’s draw a chart with Highcharts. Highcharts is not in the npm repository. There is a package with this name but it’s a server-side rendering thingy, and we only want to use it client-side in our case and we want browserify to resolve the dependencies if we require() it. Let’s see how we can browserify-shim it.

Our target is to run :

require('Highcharts');
var $ = require('jquery');

$('body').highcharts({
    series: [{
        data: [13, 37, 42]
    }]
});

Because of how Highcharts is coded, it’s not going to be straightforward. I’ll just explain shortly how it’s coded to understand the solutions. Here is a preview of highcharts.src.js :

(function () {
  var win = window;
  Highcharts = win.Highcharts = {};
...
  (function ($) {
    win.HighchartsAdapter = win.HighchartsAdapter || ($ && {
      ...
    });
  }(win.jQuery));

  ...
  // check for a custom HighchartsAdapter defined prior to this file
  var globalAdapter = win.HighchartsAdapter,
      adapter = globalAdapter || {};
  ...
}());

So, everything in a function() taking no parameters, explicit reference to window, and explicit reference to window.jQuery. That’s not very modular.

Two solutions :

Another adapter

We change package.json like that:

    "browser": {
        "Highcharts": "./libs/highcharts/highcharts.src.js",
        "HighchartsAdapter": "./libs/highcharts/standalone-framework.src.js"
    },

    "browserify-shim": {
        "Highcharts": {
            "depends": ["HighchartsAdapter:HighchartsAdapter"]
        },
        "HighchartsAdapter": {
            "exports": "HighchartsAdapter"
        }
    }

Here, we export one global symbol “HighchartsAdapter” coming from standalone-framework.src.js (available in Highcharts code source) which is NOT jquery dependent. It just defines a generic var HighchartsAdapter = { ... }; that we export under the same name, for the condition win.HighchartsAdapter = win.HighchartsAdapter || ($ && { to work. And we tell browserify that Highcharts depends on it (to be imported first).

Because Highcharts just injects itself into window (remember Highcharts = win.Highcharts = {};), we have access to the global var ‘Highcharts to create our chart :

// no jquery needed
require('Highcharts');
new Highcharts.Chart({
    chart: {
        renderTo: document.body
    },
    series: [{
        data: [13, 37, 42]
    }]
});

Okay, problem solved with another adapter independant of jQuery. Let’s say I have jQuery in my page and wants to use it.

Default jQuery adapter

Let’s make it with the default jQuery adapter now (included by default in highcharts.js). We need window.jQuery to be available when Highcharts is imported for it to create the jQuery adapter. Thus, we need to export the module jquery into this symbol jQuery.

    "browser": {
        "Highcharts": "./libs/highcharts/highcharts.src.js"
    },

    "browserify-shim": {
        "Highcharts": {
            "depends": ["jquery:jQuery"]
        }
    }

When we add the dependency to jquery:jQuery we actually define that global.jQuery = require('jquery') (global being the window in the browser), therefore Highcharts can find it. We can run our target example now.

Conclusion

To resume, when you want to work with a third party library like it was any standard npm package, you need to understand how it works internally, if it has hardcoded references to window.xxx or some other objects to be able to depends on them / export them globally.

Published by

ctheu

Hey. I love computer sciences almost since I'm born. I work with frontend and backend technologies; I can't make my mind ! Let's share some tips about how some of them work, shall we ?