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 import.
Browserify –help
Let’s start with the command line application to fully understand how browserify works originally.
Install it globally to have access to it anywhere in the console :
> npm install -g browserify
> browserify
Usage: browserify [entry files] {OPTIONS}
Standard Options:
--outfile, -o Write the browserify bundle to this file.
If unspecified, browserify prints to stdout.
--require, -r A module name or file to bundle.require()
Optionally use a colon separator to set the target.
--entry, -e An entry point of your app
--ignore, -i Replace a file with an empty stub. Files can be globs.
--exclude, -u Omit a file from the output bundle. Files can be globs.
--external, -x Reference a file from another bundle. Files can be globs.
--transform, -t Use a transform module on top-level files.
--command, -c Use a transform command on top-level files.
--standalone -s Generate a UMD bundle for the supplied export name.
This bundle works with other module systems and sets the name
given as a window global if no module system is found.
--debug -d Enable source maps that allow you to debug your files
separately.
--help, -h Show this message
For advanced options, type browserify --help advanced
.
Be simple
Let’s try the simplest command :
browserify -o bundle.js
That generates a file with this content inside:
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oSeems like the base code of the dependencies management! We find the message Node sometimes tell us : “Cannot find module xxx”.
_.bundle(‘test.js’)
Let’s try to add our file in between :
browserify test.js -o bundle.jsbundle.js takes now 376 KB. You can find the whole
lodash
package inside, then my test.js at the end:(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o*/ ;(function() { ... /* lodash code */ }.call(this)); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],2:[function(require,module,exports){ var _ = require('lodash'); var arr = [3, 43, 24, 10]; console.log(_.find(arr, function(item) { return item > 10; })); },{"lodash":1}]},{},[2]); It’s standalone (node and browser)
A lot of function and
call
and parameters are added, let’s not try to understand. So, this file is now ‘standalone’, I can make it work on its own. For let’s, let’s try to copy it to another folder without node_modules, and start it with node. It works.Let’s do a big copy/paste into the Chrome debugger to use it in a browser. It works.
Create my bundle
--require, -r A module name or file to bundle.require()You can use this option to add any package you want, even if it doesn’t belong to the dependencies. For instance, let’s add the timeago package into my bundle :
browserify -r timeago test.js -o bundle.jsThe package timeago will be append to my bundle.js. I can create a bundle with only the packages I want but not my own code if I want to keep them separated :
browserify -r timeago -r lodash -o libraries.jsIf you want to
require()
one of your file (not in node_modules), you have to specify the relative path or browserify won’t find it:browserify -r ./custom-utils.js -o libraries.jsMultiple entries
--entry, -e An entry point of your appThis option is the one implicitely used when we just typed
test.js
in the command line, it could have been-e test.js
. Moreover, you can have multiple files as entries, all of them will be taken into account to create the bundle.You are empty inside
--ignore, -i Replace a file with an empty stub. Files can be globs.As it says, this option replaces the content of the dependencies by an empty object. The dependencies are still resolved but the content of the one you ignore is just blank.
For instance, if I ignore lodash, this is the bundle I got :browserify -i lodash test.js -o bundle.js...({1:[function(require,module,exports){ },{}],2:[function(require,module,exports){ var _ = require('lodash'); var arr = [3, 43, 24, 10]; console.log(_.find(arr, function(item) { return item > 10; })); },{"lodash":1}]},{},[2]);The first module is empty and corresponds to lodash (
{"lodash":1}
). If I try to start my program standalone, this is the result :console.log(_.find(arr, function(item) { ^ TypeError: Object #The dependency (
require('lodash')
) was resolved (bundle.js states it contains it:{"lodash":1}
) but because its content is empty, it couldn’t find any methods.
Even if I tried to start it in a folder where there is a node_modules with lodash inside, that has no impact, Node tries to use the blank one.I don’t want you
--exclude, -u Omit a file from the output bundle. Files can be globs.It’s a bit similar as –ignore but that doesn’t even let a blank, that just totally ignored it. Here is the output when I exclude lodash still :
browserify -u lodash test.js -o bundle.js...({1:[function(require,module,exports){ var _ = require('lodash'); var arr = [3, 43, 24, 10]; console.log(_.find(arr, function(item) { return item > 10; })); },{"lodash":undefined}]},{},[1]);We can see that the difference compared to –ignore is : no empty module and
{"lodash":undefined}
.
If I tried to start the program outside of folder with lodash in a node_modules folder, I expect it to not be able to resolve the dependency, indeed :module.js:340 throw err; ^ Error: Cannot find module 'lodash'But, if I start it from a folder where there is a node_modules with lodash inside, then, it will successfully resolve the dependency and work : because bundle.js does not state that it contains the dependency, Node tries to look inside the node_modules folder, that makes sense.
You can use this option if you know you’re exposing the dependency somewhere else the program will have access to.The truth is out there
--external, -x Reference a file from another bundle. Files can be globs.I don’t get it entirely, it looks like –exclude, the generated bundle.js is almost the same (it’s finished by
{"lodash":"lodash"}
), the command line behavior is the same. It seems to have a difference about the file/path you are giving. I looked at the source but that doesn’t help a lot.! But it seems more correct to use –external when you just want to specify to browserify that some dependencies have already been imported in another bundle.browserify -r lodash -r timeago -r ./custom-plugins.js -o common.js browserify -x common.js page1.js -o page1-bundle.js browserify -x common.js page2.js -o page2-bundle.jsIn this example, let’s say page1 and page2 both depends on lodash, timeago and custom-plugins.js. We create a custom bundle with those libs, and create 2 smaller bundles for page1 and page2 (instead of repeating the whole source inside both files).
Hulk
--transform, -t Use a transform module on top-level files.This one is very interesting and useful. This is a intermediate program that will take an input (a Stream of data, using a Buffer object, that represents in most case a string), do something with it, and output something (that could be the same as the input if it just want to compute something from it). Then, another transform or process take this output and that becomes its input etc. until the whole process is done.
For instance, there are transforms to :
– minify/uglify js/css/html
– compile .jsx or .coffee into .js
– compile .less or .sass into .css
– generate the js source maps (to debug)
– convert ES6 to ES5
– remove console.log and debugger from your code !
Here is nice list with pointers.I want mine
You can easily make your own. For instance, let’s do a transform that will comment out our code !?
var through = require('through'); module.exports = function(filename, options) { // we got data! function write(chunk) { this.queue('// ' + new Buffer(chunk) // chunk is just an array of integer (ascii) .toString('ascii') // we convert it into a string .replace(/\n/g, '\n//')) // we add comment in from of each line // and we queue our change back to the Stream } // no more data, everything okay, pass along the stream to the next process function end() { return this.queue(null); } // through returns a Stream. The caller will call write() to give us content return through(write, end); };Our original file test.js:
var timeago = require('timeago'); console.log(timeago(new Date()));If we call it like that
browserify -t ./test-transform.js test.js
, the result is :.... :{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports} // var timeago = require('timeago'); //console.log(timeago(new Date())); // },{}]},{},[1]);Our code has been commented out! What a nice transform. Because it has been commented out, no dependency was resolved (timeago) (they are resolved after the transform).
Just a note about the code:through
allows us to easily manipulate a Stream using a write and end method, it’s very popular (100k downloads / day!).I command you
--command, -c Use a transform command on top-level files.Just like transform but using the command line. Couldn’t make it work, no matter what.
events.js:72 throw er; // Unhandled 'error' eventI am all alone
--standalone -s Generate a UMD bundle for the supplied export name.Encapsule your code into a Add the UMD bundle. They are useful to work in node, in a browser with globals, and in AMD environments. (basically, browserify adds and init
module.exports
, test for thedefine()
function, and for ‘window’ at the beginning of the bundle).alert()
--debug -d Enable source maps that allow you to debug your filesThis generates the sourcemaps at the end of the bundle. That is useful when you want to debug with minified/transformed version, you need the original code to know what’s going on. This is why the sourcemap exists. The browsers generally automatically download them if you’re trying to debug and if there are available.
This is a big base64 string that is append at the end://# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uXFwuLlxcLi5cXC4uXFxBcHBEYXRh...You can use exorcist to not include this big base64 in your JS but in another file looking more human:
{ "version": 3, "sources": [ "..\\..\\..\\..\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\browser-pack\\_prelude.js", "test.js", "lib.js" ], "names": [], "mappings": "AAAA;ACAA;AACA;AACA;;ACFA;AACA;AACA;AACA", ...Some advanced options
I’m curious
--list Print each file in the dependency graph. Useful for makefiles.If you are curious and just want to check what the dependency graph looks like, you can use this option. If I do that on the base file react.js for instance :
> browserify --list react.js \npm\node_modules\browserify\node_modules\\process\browser.js lib\ReactCurrentOwner.js lib\Object.assign.js lib\ExecutionEnvironment.js lib\ReactPerf.js lib\ReactContext.js lib\ReactElement.js lib\ReactDOM.js ...Node only
--bare Alias for both --no-builtins, --no-commondir, and sets --insert-global-vars to just "__filename,__dirname". This is handy if you want to run bundles in node.If you are running bundles in node, you can activate this option. They will be lighter because a lot of dependencies are already part of Node itself, so it won’t add them again. For more details about the flags, check
browserify --help advanced
.browserify(Node)
What I try to browserify my Node program? Some libs just does not exist on the browser right ? Most of the built-ins libraries are nonetheless compatible and will be automatically imported by browserify into your bundle.
For instance, the Buffer class. It’s a global class in Node, you don’t need to
require()
it. Let’s make a program that using it, such as :console.log(Buffer(0xBE) .toString('hex'));6500730074002d00b0d01102000000000200000000000000b8d011020000000001000000...Now, if I browserify it, I end up with a 42KB .js file because I have the definition of the Buffer class and everything it needs. And it runs on a browser. (just c/c the whole javascript in the browser et voilà)
What if I use the fs package ? You can’t access the filesystem on a browser.
require('fs') .stat('.', function(err, stats) { console.log(err, stats); });null { dev: 0, mode: 16822, nlink: 1, ...If I browserify it, the generated file is small. That’s because the ‘imported’ fs package is actually blank in my bundle (thus, it doesn’t work in a browser). That makes sense.
You can find here https://github.com/substack/node-browserify/blob/master/lib/builtins.js the list of supported/unsupported builtins (the ones that are resolved to _empty.js are not!).Show me something
Let’s give a try with true applications that have some third party libraries and some HTML. Most of the websites are using jquery, let’s try.
jQuery
First, we create our
index.js
that requires jquery:var $ = require('jquery'); $('body').append('hey!');And we create
index.html
which contains nothing but the bundle import :To create this
bundle.js
, I need to do something like:browserify -e index.js -o bundle.jsWe 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 1Our 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 wantrequire()
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 returnI 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 towindow.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 genericvar HighchartsAdapter = { ... };
that we export under the same name, for the conditionwin.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
(rememberHighcharts = 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 symboljQuery
."browser": { "Highcharts": "./libs/highcharts/highcharts.src.js" }, "browserify-shim": { "Highcharts": { "depends": ["jquery:jQuery"] } }When we add the dependency to
jquery:jQuery
we actually define thatglobal.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.