Modules &
packagers JS

Petit historique et remise en perspective

Par :

Slides :
https://modulom.github.io/xke-js-modules-packagers/

Au temps des dinosaures...qu’est ce qu’était un module js ?

  • D’abord de simples fonctions sont déclarées et deviennent accessibles sur window
    
    function validateForm() { };
                
  • Puis on utilise des Immediately-Invoked Function Expression (IIFE) et le fameux "module pattern"
    
    (function () {
      var app = {};
      app.validateForm = { };
      return app;
    })()
                
  • On peut en suite enregistrer des modules dans un name space
    
    var APP = APP || {};
    (function (APP) {
      APP.validateForm = function () { };
      return APP;
    })(APP)
                
Un peu de lecture sur le module pattern

Pour gérer les modules & les dépendances, il y a eu quelques tâtonnements

Tout dans le html ..!


<link rel="stylesheet" href="css/bootstrap.min.css"/>
<link rel="stylesheet" href="css/jquery-ui.min.css"/>
<link rel="stylesheet" href="css/jquery-ui.structure.min.css"/>
<link rel="stylesheet" href="css/jquery-ui.theme.min.css"/>
<link rel="stylesheet" href="css/font-awesome.min.css"/>
<link rel="stylesheet" href="css/app.css"/>
<script charset="utf-8" src="js/d3.min.js"></script>
<script charset="utf-8" src="js/jquery-2.1.1.js"></script>
<script charset="utf-8" src="js/jquery-ui.min.js"></script>
<script charset="utf-8" src="js/bootstrap.min.js"></script>
<script charset="utf-8" src="js/app.js"></script>
      
  • De manière synchrone, dans <head>, l’utilisateur doit attendre le chargement de tous les scripts.
  • Ou en asynchrone, en fin de <body>, les scripts se chargent en même temps que le dom s’exécute.
  • Pas de concaténation.
  • Pas de minification.
  • Gestion des dépendances faite dans le html.

Génération de bundles avec un task runner (Grunt, Gulp...)


gulp.task('bundle:js', function() {
  return gulp.src(['src/*.js', 'lib/*.js'])
    .pipe(concat('app.js'))
    .pipe(uglify())
    .pipe(gulp.dest('dist'));
});
      

Super, le code est concaténé, minifié, zippé

Mais qu’en est-il des dépendances entres modules ?

Le task runner devient garant des dépendances !


// L'ordre est important !
gulp.task('bundle:js', function() {
  return gulp.src([
    'node_modules/myproject-common/public/js/*.js',
    'node_modules/myproject-common/shared/*.js',
    'shared/*.js',
    'public/utils/**/*.js',
    'public/modules/core/**/*.js',
    'public/modules/pages/**/*.js',
    'public/modules/templates/**/*.js',
    'public/modules/styleguide/scripts/!(main).js'
    'public/angular-config.js',
    'public/angular-application.js',
    'public/modules/markets/*.js',
    'public/components/dropdown/*.js',
    'public/components/popin/*.js',
    'public/components/**/*.js',
    'public/templates/**/*.js',
    'public/pages/**/*.js',
    'public/modules/markets/*[!tests]*/*.js'
  ])
  .pipe(concat('app.js'))
  .pipe(uglify())
  .pipe(gulp.dest('dist'));
});
      

La solution : gérer les dépendances dans le code


Mais c’est ce qu’on fait en back depuis toujours, non ?


Et oui !

Le début de la guerre des modules JS

Deux challengers s’affrontent

AMD (Asynchronous Module Definition)

  • Une function unique define accessible comme variable globale permet de définir un module.
  • Les modules sont chargés de manière asynchrone, c’est la grande force d’AMD.

//define(id?, dependencies?, factory);

// moduleA.js avec une dépendance au moduleB
define('moduleA', ['moduleB'], function (moduleB) {
  moduleB.validateForm();
});
// moduleB.js
define({
  validateForm: function () { }
});
        
La page github sur AMD

CommonJS

  • Une fonction require qui permet l’import d’un module dans le scope.
  • L’objet module, qui permet l’export dans le scope courant.
  • La simplicité de la syntaxe et le fait que nodeJS est retenu une solution approchante l’a rendu très populaire.

// moduleA.js avec une dépendance au moduleB
var moduleB = require('moduleB');
module.exports = function() {
  moduleB.validateForm();
}
// moduleB.js
module.exports = {
  validateForm: function () { }
};
        
Plus de détail sur CommonJS

Les deux communautés sont très actives et chacune défend ses avantages

Une tentative d’unification

UMD (Universal Module Definition)

  • Un (des) wrapper(s) qui permet(tent) d’utiliser tous les modules quelque soit leur format en l'exportant de manière adapté au contexte
  • Assez verbeux et rendu publique très peu de temps avant les propositions des modules ES 2015, il est peu utilisé.

// moduleA.js
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['moduleB'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // Node. Does not work with strict CommonJS
    module.exports = factory(require('moduleB'));
  } else {
    // Browser globals (root is window)
    root.returnExports = factory(root.moduleB);
  }
}(this, function (moduleB) {
  moduleB.validateForm();
}));
        
UMD, pour tous les liés

The Howly Grail?

Module ES 2015

  • Un standard aux nombreuses possibilités !
  • Utilisable dès maintenant grâce à Babel

export { nom1, nom2, …, nomN };
export { variable1 as nom1, variable2 as nom2, …, nomN };
export let nom1, nom2, …, nomN; // fonctionne également avec var
export let nom1 = …, nom2 = …, …, nomN; // également avec var, const

export default expression;
export default function (…) { … } // également avec class, function*
export default function nom1(…) { … } // également avec class, function*
export { nom1 as default, … };

export * from …;
export { nom1, nom2, …, nomN } from …;
export { import1 as nom1, import2 as nom2, …, nomN } from …;
        
Référence sur les exports ES 2015

Module ES 2015


import nom from "nom-module";
import * as nom from "nom-module";
import { membre } from "nom-module";
import {membre as alias } from "nom-module";
import { membre1 , membre2 } from "nom-module";
import { membre1 , membre2 as alias2 , [...] } from "nom-module";
import membreDéfaut , { member [ , [...] ] } from "nom-module";
import membreDéfaut, * as alias from "nom-module";
import membreDéfaut from "nom-module";
        
Référence sur les imports ES 2015

Côté outils de gestions de dépendances -
(packager)

Require JS


// dans scripts/main.js
requirejs(['app'], function(app) {
  //
}

// dans scripts/app.js
// requière moduleA comme dépendance
define(['moduleA', function(moduleA) {
    return function () {
      //
    };
  }
);
      

<script data-main="scripts/main" src="lib/require.js"></script>
        

+ ou -

Avantages :

  • modules asynchrones
  • lazy loading

Inconvénients :

  • un fichier par module
  • syntaxe un peu lourde (ça resemble à un système d’injection connue non ?)

Browserify


// dans scripts/main.js
// requière moduleA comme dépendance
var moduleA = require('scripts/moduleA.js');
function () {
  moduleA.valideForm();
  //
}

// dans scripts/moduleA.js
module.exports = function () { }
        

$ browserify main.js > bundle.js
        

<script src="bundle.js"></script>
        

Brunch

C’est l’oublié des outils de build, il est pourtant très performant.
Brunch, build incrémental, rapide, facile

+ ou -

Avantages :

  • syntaxe très simple
  • bundle et minifie tous les fichiers en un (ou plus) avec un simple script

Inconvénients :

  • ajoute une phase de build
  • pas de lazy loading

Et le "futur" de la gestion des dépendances ?...

  • JSPM
  • Webpack
  • Rollup
  • ...

Merci !