Utiliser une librairie JavaScript en tant que module ECMAScript

Upcycling.

Un certain nombre de librairies JavaScript ne sont pas développées en tant que module ECMAScript mais il est néanmoins parfois possible de les utiliser comme tel.

Dans un article précédent, nous avons vu que les modules JavaScript (ECMAScript Module : ESM) sont une très bonne option pour le développement Web éco-responsable, mais si on souhaite utiliser des librairies JavaScript qui n’ont pas été développées comme des modules ECMAScript cela peut compliquer les choses et empêcher de profiter pleinement de leurs avantages, comme l’importation dynamique (import()) ou le pré-chargement (modulepreload) bien plus efficace que le pré-chargement d’un script ordinaire.

Solution

Il existe une solution simple à ce problème qui consiste à passer par un Content Delivery Network (CDN) capable de convertir une librairie non ESM en module ECMAScript.

Nous avons identifié 3 CDN qui font cela (il y en a peut être d’autres) :

  1. Skypack
  2. esm.sh
  3. jsDelivr

En utilisant ces CDN, il est possible de créer des applications basées entièrement sur des modules ECMAScript et sans avoir besoin d’outil de construction (build tools) pendant le développement.

Notre choix se porte sur jsDelivr qui est probablement le plus performant. Il offre par ailleurs des URL courtes pour obtenir un module en version ESM. Ainsi l’URL pour importer une version ESM de toute la librairie D3 est : https://esm.run/d3@7.9.0 à la place de https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm.

Remarque : La version courte sur le domaine esm.run génère une redirection sur le domaine cdn.jsdelivr.net en ajoutant /+esm à la fin de l’URL. Si vous voulez éviter ces redirections, il est donc préférable d’utiliser les URL longues.

Pour montrer la simplicité de cette solution, 2 exemples sont disponibles ci-après.

Premier exemple

Comme premier exemple, nous utilisons la sympathique petite librairie Canvas-Confetti qui permet d’afficher des confettis animés sur une page Web. Cette librairie n’est pas disponible sous la forme d’un module ECMAScript mais peut néanmoins être utilisée comme tel.

Pour cela, dans un module ECMAScript nous avons le code suivant :

import confetti from "Canvas-Confetti";

const button = document.getElementById("example2");
button?.addEventListener("click", () => confetti());

Le code commence par importer la fonction confetti() depuis le module Canvas-Confetti, puis ajoute une fonction gérant l’évènement click à un bouton d’identifiant example2.

Vous pouvez tester le fonctionnement de ce code en cliquant sur le bouton suivant : 

Pour que cela fonctionne, nous avons une carte d’importation qui ressemble à cela :

<script type="importmap">
{
"imports": {
"Canvas-Confetti": "https://esm.run/canvas-confetti@1.9.3",
"Example002": "/esm/dev/example-002.js"
}
}
</script>

C’est dans la carte d’importation que le module Canvas-Confetti est associé au code JavaScript donné par le CDN jsDelivr.

L’avantage de passer par une carte d’importation est que si demain la librairie Canvas-Confetti existe sous la forme d’un module ECMAScript, il suffira juste de modifier la carte d’importation sans avoir à modifier les modules qui importent le module Canvas-Confetti.

Deuxième exemple

Pour le deuxième exemple, nous utilisons l’excellente librairie Leaflet qui permet d’afficher des cartes sur une page Web et qui n’existe pas encore sous la forme d’un module ECMAScript (peut être dans la version 2).

Dans un module ECMAScript, nous avons le code suivant :

async function createMap(element, lat = 47.366001, lng = 1.762698, zoom = 5) {
try {
// Importation du module
const L = await import("Leaflet");
// Création du container de la carte
const mapContainer = h("div");
// Ajout dans le DOM
element.append(
h("link", {
href: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css",
rel: "stylesheet"
}),
mapContainer
);
// Création de la carte avec Leaflet
const map = L.map(mapContainer, {center: [lat, lng], zoom, scrollWheelZoom: false});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// Création d'un observeur en cas de redimensionnement de l'élément
var resizeObserver = null;
if ("ResizeObserver" in window) {
resizeObserver = new ResizeObserver(() => map.invalidateSize());
resizeObserver.observe(element);
}
return {L, map, resizeObserver};
} catch (error) {
element.append(h("p", "Problème de chargement du module."));
return null;
}
}

const mapBlock = document.getElementById("example3");
if (mapBlock) {
createMap(mapBlock);
}

Le code de ce module récupère la référence d’un élément <div> d’identifiant example3 et appelle la fonction createMap() avec cette référence si elle existe.

C’est la fonction createMap() qui se charge d’importer dynamiquement le module Leaflet avec la fonction import().

Il est bien sûr possible d’utiliser l’importation standard des modules ECMAScript avec l’instruction import, mais si l’élément <div> d’identifiant example3 n’existe pas, le module est importé pour rien. C’est une bonne pratique que nous évoquons dans notre article sur le chargement paresseux.

Si le module est importé correctement, la fonction createMap() :

  • fabrique un élément <div> (variable mapContainer) qui va contenir la carte,
  • ajoute avec la fonction append() un élément <link> pour la CSS de Leaflet et l’élément <div> pré-créé dans l’élément qui lui est passé en paramètre,
  • utilise les fonctions du module (variable L) pour créer la carte (variable map) avec un centrage par défaut sur la France,
  • fabrique un observateur de redimensionnement (API Resize Observer) pour le cas où les dimensions de l’élément changent,
  • retourne une promesse résolue (la fonction est asynchrone) avec un objet contenant la référence du module, la carte et l’observateur pour un usage ultérieur (ajout d’un marqueur sur la carte, suppression de la carte, etc.).

Si le module n’est pas importé correctement, la fonction createMap() ajoute un élément <p> avec un texte signalant le problème dans l’élément qui lui est passé en paramètre et retourne une promesse résolue avec null.

La fonction h() est celle utilisée et décrite dans un article précédent.

Il est parfaitement légal en HTML 5 d’avoir un élément <link> dans l’élément <body>, la bonne pratique étant néanmoins de l’avoir dans l’élément <head>. Mais cela permet ici de ne pas importer la CSS si le module Leaflet n’est pas importé.

Et comme pour l’exemple précédent, nous avons maintenant la carte d’importation qui ressemble à cela :

<script type="importmap">
{
"imports": {
"Canvas-Confetti": "https://esm.run/canvas-confetti@1.9.3",
"Leaflet": "https://esm.run/leaflet@1.9.4",
"Example002": "/esm/dev/example-002.js"
"Example003": "/esm/dev/example-003.js"
}
}
</script>

Et pour finir, le module donne le résultat suivant :

Conclusion

Avec cette solution, nous pouvons développer des applications ou sites Web en profitant de la richesse de l’Open Source tout en étant éco-responsable :

  • Pas besoin d’avoir de couteux processus de build sur nos postes de développement.
  • La possibilité de tout faire avec des modules ECMAScript et de donc de profiter de leurs avantages adaptés à l’éco-conception Web.
  • Une maintenance aisée grâce aux cartes d’importation.

Et en plus on peut voir cette solution comme une forme de surcyclage (upcycling), on réutilise un produit pour en faire un autre 😀.