Les modules en Javascript en 5 min

A l’origine Javascript était utilisé en tant que langage de script pour des pages web. Au fil du temps, ce langage est devenu le langage de base de nombreuses applications web. Jusqu’à très récemment il n’existait pas de concept de module en Javascript à proprement parlé. Par exemple, il était impossible de référencer directement un script Javascript à partir d’un autre. Dans un premier temps, cette limitation a eu pour conséquence de devoir stocker le code Javascript dans un même fichier, ce qui est possible pour de simples scripts mais peut s’avérer très vite compliqué pour des applications de grandes tailles.

Découper du code en plusieurs parties apportent de nombreux avantages par rapport à un code monobloc:

  • Le premier avantage est de rendre le code plus lisible puisqu’on peut le séparer, par exemple, en fonctionnalités.
  • Le code séparé en modules facilite sa réutilisation dans le reste de l’application mais aussi dans d’autres applications.
  • La gestion des dépendances entre modules est simplifiée: chaque module indique clairement ses dépendances. En cas de problème ou de dépendances cassés, on peut identifier plus facilement le ou les modules qui font défaut.
  • Les modules rendent le code plus facilement extensible car on peut plus aisément rajouter un ou plusieurs modules et les utiliser.
  • Enfin, les modules favorisent l’encapsulation pour éviter d’exposer une complexité qui n’est pas nécessaire.

Même si la notion de module n’existait pas nativement en Javascript, il a fallu trouvé un moyen de moduler le code pour le rendre plus facilement lisible. Plusieurs outils sont apparus pour étendre le Javascript et permettre une implémentation facile des modules.

Cet article a pour but de montrer quelques solutions pour implémenter des modules en Javascript:

  • En s’aidant d’outils externes pour les versions de Javascript antérieures à EcmaScript 2015.
  • En profitant de la gestion native des modules à partir d’EcmaScript 2015 (i.e. ES2015 ou ES6).

Dans premier temps, on va présenter quelques caractéristiques historiques du langage Javascript qui justifient une syntaxe particulière pour l’implémentation des modules. On va, ensuite, indiquer des solutions pour implémenter des modules avant ES2015. Pour terminer, on va expliciter la syntaxe désormais native, des modules avec ES2015.

Quelques caractéristiques de la syntaxe Javascript

En préambule, on peut expliquer quelques caractéristiques de la syntaxe Javascript qui ont permis l’implémentation du pattern module avant ES2015.

Portée de variables

La portée des variables est l’élément le plus important pour motiver le découpage en module du code en Javascript. Ainsi les variables déclarées sans le mot-clé var ont une portée globale c’est-à-dire que leur valeur persiste tout au long de la durée de vie de l’application jusqu’à leur prochaine affectation.

Cette portée globale peut entraîner plus facilement des bugs:

  • Si on modifie la valeur d’une variable dans un endroit du code, elle sera persistée et pourra être utilisée à d’autres endroits non identifiés.
  • A l’inverse, la valeur d’une variable peut être modifiée par une partie du code qu’on ne maitrise pas, ce qui peut entrainer une valeur différente de celle attendue.

Pour ces raisons:

  • On déclare les variables pour une portée limitée pour en maitriser la valeur.
  • On déclare ces variables dans le corps d’une fonction avec le mot-clé var pour limiter la portée au corps de la fonction.

Par exemple, dans le code suivant, la portée de la variable globalVar est globale (car il n’y a pas le mot clé var):

function customFunction() { 
    globalVar = 1; // la portée est globale 

    var privateVar = 2; // la portée est locale 
}

A l’inverse, la portée de privateVar est locale à la fonction.

Fonction dans une fonction

Il est possible de déclarer des fonctions dans le corps d’une fonction (i.e. closures). Cette caractéristique sert pour la définition du pattern module plus loin.

Par exemple:

function customFunc1() { 
    function customFunc2() { 
         // ...
    }; 
} 

La fonction customFunc2() est encapsulée dans la fonction customFunc1().

Fonctions anonymes

Les fonctions anonymes ou expressions lambda correspondent à la même notion qu’en C#: on affecte une fonction à une variable. L’intérêt est de pouvoir passer cette fonction en paramètre d’une autre fonction par l’intermédiaire de cette variable.

Par exemple, une fonction anonyme se déclare de cette façon:

var anonymousFunc = function() { 
    // ... 
}; 

anonymousFunc est la variable contenant la fonction. Il faut penser à utiliser le caractère “;” à la fin de la déclaration. On peut appeler cette fonction en utilisant directement la variable:

anonymousFunc();  

L’existence de la fonction est liée à la portée de la variable anonymousFunc. Si cette variable est hors de portée, elle peut être supprimée par le garbage collector.

Le “pattern” module

Ce pattern utilise les notions abordées précédemment pour définir une unité de code avec des variables dont la portée est locale à cette unité. On peut encapsuler une partie du code ou au contraire choisir d’exposer d’autres parties ce qui permet de d’implémenter la notion de module sans qu’elle soit nativement et explicitement prévue par le langage.

Pour implémenter cette notion de module:

  • On encapsule une fonction dans une variable,
  • On définit une variable dans le corps de la fonction et on retourne cette variable en retour de la fonction pour en exporter la valeur.

Par exemple:

var Module = (function() { 
    var self ={}; // On définit une variable vide 

    self.publicVariable = 1; // On ajoute des attributs à cette variable 

    var privateVar = 1; // variable privée 

    // On peut affecter une fonction anonyme 
    self.publicMethod = function() { 
        // ...
    }; 

    return self;  // On exporte la variable pour l'utiliser à l'extérieur par l'intermédiaire de la variable Module 
})(); 

L’utilisation des éléments de syntaxe var Module = (function() { ... })(); est importante car elle permet d’indiquer une fonction qui sera exécutée tout de suite c’est-à-dire une IIFE (pour Immediatly Invoked Function Expression). L’IIFE est exécutée en même temps que le code est parsé donc les déclarations dans le module sont exécutées et dans notre cas, les différentes variables sont définies.

Dans le corps du module, la variable self est la plus importante car elle est retournée à la fin de l’exécution. Ainsi les éléments déclarés avec la syntaxe self.element peuvent être accessibles à l’extérieur du module en utilisant la syntaxe Module.element.

A l’opposé, les éléments déclarés dans le corps du module avec une syntaxe du type suivant, ne sont pas accessibles à l’extérieur:

var privateVar = 1;  

Les variables déclarées dans le corps du module ne sont accessibles qu’aux fonctions du module.

Par exemple, si on écrit:

var Module = (function() { 
    var self = {}; 

    function privateFunc() { 
        // ... 
    }; 

    self.publicFunc = function() { 
        privateFunc(); 
    }; 

    return self; 
})(); 

privateFunc() est une fonction privée accessible seulement dans le corps du module. A l’opposé, self.publicFunc est accessible à l’extérieur en utilisant la syntaxe Module.publicFunc().

Le “pattern” Constructor

Il est possible de définir des modules sans les exécuter en utilisant le pattern module et l’utilisation des IIFE comme on l’a vu précédemment.

Un autre type de déclaration permet d’instancier un module de cette façon:

var moduleInstance = new Module(); 

Pour instancier un module de cette façon, sa déclaration est du type:

var Module = function() { 
    var message = "message1"; // élément de construction 

    function printMessage() { 
        console.log(message); 
    } 

    // On utilise ce type de déclaration pour exporter la fonction printMessage. 
    return { 
        printMessage: printMessage 
    } 
}; 

On peut remarquer qu’on n’utilise pas la syntaxe correspondant aux IIFE.

Pour appeler le module avec ce type de déclaration, il faut utiliser la syntaxe:

moduleInstance.printMessage(); 

Déclaration de plusieurs modules

Il est possible d’utiliser un module dans le corps d’un autre module à condition d’avoir définit les modules dans le bon ordre. Ainsi, pour pouvoir utiliser un module, il faut l’avoir défini au préalable.

Par exemple:

var Module1 = (function() { 
    var self = {}; 

    self.publicFunc = function() { … }; 
    return self; 
})(); 

var Module2 = (function() { 
    var self = {}; 

    self.publicFunc = function() { 
        Module1.publicFunc(); 
    }; 

    return self; 
})(); 

Dans le cas de l’exemple, il faut que le module nommé module1 soit définit avant le module nommé module2.

Sous-modules

On peut imbriquer des modules l’un dans l’autre. D’abord, on définit un module parent, ensuite on peut définir un module enfant en ajoutant un attribut au module parent.

Par exemple:

var ModuleParent = (function() { 
    // ... 
})(); 

ModuleParent.SousModule = (function() { 
    // ...
})(); 

Il faut noter quelques règles concernant ces sous-modules:

  • Les sous-modules ne peuvent accéder qu’aux éléments publiques du module parent.
  • Les éléments privés ne sont accessibles que dans le corps du module.

Module parent

On utilise souvent une variable globale qui contient tous les modules pour éviter de définir des modules et de les écraser par la suite (si on le déclare plusieurs fois). On définit, alors, une variable contenant tous les modules.

Par exemple, si on appelle cette variable Application:

var Application = Application || {}; 

L’écriture précédente signifie qu’on définit une variable vide si elle n’existe pas sinon on utilise la variable existante.

Ensuite on définit les modules en utilisant des attributs de la variable Application:

Application.Module1 = (function(){ 
    // ... 
}){}; 

On peut définir un autre module de cette façon:

Application.Module2 = (function(){ 
    // ... 
}){}; 

Enfin, pour éviter d’écraser des modules s’ils ont déjà été définis, on utilise la notation correspondant à l’extension de module:

Application.Module1 = (function(self){ 
    // ...  
    // Pas de déclaration de self car on l'utilise en argument. 
})(Application.Module1 || {}); 

La notation Application.Module1 || {} signifie que s’il n’existe pas de module nommé Module1 alors on crée une variable vide sinon on utilise l’instance existante.

Implémenter des modules avant ES2015

Comme indiqué en préambule, Javascript ne proposait pas de gestion native des modules avant ES2015. Avant cette version, l’implémentation de modules a donc nécessité de tirer partie de la syntaxe existante de Javascript. Ainsi, le pattern module décrit dans la partie précédente, permet d’implémenter des modules avant ES2015. Le gros inconvénient de ce pattern est qu’il nécessite de déclarer tous les modules dans le même fichier Javascript.

On peut trouver un moyen de charger plusieurs fichiers de scripts Javascript en indiquant ces scripts directement dans l’en-tête du fichier HTML principal en utilisant des balises <scripts>, par exemple:

<script type="text/javascript" src="script1.js" /> 
<script type="text/javascript" src="script2.js" /> 
<script type="text/javascript" src="script3.js" /> 
<script type="text/javascript" src="script4.js" /> 

On peut déclarer les différents scripts en omettant l’attribut type="text/javascript" qui est facultatif.

Les modules déclarés en utilisant cette méthode ont de gros inconvénients:

  • Le code est moins lisible puisqu’il faut indiquer tous les scripts au préalable dans le fichier principal.
  • On ne peut pas charger de fichiers de façon asynchrone, tous les fichiers Javascript doivent être chargés au chargement de la page.
  • Il faut qu’ils soient déclarés dans le bon ordre. Si une dépendance n’est pas satisfaite au moment où on souhaite l’utiliser, le code ne s’exécutera pas correctement.
  • Suivant la façon avec laquelle les modules sont déclarés, ils peuvent être écrasés si, par mégarde, on les définit plusieurs fois.

Un exemple d’implémentation de modules en ES5 se trouve dans le dépôt GitHub suivant: es5-browser. Cet exemple permet d’illustrer des appels d’un module à l’autre:

  • le module 1 appelle le module 2 et
  • le module 2 appelle le module 3.

Dans cet exemple:

  • On déclare tous les scripts dans le fichier index.html.
  • Les modules sont définis dans les fichiers module1.js, module2.js et module3.js.
  • La fonction permettant de démarrer l’exécution se trouve dans le fichier app.js.
  • JQuery est seulement utilisé pour lancer l’exécution dans app.js au chargement de la page HTML principale.

Pour exécuter cet exemple, il faut simplement ouvrir le fichier index.html avec un browser et afficher la console de développement.

Affichage de la console de développement dans un browser

Pour tous les exemples provenant du dépôt GitHub https://github.com/msoft/javascript_modules, pour voir les résultats d’exécution, il faut afficher la console de développement:

  • Sous Firefox: on peut utiliser la raccourci [Ctrl] + [Maj] + [J] (sous MacOS: [⌘] + [Maj] + [J], sous Linux: [Ctrl] + [Maj] + [K]) ou en allant dans le menu “Développement web” ⇒ “Console du navigateur”.
  • Sous Chrome: utiliser le raccourci [F12] (sous MacOS: [⌥] + [⌘] + [I], sous Linux: [Ctrl] + [Maj] + [I]) puis cliquer sur l’onglet “Console”. A partir du menu, il faut aller dans “Plus d’outils” ⇒ “Outils de développement”.
  • Sous EDGE: utiliser le raccourci [F12] puis naviguer jusqu’à l’onglet “Console”.

L’exemple précédent affichera dans la console de développement un résultat du type:

Executed from module3.SubModule.init()
Executed from module3.privateFunc()
Executed from module3.init()
Executed from module2.publicFuncModule2()
Executed from module1.privateFunc()
Executed from module1.publicFuncModule1()

Utiliser des “loaders”

Pour éviter tous les inconvénients liés à la déclaration préalable des scripts contenant les modules dans l’entête de la page HTML principale, on peut passer par l’intermédiaire de loaders qui vont apporter une flexibilité supplémentaire en enrichissant la syntaxe Javascript. Pour faciliter le chargement des modules, loaders se basent sur une syntaxe particulière correspondant à un “format” pour savoir comment charger les modules:

  • Format: correspond à la syntaxe utilisée pour indiquer des modules dans le code Javascript. Cette syntaxe est interprétée par le loader.
  • Loader: il aide à interpréter un format de module spécifique.

Du code Javascript peut être exécuté sur un browser directement ou à partir d’un serveur Node.js. Suivant la façon d’exécuter le code, on peut être amené à utiliser un format particulier et un loader spécifique.

Les formats les plus connus sont CommonJS, AMD (i.e. Asynchronous Module Definition) et UMD (i.e. Universal Module Definition). Les loaders les plus connus sont RequireJS (utlisant le format AMD), SystemJS (qui permet de gérer plusieurs formats).

Utiliser des “bundlers” ou des “transpilers”

En plus des loaders, il est possible d’exécuter des outils pour compiler du code Javascript pour générer un code Javascript capable d’être exécuté dans des environnement différents et plus hétérogènes.

Ces outils peuvent être des bundlers qui vont générer un fichier Javascript unique en incluant toutes les dépendances au moment de la compilation. Le code produit sera compatible ES5 et utilisable sur tous les browsers supportant ES5. Le gros inconvénient des bundlers est qu’ils vont générer un seul fichier contenant tout le code Javascript. Dans le cas d’une grosse application et si l’application comporte beaucoup de code Javascript, le temps de chargement du fichier bundle peut prendre du temps et ralentir le chargement de la page principale.

Les bundlers les plus connus sont Browserify ou webpack.

Une autre solution est de passer par des transpilers qui vont, de même, générer un code Javascript compatible avec ES5 après une étape de compilation. Les transpilers proposent une syntaxe plus étendues que celles des bundlers qui sont plus spécifiques à la gestion des modules.

Les transpilers le plus connus sont Babel, Typescript et CoffeeScript.

Pour utiliser le bundler Browserify, il faut:

  1. Installer Browserify avec npm:
    npm install --save-dev browserify
    
  2. L’exécuter en utilisant le module principal et en dirigant le résultat de la génération dans un répertoire de sortie. Par exemple, en exécutant:
    node_modules/.bin/browserify js/app.js --outfile build/bundle.js 
    

    Le résultat de la génération se trouve dans le fichier build/bundle.js

  3. Pour charger le fichier bundle final à partir de la page principale, il faut ajouter la ligne suivante dans le fichier HTML principal:
    <script src = "./build/bundle.js"></script> 
    

Pour utiliser le transpiler webpack et Babel, on peut réaliser les étapes suivantes:

  1. Installer webpack et Babel avec npm en exécutant:
    npm install --save-dev babel-loader babel-core babel-preset-env webpack
    
  2. Configurer le fichier de configuration de webpack webpack.config.js avec les paramètres suivants:
    var path = require('path'); 
    var webpack = require('webpack');
    
    module.exports = { 
       entry: './src/index.js', // point d'entrée de l'application
       module: { 
        rules: [
          {
              test: /\.js$/,
              loader: 'babel-loader',     // On utilise le loader Babel
              query: {
                  presets: ['es2015']   // Paramétrage de Babel
              }
          }
         ]
       }, 
       resolve: { 
         extensions: [ '.tsx', '.ts', '.js' ] 
       }, 
       output: { 
         filename: 'app.js',      // Fichier de sortie
         path: path.resolve(__dirname, 'public')  // Répertoire de sortie
       } 
    };
    
  3. Ajouter la ligne suivante dans le fichier HTML principal:
    <script type="text/javascript" src="public/app.js"></script> 
    

Un exemple plus complet se trouve dans le dépôt GitHub: es6_webpack_babel. Cet exemple permet d’illustrer l’utilisation de webpack et du transpiler Babel pour générer un bundle compatible ES5 à partir du code Javascript ES6/ES2015. Cet exemple permet d’effectuer des appels d’un module à l’autre.

Pour exécuter cet exemple, il faut:

  1. Télécharger les packages npm en exécutant la ligne: npm install dans le répertoire de l’exemple,
  2. Exécuter le commande npm run build pour que webpack génère le bundle,
  3. Lancer le serveur web de développement en exécutant la commande npm run start
  4. Se connecter à l’adresse http://127.0.0.1:8080 pour voir le résultat de l’exécution après avoir ouvert la console de développement.

Le résultat est très similaire à celui obtenu avec les modules ES5.

Synthèse des possibilités

Suivant le cas d’utilisation et la façon d’exécuter le code Javascript, on peut utiliser un format particulier ce qui implique ensuite un bundler et/ou un transpiler particulier.

On peut synthétiser les différentes possibilités dans le tableau suivant:

Format CommonJS AMD UMD
Environnement d’exécution Principalement sur un serveur Node.js mais il est possible de l’exécuter directement sur un browser avec des bundlers. Browser Serveur et browser
Outils utilisables
  • Avec Node.js, il n’est pas nécessaire d’utiliser un loader, CommonJS est géré nativement.
  • Directement sur un browser, on peut utiliser Browserify ou webpack pour générer un bundle.
  • SystemJS en tant que loader de modules
  • RequireJS,
  • SystemJS
  • Avec Node.js, on peut générer des modules compatibles avec CommonJS.
  • Sur un browser, on peut générer des modules AMD et utiliser RequireJS ou SystemJS.
  • On peut aussi générer un bundle avec webpack.
Chargement asynchrone Non Oui Oui
Relation avec le système de fichiers 1 – 1 N – N N – N

Exemples de syntaxe

AMD + RequireJS

AMD est le format utilisé par le loader RequireJS. Il rajoute une fonction qui n’existe pas dans la syntaxe Javascript: define. Cette fonction comporte 2 paramètres:

  • Une tableau de dépendances: ce sont les fichiers nécessaires à l’exécution de la fonction.
  • Une fonction: cette fonction est le module à définir.

La fonction define est interprétée par le loader, ce n’est pas une fonction Javascript.

Par exemple:

define([], function() {  
    function func1() {
         // ...
    }

    function func2() { 
         // ...
    }

    return { 
        func1: func1, 
        func2: func2 
    } 
}); 

define(['./module1', './module2'], 
    function(module1, module2) { 
        // ...
    }); 

Les modules sont injectés dans la fonction en paramètres. module1 et module2 indiquent les fichiers Javascript sans l’extension .js.

Pour utiliser le loader RequireJS:

  1. Il faut l’installer avec npm en exécutant:
    npm install --save requirejs 
    
  2. Il faut l’inclure dans le code HTML de la page principale:
    <script data-main="app" src="node_modules/requirejs/require.js"></script> 
    

    L’attribut data-main="app" correspond au point d’entrée de l’application qui est à un fichier Javascript contenant la configuration de requireJS.

Un exemple de cette implémentation se trouve dans le dépôt GitHub suivant: amd_requirejs-browser. Cet exemple permet d’illustrer l’utilisation d’AMD et du loader RequireJS pour permettre d’effectuer des appels d’un module à l’autre. Cet exemple est exécutable sur un browser.

Pour exécuter cet exemple, il faut:

  1. Exécuter la commande npm install dans le répertoire de l’exemple,
  2. Ouvrir le fichier index.html directement avec un browser après avoir ouvert la console de développement.

Le résultat dans la console de développement est:

Executed from main
main: calling module1 
Executed from module1Func()
main: calling module2
Executed from module2Func()
Executed from module1Func()
Executed from module3Func()

CommonJS + SystemJS

CommonJS est le format utilisé par le loader SystemJS et nativement par Node.js. En utilisant le format CommonJS, la définition est différente de celle d’AMD, on déclare les fonctions et on les ajoute aux modules en tant qu’attribut.

Par exemple:

function func1() { 
    // ... 
} 

function func2() { 
    // ... 
} 

Module.exports = { 
    func1: func1, 
    func2: func2 
}; 

Pour inclure des modules provenant d’un autre fichier, on utilise la fonction require.

Par exemple:

var moduleInFile = require('./file.js'); 

Avec cette déclaration, moduleInFile est une variable locale utilisable directement:

moduleInFile.func1(); 

Pour utiliser le loader systemJS:

  1. Il faut l’installer avec npm en exécutant la commande:
    npm install --save systemjs 
    
  2. Il faut inclure dans le code HTML de la page principale la référence suivante:
    <script src="node_modules/systemjs/dist/system.js"></script> 
    <script  
          // Le code suivant correspond à de la configuration 
          System.config({ 
               meta: {    
                   format: 'cjs'  // cjs correspond à la configuration pour le format commonjs 
               } 
         }); 
    
         System.import('js/module1.js')
           .then(function(module1) {
               module1.publicFuncModule1();
           }); // import de systemjs 
    </script> 
    

System.import permet d’indiquer au loader le module root. Il ne doit pas être une dépendance d’une autre module.

Un exemple de cette implémentation se trouve dans le dépôt GitHub suivant: commonjs_systemjs-node. Cet exemple permet d’illustrer l’utilisation de CommonJS avec le loader SystemJS exécuté sur un serveur Node.js. L’exemple montre des appels d’un module à l’autre.

Pour exécuter cet exemple, il faut:

  1. Exécuter la commande npm install dans le répertoire de l’exemple,
  2. Lancer le serveur web de développement en exécutant la commande npm run start
  3. Se connecter à l’adresse http://127.0.0.1:8080 pour voir le résultat de l’exécution après avoir ouvert la console de développement.

Les modules à partir de ES2015/ES6

La norme ES2015/ES6 ajoute des fonctionnalités par rapport à ES5 comme let, var, const et les modules. Toutefois ES2015 n’est pas compatible avec tous les browsers. Si on implémente une application en ES2015, elle pourrait ne pas s’exécuter sur des browsers non compatibles. Pour palier à ce problème, on peut utiliser des transpilers qui peuvent convertir le code ES2015 en ES5 de façon à ce qu’il soit compatible avec la grande majorité des browsers. On code ainsi, en bénéficiant de la syntaxe ES2015 et le code exécuté est compatible ES5.

Les modules ES2015 sont supportés nativement par:

  • Chrome à partir de la version 63.
  • Firefox à partir de la version 60,
  • Edge à partir de la version 16,
  • Safari à partir de la version 11,
  • Node.js à partir de la version 10.

Pour plus d’informations sur les compatibilités des browsers:

Il existe plusieurs façons d’exécuter du code Javascript comportant des modules ES2015:

  • Exécuter le code Javascript sur un serveur Node.js: à partir de la version 10, Node.js gère nativement le chargement de modules au format ES2015.
  • Il est aussi possible d’associer un loader comme RequireJS ou SystemJS qui va charger lui-même les modules.
  • On peut générer des bundles compatibles ES5 en utilisant Browserify ou webpack.

Export et import de modules

Les propriétés les plus importantes à avoir en tête avec les modules ES2015 sont les suivantes:

  • Par défaut, tous les modules ES2015 sont privés.
  • Un fichier Javascript correspond à un module.
  • Pour exposer des éléments comme des variables, des fonctions ou des classes à l’extérieur d’un module, il faut utiliser le mot-clé export.
Mode strict en ES5

Il est possible d’utiliser le mode strict (i.e. strict mode en ES5). Ce mode permet de ne pas déclarer des variables par inadvertance car il faut obligatoirement déclarer une nouvelle variable en utilisant var. Si on se trompe en effectuant une affectation de variable et en se trompant dans le nom de la variable, il y aura une erreur de façon à éviter qu’une variable avec un mauvais nom ne soit déclarée au niveau global et ainsi, affecter correctement une valeur à la bonne variable.

Pour utiliser le mode strict, il faut l’indiquer dans les premières lignes d’un script Javascript, par exemple:

"use strict"; 

var newValue = 3.14;

Exporter un module

L’export d’éléments permet de les exposer à l’extérieur du module de façon à les rendre accessible à l’extérieur du module.

Pour exporter, on utilise le mot-clé export.

Par exemple:

export function func1() {
    // ... 
}

Dans cet exemple, on exporte une fonction à l’extérieur du module.

D’autres formes de syntaxe sont possibles pour exporter plusieurs éléments du module en une fois:

function func1() {
    // ... 
} 

function func2() { 
    // ... 
} 

export { func1, func2 };

On peut aussi indiquer l’instruction export avant la déclaration des éléments à exporter:

export { func1, func2 };

function func1() { 
    // ... 
} 

function func2() { 
    // ... 
} 

Importer un module

L’import d’un module correspond à importer les éléments exportés par ce module dans un autre.

Pour importer tous les éléments exportés d’un module dans une variable, on peut utiliser la syntaxe:

import * as moduleInFile from './file.js'; 

Dans ce cas, la variable est moduleInFile, on peut l’utiliser directement pour accéder aux éléments du module, par exemple:

moduleInFile.func1();

On peut importer un élément spécifique et non pas tous les éléments exportés du module en précisant les éléments à importer à partir de leur nom, par exemple:

import { func1 as funcFromOuterModule, func2 } from './file.js'; 

func1 as funcFromOuterModule permet de renommer le nom de l’élément importé. Cette déclaration est facultative, ainsi elle n’est pas utilisée pour func2.

Dans ce cas, on peut appeler directement les fonctions:

func2(); 
funcFromOuterModule(); 

Export par défaut

L’export d’un élément par défaut permet d’indiquer l’élément qui sera importé dans un module si ce dernier ne précise pas ce qui doit être importé. Ainsi, au moment d’importer l’élément d’un module, il ne sera pas nécessaire de préciser le nom de l’élément à importer, l’élément par défaut sera le seul élément importé même si le fichier comporte d’autres éléments.

Par exemple:

function func1() {
    // ...
} 

function func2() { 
    // ... 
} 

export default func1;

Au moment d’importer, le seul élément importé sera l’élément exporté par défaut:

import func1 from "file"; // L'import se fait à partir du fichier file.js.  

L’import ne comporte pas d’accolades. C’est l’élément par défaut qui est importé.

L’élément importé par défaut peut être utilisable directement:

func1(); 

Export nommé

On peut nommer un export de façon à modifier le nom de l’élément qui est exporté.

Par exemple, dans un premier temps on exporte un élément en le renommant:

function func1() {
    // ... 
} 

function func2() { 
    // ... 
} 

export default func1;  
export var finalFunc = func2;

En plus de l’élément par défaut, on décide de renommer un élément exporté.

A l’import, il faut indiquer le nouveau nom de l’élément:

import { finalFunc } from "file"; 

On peut directement utilisé le nouveau nom de l’élément:

finalFunc();

Dans cet exemple, si on souhaite importer l’élément par défaut, on peut écrire:

import func1, { finalFunc } from "file"; 

Utilisation des modules ES2015

Dans un browser

Pour utiliser des modules ES2015 directement dans un browser, en plus du support de ces modules suivant la version du browser, il faut inclure les fichiers Javascript en utilisant la syntaxe suivante dans l’entête du fichier HTML principal:

<script type="module" src="./file1.js" /> 

Ou plus directement:

<script type="module"> 
import { elementToImport } from './elementModule.js'; 
// ... 
</script> 

Les browsers qui ne sont pas compatibles ES2015 ne vont pas charger les scripts déclarés avec l’attribut type="module".

Un exemple plus complet se trouve dans le dépôt Github: es6-browser. Cet exemple permet d’illustrer l’utilisation de la syntaxe ES2015 pour effectuer des appels d’un module à l’autre. Cet exemple exécutable à partir d’un browser, toutefois on utilise Node.js pour éviter l’erreur correspondant au chargement de scripts à partir d’un autre domaine (i.e. erreur Cross-origin resource sharing (CORS)).

Pour exécuter cet exemple, il faut:

  1. Télécharger les packages npm en exécutant la ligne: npm install dans le répertoire de l’exemple,
  2. Lancer le serveur web de développement en exécutant la commande npm run start
  3. Se connecter à l’adresse http://127.0.0.1:8080 pour voir le résultat de l’exécution après avoir ouvert la console de développement.

Le résultat de l’exécution est:

Executed from module1.privateFunc()
Executed from module1.publicFuncModule1()
Executed from module2.publicFuncModule2()
Executed from module3.privateFunc()
Executed from module3.publicFuncModule3()

Ordre d’exécution des modules

Suivant la façon de déclarer les modules dans une balise <script>, l’ordre d’exécution est différent:

  1. Balise <script> avec l’attribut src: par exemple:
    <script src="file.js" />
    
  2. Module déclaré dans le corps d’une balise <script> de façon “inline”:
    <script type="module"> 
    // Corps du module 
    </script> 
    
  3. Utilisation de l’attribut defer pour retarder l’exécution du script jusqu’à ce que le document soit entièrement chargé et parsé:
    <script defer src="file.js" /> 
    
  4. Les modules déclarés avec les attributs src et type sont exécutés en dernier:
    <script type="module" src="file.js" /> 
    

Attribut “nomodule”

Suivant les besoins, il peut être nécessaire d’avoir des scripts différents pour les browser qui gèrent les modules ES2015 et pour les browsers qui ne les gèrent pas. La solution est d’utiliser l’attribut nomodule au moment de déclarer le script. Ainsi:

  • Pour déclarer les scripts destinés aux browsers ne gérant pas les modules ES2015: on utilise une balise <script> sans l’indication du type module mais avec l’attribut nomodule. Les browsers gérant les modules n’exécuteront pas ces scripts:
    <script nomodule src="runs_if_module_not_supported.js" /> 
    
  • Pour déclarer les scripts destinés aux browsers gérant les modules ES2015, il suffit d’utiliser l’attribut indiquant le type:

    <script type="module" src="runs_if_module_supported.js" /> 
    

    Les browsers incompatibles avec ES2015 ne chargeront pas ces modules.

Différence d’ordre de chargement des modules entre ES2015 et CommonJS

Il existe une différence dans la façon dont les modules sont chargés entre ES2015 et avec le format CommonJS:

  • Avec CommonJS, les modules chargent leurs dépendances en même temps qu’ils en exécutent le code.
  • Avec ES2015, les modules sont pré-parsés de façon à résoudre tous les imports avant d’exécuter le code.

Ainsi on écrit les modules ES2015 suivant:
Dans le fichier file1.js:

console.log('Exécute file1.js'); 
import { exportedFromTwo} from './file2.js'; 
console.log(exportedFromTwo); 

Dans le fichier file2.js:

console.log('Exécute file2.js'); 
export const exportedFromTwo = "Provient de file2.js"; 

Le résultat de l’exécution sera:

Exécute file2.js 
Exécute file1.js 
Provient de file2.js 

Dans le cas des modules déclarés avec le format CommonJS:
Dans le fichier file1.js:

console.log('Exécute file1.js'); 
const exportedFromTwo = require('./file2.js'); 
console.log(exportedFromTwo); 

Dans le fichier file2.js:

console.log('Exécute file2.js'); 
module.exportedFromTwo = 'Provient de file2.js'; 

Le résultat de l’exécution sera:

Exécute file1.js 
Exécute file2.js 
Provient de file2.js 

Pour conclure…

Comme on a pu le voir, les modules en Javascript s’implémentent de façon très différente. Avant l’apparition de la syntaxe native en ES2015, il a fallu trouver des work-arounds et utiliser différents outils pour implémenter des modules. Désormais, avec la syntaxe ES2015, l’implémentation des modules est plus facile et plus homogène. Toutefois, beaucoup de browsers sont utilisés avec des versions non compatibles avec ES2015. Il faut donc toujours produire du code Javascript permettant d’être exécuté sur ce type de browser. Ainsi des outils comme des bundlers et des transpilers rendent possible l’implémentation du code Javascript avec la syntaxe ES2015 et son exécution avec du code ES5.

Références
One response... add one

Leave a Reply