Tutoriel Vidéo JavaScript Les modules Bundler

Télécharger la vidéo

Lorsque l'on travaille sur du JavaScript aujourd'hui il est devenu quasiment nécessaire d'utiliser un module bundler. En revanche, les outils ne cessent de se multiplier et il peut être difficile de faire son choix, c'est pourquoi je vous propose de comparer les outils les plus populaires actuellement.

Résumé

  Browserify Webpack Rollup Fuse-box Parcel
Objectif Convertir du code NodeJS (require) en code exploitable par les navigateurs Permettre d'importer n'importe quel type de fichier depuis le fichier d'entré (en supportant la syntaxe ES2015). Permettre le Code Splitting et le HotReload. Obtenir un fichier de sortie le plus petit possible en utilisant le tree shaking et en sortant le code "à plat" Mettre le TypeScript au premier plan et utiliser TypeScript comme brique principal pour interpréter le code Simplifier la configuration en élimininant le besoin de configuration
Fonctionnalités supportées
Syntaxe ES2015 Le code doit être convertit en ES5 avant d'être utilisé dans browserify Oui Oui Oui Oui
Gère le CSS Via un plugin Via un plugin Via un plugin Oui Oui
Serveur web interne Non Oui Via un plugin Oui Oui
Hot Reload Non Oui Non Oui Oui
Code splitting Non Oui Non  Oui Oui
Tree shaking Non Via Uglify  Oui Via Quantum Non
Source map Oui Oui Oui Oui Oui

Bundler vs Task Runner

Avant de nous attarder sur les différents outils je pense qu'il est intéréssant de faire la distinction entre un module bundler et un task runner (comme gulp, grunt...).

Un task runner a pour but d'effectuer une série de tâches sur un ou plusieurs fichiers (minification des images, conversion d'un fichier SASS en CSS...) et n'inclue aucune logique. Son seul but est de lancer des outils spécifiques suivant certaines conditions.

Un Module Bundler est un outil plus spécifique (qui peut d'ailleurs être utilisé au sein d'un task runner) qui cible en premier lieu les fichiers JavaScript et qui a pour but de fusionner un fichier et ses différentes dépendances. L'objectif est de n'avoir qu'un seul fichier en sortie pour optimiser les temps de chargement d'une application.

Quel bundler choisir ?

Pour résoudre le problème de multiplication des fichiers JavaScript on s'est contenté pendant un moment de concaténer les fichiers. Malheureusement, cette méthode a très rapidement montré ses limites (il faut définir l'ordre des fichiers, vérifier que des variables ne se court-circuite pas...)

Browserify

Browserify lets you require('modules') in the browser by bundling up all of your dependencies.

Browserify se base sur l'approche apportée par NodeJS (require()) et permet aussi de charger des modules provenant de npm dans le code. On va lui spécifier un fichier en entrée et il va se charger du reste :

let fs = require("fs")
let browserify = require('browserify')
let babelify = require('babelify')
let vueify = require('vueify')

browserify('./src/app.js')
  .transform(babelify)
  .transform(vueify)
  .bundle()
  .pipe(fs.createWriteStream("dist/bundle.js"))

Il est possible de lui donner en plus une liste de transformations à effectuer pour supporter plus de syntaxes (comme la syntaxe ES2015, ou d'autres langages).

Webpack

webpack is a static module bundler for modern JavaScript applications.

Webpack pousse le système un peu plus loin en permettant le chargement de n'importe quel type de fichier au travers d'un système de loaders qui permet de définir comment transformer chaque syntaxe. Cette méthode permet d'avoir un contrôle total sur le processus mais entraîne aussi une configuration plus complexe à définir.

const path = require('path')
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  entry: ['./src/app.js'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/dist/'
  },
  module: {
    rules: [
      {
        test: /\.scss/,
        loader:  ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader']
        })
      }, {
        test: /\.js$/,
        loader: 'babel-loader'
      }, {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("bundle.css")
  ]
}

La version 2 de webpack a ajouté le support de la syntaxe ES2015 pour la gestion des modules ce qui permet de gérer les dépendances sans devoir convertir le code au préalable (cela permet aussi d'émettre du code ES2015 si on le souhaite).

Webpack a aussi introduit le principe du "Code Splitting" qui permet de séparer le code en plusieurs fichiers pour permettre de charger de manière asynchrone certaines librairies.

return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
  // Lodash est chargé ici
}).catch(error => 'An error occurred while loading the component')

Enfin, il offre aussi une API pour gérer le HMR (Hot Module Reload) qui permet de recharger un module "à chaud" sans avoir besoin de réactualiser la page. C'est une fonctionnalité qui est très utilisée avec les frameworks front-end (vuejs, react, angular...) et qui permet de voir les changements sans avoir besoin de réactualiser la page. En revanche cela demande encore plus de configuration et la mise en place d'un serveur de développement spécifique au travers de Webpack Dev Server.

Rollup

Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application

Rollup est un bundler un peu différent des autres car il se focalise avant tout sur le code généré.

La plupart des autres bundler prennent le code des différents modules et le mette dans une fonction. Ils créent ensuite un bundle qui implémente un système de chargement pour ces différents module. A l'éxécution, chacune de ses fonctions va être éxécutée pour construire la liste des module disponibles. Cette manière de faire les choses permet de supporter des fonctionnalité spécifiques comme le Code Splitting ou le Hot Module Reload mais a un coût en terme d'éxécution et de poid de fichier.

Rollup approche le problème différemment en générant le code "à plat" :

// Code de base
import hello from './hello'
console.log(hello())

// code généré par rollup
(function () {
  'use strict';
  function hello () {
    return 'Hello world';
  }
  console.log(hello());
}());

// Code généré par webpack 
! function(modules) {
    var installedModules = {};

    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) return installedModules[moduleId].exports;
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: !1,
            exports: {}
        };
        return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__), module.l = !0, module.exports
    }
    __webpack_require__.m = modules, __webpack_require__.c = installedModules, __webpack_require__.d = function(exports, name, getter) {
        __webpack_require__.o(exports, name) || Object.defineProperty(exports, name, {
            configurable: !1,
            enumerable: !0,
            get: getter
        })
    }, __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ? function() {
            return module.default
        } : function() {
            return module
        };
        return __webpack_require__.d(getter, "a", getter), getter
    }, __webpack_require__.o = function(object, property) {
        return Object.prototype.hasOwnProperty.call(object, property)
    }, __webpack_require__.p = "/dist/", __webpack_require__(__webpack_require__.s = 0)
}([function(module, exports, __webpack_require__) {
    module.exports = __webpack_require__(1)
}, function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", {
        value: !0
    });
    var __WEBPACK_IMPORTED_MODULE_0__hello__ = __webpack_require__(2);
    console.log(Object(__WEBPACK_IMPORTED_MODULE_0__hello__.a)())
}, function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    __webpack_exports__.a = function() {
        return "Hello world"
    }
}]);

Il supporte aussi par défaut le Tree Shaking ce qui permet d'alléger encore plus le code en n'incluant que le strict minimum.

import {chunk} from './utils'
// Rollup n'importera que la fonction chunk au lieu d'importer tout le module ./utils

Au niveau de la configuration, le fonctionnement est assez proche de celui de browserify avec un point d'entrée et une série de plugins pour les transformations. On notera par contre la possibilité de choisir le format de sortie pour permettre l'export de librairies par exemple.

import vue from 'rollup-plugin-vue'
import scss from 'rollup-plugin-scss'
import babel from 'rollup-plugin-babel';
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';

export default {
  input: 'src/app.js',
  output: {
    file: 'dist/bundle.js',
    format: 'iife'
  },
  plugins: [
    vue({styleToImports: true }),
    resolve(), // support des modules node
    replace({'process.env.NODE_ENV': JSON.stringify( 'production' )}),
    scss({output: 'dist/bundle.css'}),
    babel({exclude: 'node_modules/**'}),
  ]
};

L'absence du support du Code Splitting ou le Hot Module Reload rend rollup moins intéréssant pour bundler le code d'une application. En revanche, son support du Tree Shaking et la possibilité de choisir différents formats de sortie en fait un outil plus intéréssant pour publier le code d'une librairie (car on n'inclurera pas le code d'un loader comme avec webpack) et on réduira au maximum la taille du fichier final.

FuseBox

FuseBox is a blazing fast bundler/module loader, where you measure you build time in millseconds.

Fuse-box est un bundler qui se focalise principalement sur les temps de build et sur le support du TypeScript avec un minimum de configuration. Il intègre aussi de nombreaux outils par défaut pour gérer la plupart des cas que l'on peut rencontrer (il intègre même un outil pour les tests unitaires) et il n'est donc pas nécessaire d'installer une série de plugins avant de pouvoir commencer.

const { FuseBox, WebIndexPlugin,  SassPlugin, CSSPlugin, VueComponentPlugin, CSSResourcePlugin } = require("fuse-box");
const isDev = process.env.NODE_ENV === 'development'

const fuse = FuseBox.init({
  homeDir : "src",
  target : 'browser@es5',
  output : "dist/$name.js",
  plugins : [
    // VueJS complique toujours un peu les builds avec le support du style :(
    VueComponentPlugin({
      style: [SassPlugin({ inject: true }), CSSPlugin({ group: 'app.css' })]
    }),
    // Gestion du CSS (via SASS)
    [
      SassPlugin({ inject: true }),
      CSSPlugin({ group: "app.css", outFile: `dist/bundle.css` }),
    ],
    // Gestion de la page html
    WebIndexPlugin({
      template: './index.html',
      appendBundles: true
    }),
  ]
})
if (isDev) {
    fuse.dev();
}
let bundle = fuse.bundle("bundle").instructions(" > app.js")
if (isDev) {
    bundle.hmr().watch()
}
fuse.run();

Fuse-box est en revanche assez récent (il a gagné en popularité en début d'année 2017) et n'a pas encore la communauté de Webpack, mais son support du Typescript par défaut en fait un choix intéréssant si vous utilisez cette syntaxe.

Parcel

Blazing fast, zero configuration web application bundler

Parcel est un projet qui est encore tout récent (Décembre 2017) mais qui propose une approche assez originale pour mériter sa place ici. L'objectif est de proposer des temps de builds plus rapide gràce à la parallélisation mais surtout de retirer la configuration en se basant sur un fichier HTML comme point d'entrée.

<!DOCTYPE html>
<html>
<head>
  <title>Title</title>
  <link rel="stylesheet" href="./src/app.scss">
</head>
<body>
<div id="app"></div>
<script src="./src/app.js"></script>
</body>
</html>

Il va automatiquement détecter les types de fichiers utilisés et télécharger les dépendances nécessaires à leurs traitements. Si votre manière de travailler correspond aux options proposées par défaut, Parcel peut être redoutablement efficace. En revanche, si vous rencontrez un cas particulier, l'absence de configuration peut devenir un réel problème.

Pour résumer ?

Si je devais résumer en une ligne ces outils :

  • Browserify est l'outil qui a ouvert la voix mais qui est maintenant un peu dépassé par la nouvelle génération.
  • Webpack est l'outil qui est le plus configurable mais qui demande aussi un certain temps d'apprentissage avant de pouvoir avoir quelquechose de concret.
  • Rollup est intéréssant mais c'est un outil que je réserverais dans le cadre de la publication d'une librairie.
  • Fuse-box pour les amoureux du TypeScript.
  • Parcel pour ceux qui veulent que ça marche sans se prendre la tête et qui peuvent se contenter du système par défaut.