Ouvrir la navigation secondaire

Ce billet a plus de deux ans. S'il contient des informations techniques elles sont peut être obsolètes.

Précédent Django avec CloudFront et S3, PEP 476
Suivant Passage par partage d'objet en Python et JavaScript

Browserify

JavaScript n'a pas de mécanisme natif d'import de modules, tout du moins pas avant ECMAScript 6. En attendant, des palliatifs ont été développés dont CommonJS et AMD.

Browserify est une application Node permettant, via une étape de build côté serveur, d'utiliser les modules CommonJS et les paquets npm dans le navigateur :

Use a node-style require() to organize your browser code and load modules installed by npm.

browserify will recursively analyze all the require() calls in your app in order to build a bundle you can serve up to the browser in a single <script> tag.

Installation

On a donc besoin de node, de npm et de browserify au niveau global pour pouvoir l'utiliser en ligne de commande :

$ sudo npm install -g browserify

Pour lister et vérifier les paquets installés globalement :

$ npm list -global --depth=0

Pour mettre à jour browserify au niveau global :

$ sudo npm update -global browserify

Fonctionnement

La commande browserify prend un ou plusieurs fichiers JavaScript en entrée et génère un flux des fichiers JavaScript concaténés sur la sortie standard qu'il est possible de rediriger avec > :

$ browserify src/main.js > dist/bundle.js

Le résultat bundle.js contient maintenant toutes les dépendances (exprimées via require()) dont main.js a besoin pour fonctionner et prend la forme d'une Immediately-Invoked Function Expression (IIFE) dont le premier argument est un objet qui fait correspondre pour chaque module importé un numéro unique (en tant que clé) et un tableau de 2 éléments (en tant que valeur) dans lequel :

  • le premier élément est le code source du module enveloppé dans une closure
  • le second élément (optionnel) est un autre objet qui fait correspondre les dépendances du module lui même avec un des numéros uniques

Comme chaque module est enveloppé automatiquement dans une closure on peut désormais utiliser 'use strict' en début de fichier sans faire gueuler JSHint, plus besoin de wrapper nos modules dans des IIFE à la main :)

Pour plus de détails je vous conseille de lire How Browserify Works et le manuel Browserify.

Les fonctions require, exports et module.exports

Dans Node.js l'élément fondamental est le module qui correspond simplement à un fichier JavaScript qui peut exposer tout ou partie de son code par l'intermédiaire de module.exports et exports. Les parties exposées peuvent ensuite être importées dans d'autres modules avec require.

module.exports est une spécificité de Node.js qui n'existe pas dans la spécification de CommonJS, ce qui vaut à son système de module l'appellation de node-flavored version of the commonjs module system dans le manuel Browserify.

D'après la documentation de Node.js, exports est une référence à module.exports :

The exports variable that is available within a module starts as a reference to module.exports. As with any variable, if you assign a new value to it, it is no longer bound to the previous value.

Voici ce qui peut arriver en cas de rebinding sauvage :

var module = {};
var exports = module.exports = {};

module.exports.foo = 'foo';

console.log(module.exports.foo);  // 'foo'
console.log(exports.foo);  // 'foo'

exports = 'bar';  // rebinding via a simple operation of assignment

console.log(exports.foo);  // undefined
console.log(module.exports.foo);  // 'foo'

La documentation de Node.js précise :

As a guideline, if the relationship between exports and module.exports seems like magic to you, ignore exports and only use module.exports.

Toujours est-il qu'il est possible de tirer parti de cette différence avec subtilité :

Mappage de source

Revenons à browserify qui va effectuer une concaténation et produire un fichier unique pouvant compliquer le débogage.

Les source maps sont une solution à ce problème et sont supportées par les outils de développement des navigateurs modernes (dont IE depuis la version 11).

browserify fait partie des outils supportant la génération d'un mappage de source et propose une option --debug pour l'activer.

Transformations

browserify donne aussi la possibilité d'appliquer des transformations sur le code source pendant la phase de build avec l'option --transform.

À titre d'exemple il est possible d'utiliser des modules AMD avec deamdify, de compiler du JSX en JavaScript avec reactify ou d'écrire son code en EcmaScript 6 et de dire à browserify de le passer à la moulinette du transpileur 6to5ify babelify :

$ npm install babelify
$ browserify --transform babelify src/main.js > dist/bundle.js

Automatisation

Vu que browserify recherche les dépendances externes dans le fichier package.json, on peut dire au revoir à Bower et ne conserver qu'un seul gestionnaire de paquets (ce qui, avec un peu de recul, s'impose comme une évidence).

Le fait de pouvoir appliquer des transformations permet également de considérablement réduire la verbosité du script de build.

Sa capacité à être utilisé en ligne de commande permet de l'intégrer dans a peu près n'importe quel système : Makefile, npm, Grunt, Gulp, etc.

En ce qui me concerne, j'ai intégré browserify dans mon Gruntfile.js :

browserify: {
    dev: {
        options: {
            browserifyOptions: {
                debug: true
            }
        },
        src: '<%= paths.src %>/main.js',
        dest: '<%= paths.dist %>/js/bundle.js'
    },
    prod: {
        options: {
            browserifyOptions: {
                debug: false
            }
        },
        src: '<%= paths.src %>/main.js',
        dest: '<%= paths.dist %>/js/bundle.js'
    }
},

Ressources