Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les architectures de modules en JavaScript

Poster un commentaire

Les bonnes pratiques modernes de codage en JavaScript poussent de plus en plus vers la structuration du code en modules. En général on débute avec les modules est via certains patterns comme Revealling Pattern Module (RPM), mais il est important de connaître d’autres architectures plus performantes.

Les 4 plus fréquentes sont les suivants:

  1. Revealling Pattern module (UMD)
  2. CommonJS
  3. AMD
  4. Modules ES6

 

RPM est pour l’essentiel l’exploitation des capacités de closure proposées par les fonctions Javascript tandis que les modules ES6 sont des extensions du langage… mais des extensions assez récentes donc pas toujours supportées. On les trouvent mises en œuvre dans TypeScript par exemple.

 

CommonJS et AMD c’est différent, il s’agit de spécifications. Pour les mettre en œuvre on doit utiliser un framework capable de gérer des modules au format CommonJS ou AMD. Différents chargeur de script existent à cet effet, mais certains sont plus typiques que d’autres.

CommonJS est étroitement lié à NodeJS par exemple, tandis que vous profiterez des joies de AMD avec RequireJS ou Angular 1. Mais cela ne se traduit pas par des barrières pour autant. Un chargeur de scripts est après tout lui-même un module.

Par ailleurs, les chargeurs de scripts sont souvent capables de prendre en charge plusieurs formats.

 

Mais pourquoi plusieurs définitions de modules?

C’est une question de choix personnel et les besoins ne sont pas forcément les même d’ailleurs. Les différents formats de modules ne correspondent pas à des bonnes pratiques ou des patterns en tant que tels. Il s’agit simplement d’implémentations différentes de l’idée de module.

Par exemple CommonJS charge les dépendances de façon synchrone ce qui n’est pas une option favorable dans un contexte Web où l’on préfèrera AMD ou ES6. ES6 n’est pas supporté par tous les navigateurs par contre.

 

Je propose de présenter une démonstration complète de ces 3 architectures dans cet article. Au menu:

  • Préparation de l’environnement de test
    Projet de test
    Serveur web
  • CommonJS
    Démo Node
    Démo Web avec SystemJS
  • AMD
    Démo Node avec RequireJS
    Démo Web
  • ES6

Préparation de l’environnement de test

 

Création d’un projet

On aura besoin de NodeJS pour exécuter le code serveur. On peut télécharger ici:

https://nodejs.org/en/download/

NodeJS installe également npm qui est le gestionnaire de package typique associé à cet environnement (le Nuget de NodeJS).

On créé ensuite un répertoire de travail dans lequel on ouvre une console.

Pour finir on initialise un environnement en lançant cette commande:

npm init

 

On devrait voir apparaître un fichier package.json que nous remplirons plus tard avec ses dépendances. Selon le projet des appels supplémentaires à npm install seront nécessaires.

Installer un serveur web

On aura besoin d’un mini serveur NodeJS:

npm live-server -g

Live-server dispose d’un service de Watch qui détecte toutes modification du projet et recharge la page en cours.

Et lancez dans le répertoire du projet:

live-server

La page HTML du projet s’ouvre dans votre navigateur par défaut.

Revealling Pattern Module ou UMD (Universal Module Definition)

Les choses vont être brèves puisque j’ai déjà parlé de ce pattern dans un article précédent:

https://amethyste16.wordpress.com/2016/03/01/le-pattern-module-en-javascript/

Le principal avantage de ce pattern est qu’il n’y a pas besoin d’outillage pour le mettre en œuvre.

Retenez aussi que le pattern se présente en deux versions:

  1. Singleton
  2. Constructeur

 

Son principal inconvénient est qu’il gère assez mal les dépendances. C’est un problème que va résoudre les autres formats de module.

CommonJS

Architecture

CommonJS est une spécification de module et de package en JavaScript dont le site officiel est :

http://www.commonjs.org/

Mais il est très pauvre en renseignement.

 

CommonJS définit un module comme un bloc de code avec une déclaration explicite de ce qui est exporté vers les dépendances. Il suffit de rattacher une déclaration à la variable exports. Cette variable contient les objets qu’un module souhaite rendre public.

L’import du module s’effectue à l’aide de la fonction require(). Require charge le module dont l’uri lui est fournit et retourne une instance réutilisable de la partie publique.

Typiquement on rencontrera le schéma de code suivant:

 

Définition d’un module dans le fichier monModule.js:


exports.add= function(a,b) {

return a+b;

}

 

Et l’import:


var monModule=require("monModule.js");

var result = monModule.add(5,3);

 

la fonction require() charge un module et renvoi une instance vers le module. Si le module est déjà chargé il n’est pas rechargé une deuxième fois.

 

Note: Require est une méthode synchrone ce qui est un inconvénient pour du code côté client. De fait CommonJS est plus volontiers implémenté dans des frameworks serveurs.

Démonstration NodeJS

Voyons un exemple concret maintenant afin d’explorer la façon dont sont chargées les dépendances. On va commencer par un exemple « hello world » avec NodeJS.

On construit un fichier ‘mathModule.js’:


var add = function(a,b) {
   return a+b;
}

exports.add =add;

Il s’agit, comme vous le constatez, de la définition d’un module qui exporte une méthode unique: add().

 

Et le fichier de test code.js:


var mathModule = require('./mathModule');

var resultat= mathModule.add(4,5);
console.log(resultat);

 

Note: l’extension .js dans le nom du fichier est facultative.

On le lance en ligne de commande:

Le résultat (9) est affiché démontrant le bon fonctionnement du module.

Et si mathModule référence lui aussi un module, par exemple displayModule.js:

 


exports.display = function(valeur) {
   return "Le resultat de l'operation est: " + valeur;
}

Une fonction display() qui reformate la sortie.

On modifie mathModule:


var displayModule = require('./displayModule');

var add = function(a,b) {
   return displayModule.display(a+b);
}

exports.add =add;

Et le résultat est celui espéré:

Rien de très compliqué.

Démonstration projet Web

Il est possible de faire fonctionner des modules au format CommonJS dans le web. Pour la démo on va utiliser le chargeur de scripts SystemJS.

On créée donc un projet comme précédemment. On complète en chargeant les packages:

npm install systemjs –save

On va créer un projet identique à celui qui servira plus loin pour la démo AMD, mais les modules seront au format AMD cette fois.

Le module display.js:


function display(msg) {
   return "Résultat du calcul: " + msg;
}

exports.display=display;

Le module mathModule.js:


var display = require('displayModule.js');

function add(a,b) {
   var resultat = a+b;
   return display.display(resultat);
}

exports.add = add;

Notez la dépendance avec le module display.js.

Et pour finir le module qui se charge de faire le lien avec le dom:


var math=require('mathModule.js');

function ajouter() {
   var r = math.add(a.value,b.value);
   resultat.innerText = r;
}

var a = document.getElementById("a");
var b = document.getElementById("b");
var resultat = document.getElementById("resultat");
var bouton = document.getElementById("calc");
bouton.onclick =ajouter;

 

Ce module est particulier car il n’exporte rien. Il existe juste pour lier l’évènement click à une fonction Javascript.

Il nous faut maintenant une page HTML que voici:


<!DOCTYPE html>
<html>
<head>
<script src=".\node_modules\systemjs/dist/system.js"></script>
<script>
System.config({
   meta:{
      format:'cjs'
   }
});

System.import('domModule.js');
</script>

</head>
<body>
A <input id="a" value="4" /><br/>
B <input id="b" value="5" /><br/>
Résultat <div id="resultat"></div><br/><br>
<input type="button" id="calc" value="Add" />
</body>
</html>

 

Examinons le code. On ajoute deux éléments <script>.

Le premier charge SystemJS. Le suivant apporte de la configuration à SystemJS. La configuration est injectée avec la fonction config() de l’objet System définit dans le SDK SystemJS.
Elle nous permet de définit une propriété meta appelée format à la valeur ‘cjs’. Cela indique à SystemJS qu’il devra charger des modules au format CommonJS. SystemJS peut en effet charger des modules selon différents format.

 

Pour tester on lance cette commande depuis le répertoire du projet:

live-server

Il s’ouvre le formulaire:

Je clique sur ADD et constate que tout baigne:

 

AMD (Asychronous Module Definition)

Architecture

Le A de AMD indique que le chargement des modules et dépendances est fait de façon asynchrone ce qui est une nette amélioration par rapport à CommonJS.

https://github.com/amdjs/amdjs-api

 

La spécification réclame juste une fonction ayant cette signature:

define(id?, dependencies?, factory);

Les deux premiers paramètres sont facultatifs.

La fonction peut porter des noms variés, RequireJS utilise define().

  • id
    Identifiant facultatif du module
  • dependencies
    un tableau contenant les uri vers les dépendances.
    Le préfixe .js est facultatif
    Le tableau peut être vide
    Les dépendance sont chargées de façon asynchrone
    Le module n’est construit que si les dépendances ont été chargées
  • factory
    Une fonction, un objet qui construit le module. Une déclaration aussi simple que:
    define(42);Est un module valide, reste à lui trouver un intérêt…
    S’il y a des dépendances, on les injecte dans factory

RequireJS est un des chargeurs de module AMD le plus utilisé, mais il y en a d’autre comme Dojo ou Angular 1.

Nous reconnaissons là des syntaxes déjà croisées dans Angular 1.

 

Note: AMD est en fait un ensemble de spécifications JavaScript dont une partie définit un modèle de description des modules.

 

Démonstration NodeJS

AMD est une spécification typique pour les applications web, mais on peut aussi s’en servir côté serveur, la preuve en démo au sein d’une application NodeJS:

On reconstruit un projet comme indiqué précédemment, mais cette fois on importe le package RequireJS:

npm install requirejs –save

 

Note: on (en tout cas moi) oublie souvent le –save. C’est dommage car du coup package.json ne se met pas à jour et on a des surprises lors du déploiement.

 

On va essayer de refaire l’exemple précédent en AMD, construisons monAppli.js:


var a=4;
var b=5;

var requirejs = require('requirejs');

requirejs([],
function() {
   function add(a,b) {
      return a+b;
   };

   var valeur=add(a,b);   
   console.log(valeur);
});

On commence par charger le module RequireJS. On reconnaît ensuite une syntaxe typiquement AMD. Si je lance:

Ca marche donc.

Il est également possible de charger en syntaxe AMD un module CommonJS avec le package AdmRequire:

http://arqex.com/874/reusing-require-js-modules-node-js

 

Note perso: Je ne sais pas ce que vous en pensez, mais l’exemple qui précède fonctionne certes, mais fait très artificiel. La raison est que la syntaxe AMD n’est réellement intéressante que sur des applis web. Les consoles, ce n’est pas le milieu naturel d’AMD.

Démonstration projet Web

Examinons un exemple Web cette fois. On va reprendre le projet Web construit en architecture CommonsJS, mais les modules seront portés au format AMD.

On commence par créer un module displayModule.js:

define(function() {
      function display(msg) {
      return "Résultat du calcul: " + msg;
   }
 
   return {
      display:display
   }
});

 

On reconnaît là un module bien connu. Le deuxième module est mathModule.js:


define(['displayModule'], function(display) {
      function add(a,b) {
         var resultat = a+b;
         return display.display(resultat);
      }

   return {
     add:add
   }
});

Cette fois on a une dépendance que l’on injecte dans le module.

Il nous reste plus qu’à construire une page index.html:


<!DOCTYPE html>
<html>
<head>
<script data-main="./domModule.js" src=".\node_modules\requirejs/require.js"></script>

&nbsp;

</head>
<body>
   A <input id="a" value="4" /><br/>
   B <input id="b" value="5" /><br/>
   Résultat <div id="resultat"></div><br/><br>
   <input type="button" id="calc" value="Add" />
</body>
</html>

 

On charge RequireJS avec la balise <script>. Vous remarquez tout de suite l’attribut data-main. Cet attribut indique à RequireJS de charger le script fournit. Le script en question est un module lui aussi, chargé de faire la liaison avec le DOM:

 


define(['mathModule'], function(math) {

&nbsp;

function ajouter() {
      var r = math.add(a.value,b.value);
         resultat.innerText = r;
      }

      var a = document.getElementById("a");
      var b = document.getElementById("b");
      var resultat = document.getElementById("resultat");
      var bouton = document.getElementById("calc");
      bouton.onclick =ajouter;
   }

);

 

 

Et lancez dans le répertoire du projet:

live-server

Vous verrez ceci apparaître:

Cliquez sur Add:

 

Modules ES6

J’ai déjà abordé la question dans cet article:

https://amethyste16.wordpress.com/2016/03/01/le-pattern-module-en-javascript/

 

Bibliographie

 

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s