JavaScript, les bons éléments, façon 2020 : Modules

Le cas Node.js

Revenons à notre année 2009. On l’a vu dans la partie précédente, JavaScript ne disposait pas de modules à cette époque.

Pourquoi elle ? Déjà, c’est la sortie de l’ES5, standardisation de l’objet global JSON, méthodes .map() et .filter() sur les tableaux, String.trim(), strict mode, getters et setters… Le JavaScript devient un langage plus puissant. 2009, c’est juste après la sortie de Google Chrome, équipé d’un moteur JavaScript dérivé de celui de Safari, nommé V8.

Ce moteur, libre, va servir de base à un projet devenu énorme : Node.js. Le projet surprend de prime abord : On veut porter hors du navigateur un langage encore peu puissant, quasi dépourvu de librarie standard (à part Math et le ridicule Date), et incapable d’assurer des tâches classiques, notamment les I/O.

Pour construire cette librairie, il a été choisi, à la manière de Python, de ne pas ajouter une pléthore de fonctions globales, mais de proposer des modules : des fichiers indépendants pouvant être chargés à la demande.

Un standard est créé pour ceux-ci, le modèle CommonJS. La syntaxe est très simpliste et rejoint ce qui existe déjà en JavaScript, un appel de fonction, dont le résultat est stocké dans une variable.

// L'import du module fs via CommonJS, à la sortie de Node.js
var fs = require('fs');

Si cela paraît simple, c’est assez déroutant pour JavaScript : l’appel du chargement du module est synchrone, il interrompt l’exécution du code jusqu’à que celui-ci soit chargé en mémoire. Pour Node.js, s’exécutant sur serveur, cela est moins important (et encore) mais cela laisse déjà présager l’impossibilité de porter CommonJS sur navigateur.

De plus, dans Node, chaque fichier est un module : ses fonctions et variables ne sont pas visibles par tous les autres fichiers, et on va décider ce qu’on exporte ou non. Pour cela, Node expose un objet module accessible sur l’objet global (qui en prime, n’est pas window mais global, histoire de bien segmenter navigateur et Node).

// my_module.js : L'export de variables et fonctions en CommonJS
var hello = 'world';

function sayHello() {
  console.log('hello ' + hello);
}

// On exporte que la fonction sayHello (sous le même nom, la clé de l'objet étant le nom sous laquelle la variable est exportée), et non pas la variable 'hello'
module.exports = {
  sayHello: sayHello
};
// index.js : L'import d'un module utilisateur (notez l'absence de .js)
var MyModule = require('./my_module');

// Appel de la fonction exportée
MyModule.sayHello();

// hello n'est pas exporté
typeof MyModule.hello === 'undefined'; // true

Avec Node.js, on peut installer des paquets via npm, le package manager. L’import de module installés via npm se fait en précisant le nom du module sans préfixe de chemin, c’est à dire sans aucun ., ou autres /.

Node saura que l’on fait référence à un module nommé plutôt qu’à un fichier local.

// Après un `npm install uuid`, il est possible de faire :
var uuid = require('uuid');

Le cas ECMA

État de l’art

Si Node.js a permis de légitimer JavaScript comme langage "sérieux", son système de module n’était pas portable et il a fallu standardiser autrement ce qui aurait dû dès le départ être le coeur du langage.

L’ECMA (l’organisation qui édite le standard définissant l’ECMAScript, nom formel du JS) a planché sur une norme complète pour importer et exporter des variables, fonctions ou même d’autres modules.

La source d’inspiration est relativement claire, elle s’approche de ce qu’est capable de faire Python, une des références en la matière en terme de souplesse d’import, tout en agrémentant ceci d’exports explicites, comme fait Node.js.

# Un exemple de module Python

# Importer tout un module dans une variable
import os

# Importer une partie d'un module dans une variable
from numpy import arange

# Importer un fichier local (src/hello.py)
import src.hello as hello

La conception d’une alternative universelle au CommonJS

En JavaScript, les modules font un peu plus verbeux. La source est contenu dans une string, afin de permettre l’utilisation d’URLs (navigateur friendy, donc).

Par défaut, un fichier n’est pas un module : son contenu (fonctions et variables déclarées) sont dans l’espace global et lisibles par les autres fichiers.

Dès la présence d’un import ou export, alors le fichier devient un module : Les objets déclarés deviennent locaux au module. Si ce comportement est semblable au CommonJS, il requiert la présence d’un des deux mots clés cités pour être activé.

/* Importer l'ensemble d'un module, façon require() : */

// Depuis un chemin relatif
import * as World from './world.js';
// ou depuis un chemin absolu
import * as World from 'https://example.com/js/world.js';

// On accède au contenu du module en utilisant ses propriétés
World.hello();


/* Importer une partie d'un module : */
// Importe uniquement l'objet 'hello' du module
import { hello } from './world.js';

hello();


/* Importer l'import 'default' (voir plus tard l'export) */
import helloWorld from './world.js';

helloWorld();

On constate que les modules prennent systématiquement une URL en paramètre, ce qui diffère du CommonJS. Autre chose, on ne peut importer que des fichiers, pas de raccourcis pour importer des modules installés via package manager (ce qui n’aurait de toute manière pas de sens, sur navigateur il n’y pas de notions de paquets).

L’import est asynchorne et ne doit être fait qu’en début de fichier, à la racine du code, tout autre emplacement est une erreur. Cela permet de lire d’autres fichiers JavaScript pendant que les modules du fichier en cours sont en cours de téléchargement.

Une incompatibilité prévisible d’avance

Les modules CommonJS et ECMA sont incompatibles entre eux : l’un est synchone et l’autre non, l’un ne prend que des URLs l’autre des noms de modules ou des chemins de fichiers, l’un peut être inline et l’autre non…

Node supporte depuis peu les modules ECMA (à condition de ne pas mixer CommonJS et ECMA), modulo un flag lors du démarrage de Node.

L’import de paquets Node se fait à la manière établie par TypeScript il y a quelques temps de cela (dont le compilateur est capable de transpiler les modules ECMA en CommonJS).

// On fait référence au paquet uuid, si ce code est exécuté dans Node
import uuid from 'uuid';

Exporter des choses d’un fichier

L’exportation peut se faire de plusieurs manières : Il faut déjà différencier export nommé de l’export par défaut.

L’export peut se faire à deux moments : lors de la déclaration de la dite-chose exportée ou plus tard dans le fichier.

Reprenons notre exemple de tout à l’heure qui importait un fichier world.js, écrivons le.

// world.js : Export à la déclaration

var world = 'world';

// La fonction hello est exportée de manière nommée,
// permet le `import { hello }`
export function hello() {
  console.log('Hello ' + world);
}

// Elle est exportée par défaut, 
// permet le `import helloWorld`
export default hello;
// world.js : Export après déclaration

var world = 'world';

function hello() {
  console.log('Hello ' + world);
}

// La fonction hello est exportée,
// permet le `import { hello }`
export {
  hello
};

// Elle est exportée par défaut, 
// permet le `import helloWorld`
export default hello;

Un troisième type d’export existe, il permet d’assigner une valeur au module lorsqu’il est importé. Il ne peut y avoir aucun autre export dans le fichier que celui-ci.

// test.js
function test() {
  console.log('hello, 1, 2, 3');
}

export = test;
// C'est équivalent à la version CommonJS
// module.exports = test;


// main.js
// C'est +- comparable à un export par défaut, mais il empêche
// complètement d'exporter autre chose (contrairement à lui)
import test from './test.js';

test();

L’import dynamique asynchrone

J’ai menti quand j’ai dit que l’import des modules n’était possible qu’en début de fichier. En fait, depuis très récemment, il est possible de faire des imports n’importe où.

Cependant, contrairement au CommonJS, ces imports ne sont pas synchrones (essentiellement car ils dépendent d’URLs). Lorsque vous importez un module via la fausse fonction import(), celui-ci sera enveloppé dans une Promise.

On parlera des promesses plus tard, mais sachez néanmoins qu’il est possible d’importer dynamiquement des modules.

export function main() {
  var test_module = import('./test.js');

  test_module.then(function (test) {
    // Le module est chargé et accessible
  });
}

La fin

Je pense que l’on a fait le tour rapide des différentes fonctionnalités offertes par les modules ECMA et pourquoi ils ont été créés.

Allez voir la suite de cette série d’articles sur JavaScript en cliquant ici, on parle de let, const, Map et Set !

Article précédent : IntroductionArticle suivant : let, const, Map et Set

Laisser un commentaire