JavaScript pour Pro

Les modules

Diviser les programmes JavaScript en plusieurs modules qu'on pourrait importer les uns dans les autres est un mécanisme qui n'existe que depuis 2015 (ES6).

Un module est un fichier JS ...

... et donc, tout fichier JS est un module, lorsqu'on écrit de façon modulaire.

Un module ne peut être appelé depuis le code HTML, via la balise <script>, que si l'attribut type prend pour valeur "module" :
<script type="module" src="m_xxx.js">

Pour éviter la confusion, l'extension des fichiers JS de type module peut-être .mjs. Toutefois, ... la plupart des serveurs (même Apache) ne sont pas (encore) configurés pour supporter cette extension, par défaut.

Configuration : Les fichiers ayant l'extension .mjs doivent être "servis" avec un en-tête Content-Type qui contient un type MIME JavaScript comme text/javascript

Un fichier HTML peut continuer à appeler des fichiers JS "classiques". Et, donc pour éviter la confusion, il est donc recommandé de préfixer les modules JS. Par exemple, par m_.

Pour exécuter des modules en local, il faut utiliser un serveur web local.

Exemple

Exemple d'utilisation d'une fonction importée.

Contenu du fichier JS, appelé exportAlert.js, qui contient une fonction importable
et qui est situé dans le dossier-fils (appelé "modules") du dossier de la présente page web :

export function alerte() { alert("Bonjour './'") }

Contenu du fichier JS, appelé m_jour5.js, qui importe (la fonction alerte) :

/* l'appel du présent fichier se fait via : type="module" */
import { alerte } from "./modules/exportAlert.js";
/* NB : Relative references must start with either "/", "./", or "../"
=> import { alerte } from "modules/exportAlert.js";  == erreur
=> Un fichier JS qui utilise le mot-clé "import" doit être appelé via "/", "./", or "../" */

function appelAlert() { alerte() }

/* appelAlert(); /* = appel lors du chargement de la page */

/* NB : La fonction appelAlert() ne peut pas être appelée via un onclick="..." (dans le code HTML)
=> créer un écouteur => le bouton doit disposer de l'attribut id */
document.getElementById("btnAppelAlert").onclick = function(evt){
 appelAlert()
}

Un fichier JS qui utilise le mot-clé import doit être appelé via "/", "./", ou "../".

Extrait du code HTML de la présente page web qui appelle un fichier JS qui importe :

              ...
              <button id="btnAppelAlert">Afficher : <i>Bonjour ./</i></button>
              <!-- Code HTML du bouton ci-dessous -->
              ...
    <script src="jour5.js"></script>
    <script src="m_jour5.js" type="module"></script>
  </body>
</html>
Si on fait appel à des importations, ...

... il est préférable de scinder le code JS en deux fichiers JS.

Le premier est celui qui n'est pas lié aux importations.

Le second est lié aux importations.
Par commodité, le nom est préfixé par m_ (comme dans le mot module ...)

Cette séparation permet à la page web de rester partiellement opérationnelle, en cas d'erreur dans le second fichier.

En effet, si tout le code JS se trouve dans un seul fichier, comme les importations ont lieu au début du fichier, une erreur dans un chemin relatif arrêtera l'exécution du code JS.

Exemple. Code JS du fichier jour5.js :

if (location.protocol.substr(0,4)!="http") document.getElementById("noServer").innerHTML
 ="<span class='red'>Pas connecté à un serveur !</span>"

Si la page web n'est pas sur un serveur, s'affiche en rouge et en haut de page un message.

Mais, ... si cette instruction était placée après un import et que le fichier visé soit manquant, l'exécution s'arrêtera à cette importation ratée et donc les instructions qui suivent ne seront pas exécutées. L'utilisateur ne sera donc pas prévenu que la page web ne provient pas d'un serveur.

Et, donc, qu'elle ne fonctionnera pas correctement même s'il n'existe pas d'erreur au niveau des chemins d'importations.

Le bouton ci-dessous (Afficher : Bonjour ./) ne fonctionnera pas.

Le bouton ci-dessus ne fonctionne que :

  1. Si la présente page web est sur un serveur,
  2. Et, si m_jour5.js est appelé via type="module",
  3. Et, si un écouteur a été créé (dédié à l'événement click de ce bouton).

Disposer d'un serveur web local léger

Un serveur (local) tel qu'Apache peut être utilisé pour tester l'exécution de modules JS. Mais, il est lourd ...

Pour disposer d'un serveur web local léger, il n'est pas recommandé d'installer un programme développé par un éditeur peu connu. Python est très connu et fiable.

Si Python est installé sur l'ordinateur, on dispose d'un serveur web local léger et suffisant pour l'exécution de fichiers JS.
Sinon, il est recommandé d'installer Python.

Il suffit alors de se placer dans le dossier contenant les fichiers (via l'explorateur de fichiers), d'ouvrir un terminal (via le menu contextuel) et d'y taper la commande :

py -m http.server

Il est préférable que le fichier index.htm (ou index.html) soit le fichier d'accueil d'un site "autonome" (= dont toutes les ressources, telles que les fichiers CSS, images, ... soient dans ce dossier ou ses sous-dossiers)

Tester un site sur un serveur local est un bon moyen de voir s'il est autonome.
Un site autonome peut facilement être déplacé sur un ou plusieurs serveurs connectés à Internet.

Dans le navigateur, ouvrir un nouvel onglet et taper dans la barre des URLs :

http://localhost:8000/

Pour éteindre ce serveur, dans le terminal, il suffit de taper : CTRL + C
Et, exit pour fermer le terminal.

export

Pour que des variables (souvent des constantes) et des fonctions (et classes) puissent être exportées, il suffit d'indiquer, à la fin du fichier, export suivi de leurs noms séparés par une virgule entre accolades. Tel que

const name = ...;
function draw() { ... }
function reportArea() { ... }
function reportPerimeter() { ... }

export { name, draw, reportArea, reportPerimeter };

Le nom de la fonction ne doit pas être suivi de parenthèses.
Les valeurs exportées doivent être présentes au plus haut niveau du script, il n'est pas possible d'utiliser export dans une fonction.

Ou,

Le nom de chaque variable et fonctions exportable est précédé du mot export

export const name = "square";

export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color,
  };
}

Remarquons qu'un objet peut être retourné par une fonction.
Le nom d'une variable ne peut pas correspondre à celui d'une fonction.

Chemin relatif

Maintenant que nous disposons de fichiers au contenu exportable, il faut connaître le chemin vers ces fichiers pour les importer.

Un fichier JS qui utilise le mot-clé import doit être appelé via "/", "./", ou "../".

Le chemin donné peut être absolu (en commençant par /) ... mais, dans ce cas, l'application web ne peut être déplacée !

En pratique, pour pouvoir déplacer l'application web, le chemin donné est toujours relatif :

Souvent ce dossier est celui qui contient le fichier m_main.js.

Un fichier au contenu exportable peut aussi (préalablement) importer ...

import

Lorsque des modules destinés à l'exportation existent, il ne reste plus qu'à les importer ...

import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";

let myCanvas = create("myCanvas", document.body, 480, 320);
let reportList = createReportList(myCanvas.id);

let square1 = draw(myCanvas.ctx, 50, 50, 100, "blue");
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);

as

Une fonction d'un module peut avoir le même nom que celle d'un autre module ... Pour éviter lors de l'importation, une SyntaxError: redeclaration of import name, il faut renommer ces fonctions via as.

import * as SQUARE from "./modules/square.js"; /* dispose d'une fonction exportable 'draw' */
import * as CIRCLE from "./modules/circle.js"; /* dispose d'une fonction exportable 'draw' */

let square1 = SQUARE.draw(...);
let circle1 = CIRCLE.draw(...);

import * permet d'importer tout ce qui est exportable.

Il est toutefois possible de n'importer que partiellement.

import { draw, reportArea } as SQUARE from "./modules/square.js";

let square1 = SQUARE.draw(...);

Par convention, le namespace est écrit en majuscules.

Ou,

import {
  draw as drawSquare,
  reportArea as reportSquareArea
} from "./modules/square.js";

let square1 = drawSquare(...);

DOM

Qui dit module, dit écouteur créé en JS.

Exemple

<script type="importmap">

La valeur importmap de l'attribut type de la balise script n'est reconnue des principaux navigateurs web que depuis mars 2021.

Dans l'instruction (contenue dans un fichier JS) :
import { alerte } from "./modules/exportAlert.js"; ce qui suit le mot-clé from est appelé un module specifier.

Jusqu'ici, ce module specifier était une URL soit relative soit absolue.

Grâce au contenu de cette balise HTML, il est possible de remplacer cette URL par une clé quelconque.

Il est même possible de lier plusieurs clés à plusieurs fichiers JS. Bref, de créer une carte (en anglais, map) de correspondances.

Le contenu de cette balise HTML est un objet au format JSON.

Cette balise est unique dans le fichier HTML. Logique, vu qu'on ne peut fait qu'une carte de correspondances. De plus, les autres attributs de cette balise (src, async, nomodule, defer, crossorigin, integrity, and referrerpolicy) ne doivent pas être présents.

Exemple

<script type="importmap">
  {
    "imports": {
      "square": "./module/shapes/square.js",
      "circle": "https://example.com/shapes/circle.js"
    }
  }
</script>

Même s'il n'existe qu'une seule correspondance, la clé de cet objet est toujours imports (avec un s)
De plus, lorsqu'il existe plusieurs couples de key:value, ils sont séparés par une virgule.

Cette balise doit être placée avant toute balise <script> important des modules utilisant des spécificateurs présents dans la carte.. Elle peut être placée dans le head du fichier HTML

Plus d'info sur MDN