X

Le cas des dépendances faibles dans JS – Lea Verou


Temps de lecture: 5 minutes

Plus tôt dans la journée, j’ai brièvement envisagé d’écrire une bibliothèque pour envelopper et améliorer querySelectorAll de certaines manières. Je pensais que je préférerais ne pas introduire une dépendance Parsel prête à l’emploi, mais ne l’utiliser que pour analyser correctement les sélecteurs lorsqu’elle est disponible, et utiliser une expression régulière plus grossière lorsqu’elle ne l’est pas (ce qui couvrirait la plupart des cas d’utilisation pour ce que je voulais faire) .

Autrefois, où chaque bibliothèque introduisait un global, je pouvais simplement faire :

if (window.Parsel) {
	let ast = Parsel.parse();
	// rewrite selector properly, with AST
}
else {
	// crude regex replace
}

Cependant, avec ESM, il ne semble pas y avoir de moyen de détecter si un module est importé, sans l’importer vous-même.

je tweeté à propos de ça…

Je pensais que c’était un paradigme commun, et tout le monde comprendrait pourquoi c’était utile. Cependant, j’ai été surpris de constater que la plupart des gens étaient déconcertés par mon cas d’utilisation. La plupart d’entre eux pensaient que je parlais soit d’importations conditionnelles, soit de récupération d’erreur après l’échec d’importations.

Je soupçonne que c’est peut-être parce que ma perspective principale pour écrire JS est celle d’un auteur de bibliothèque, où je ne contrôle pas l’environnement hôte, alors que pour la plupart des développeurs, leur perspective principale est celle d’écrire JS pour une application ou un site Web spécifique.

Après Kyle Simpson m’a demandé d’élaborer à propos du cas d’utilisation, j’ai pensé qu’un article de blog était en ordre.

Le cas d’utilisation est essentiellement amélioration progressive (en fait, j’ai caressé l’idée d’intituler cet article de blog “JS progressivement amélioré”). Si la bibliothèque X est déjà chargée par un autre code, faites une chose plus élaborée et couvrez tous les cas extrêmes, sinon faites une chose plus basique. C’est pour les dépendances qui ne sont pas vraiment dépendreencements, mais plus comme sympa à avoir.

Nous voyons souvent des modules qui font très bien les choses, mais utilisent une tonne de dépendances et ajoutent beaucoup de poids, même aux projets les plus simples, car ils doivent répondre à tous les cas extrêmes dont nous ne nous soucions peut-être pas. Nous voyons également des modules sans dépendance, mais c’est parce que beaucoup de choses sont implémentées de manière plus grossière ou que certaines fonctionnalités ne sont pas là.

Ce paradigme vous offre le meilleur des deux mondes : Sans dépendance (ou à faible dépendance), qui peuvent utiliser ce qui est disponible pour améliorer la façon dont ils font les choses avec zéro impact supplémentaire.

En utilisant ce paradigme, le la taille de ces dépendances n’est pas un souciparce qu’ils sont dépendances homologues facultatives, afin que l’on puisse choisir la meilleure bibliothèque pour le travail sans être affecté par la taille du bundle. Ou même en utiliser plusieurs ! Il n’est même pas nécessaire de choisir une dépendance pour chaque chose, elles peuvent prendre en charge des bibliothèques plus grandes et plus complètes lorsqu’elles sont disponibles et se rabattre sur des micro-bibliothèques lorsqu’elles ne le sont pas.

Quelques exemples en plus de celui du premier paragraphe :

  • Un convertisseur Markdown vers HTML qui met également en évidence la syntaxe des blocs de code si Prism est présent. Ou il pourrait même prendre en charge plusieurs surligneurs différents !
  • Un éditeur de code qui utilise Incrementable pour rendre les nombres incrémentables via les touches fléchées, s’il est présent
  • Une bibliothèque de modèles qui utilise également Dragula pour rendre les éléments réorganisables par glisser-déposer, le cas échéant
  • Un cadre de test qui utilise Tippy pour de belles fenêtres contextuelles d’information, lorsqu’il est disponible
  • Un éditeur de code qui affiche la taille du code (en Ko) si une bibliothèque à mesurer est incluse. Le même éditeur peut également afficher la taille du code gzip si une bibliothèque gzip est incluse.
  • Une bibliothèque d’interface utilisateur qui utilise un élément personnalisé s’il est disponible ou l’élément natif le plus proche lorsqu’il ne l’est pas (par exemple, un sélecteur de date sophistiqué par rapport à <input > ) quand ce n’est pas le cas. Ou Awesomplete pour la saisie semi-automatique lorsqu’elle est disponible, et revenez à un simple <datalist> quand ce n’est pas le cas.
  • Code qui utilise une bibliothèque de formatage de date lorsqu’une est déjà chargée, et revient à Intl.DateTimeFormat quand ce n’est pas le cas.

Ce modèle peut même être combiné avec un chargement conditionnel: par exemple, nous vérifions tous les surligneurs de syntaxe connus et chargeons Prism s’il n’y en a pas.

Pour récapituler, certains des principaux avantages sont:

  • Performance: Si vous chargez des modules sur le réseau, les requêtes HTTP sont coûteuses. Si vous effectuez un pré-groupement, cela augmente la taille du groupe. Même si la taille du code n’est pas un problème, les performances d’exécution sont affectées si vous prenez le chemin lent mais toujours correct lorsque vous n’en avez pas besoin et une approche plus grossière serait satisfaisante.
  • Choix: Au lieu de choisir une bibliothèque pour ce dont vous avez besoin, vous pouvez en prendre en charge plusieurs. Par exemple, plusieurs surligneurs de syntaxe, plusieurs analyseurs Markdown, etc. Si une bibliothèque est toujours nécessaire pour faire ce que vous voulez, vous pouvez la charger conditionnellement, si aucune de celles que vous supportez n’est déjà chargée.

Les dépendances faibles sont-elles un anti-modèle ?

Depuis que cet article a été publié, certains des commentaires que j’ai reçus allaient dans le sens de « Les dépendances faibles sont un anti-modèle parce qu’elles sont imprévisibles. Que faire si vous avez inclus une bibliothèque mais que vous ne souhaitez pas qu’une autre bibliothèque l’utilise ? Vous devriez plutôt utiliser des paramètres pour fournir explicitement des références à ces bibliothèques.

Il y a plusieurs contrepoints à faire ici.

Premièrement, si les dépendances faibles sont bien utilisées, elles ne servent qu’à améliorer le comportement par défaut/de base, il est donc très peu probable que vous souhaitiez le désactiver et revenir au comportement par défaut.

Deuxièmement, les dépendances faibles et l’injection de paramètres ne sont pas mutuellement exclusives. Ils peuvent fonctionner ensemble et se compléter, de sorte que les dépendances faibles fournissent des valeurs par défaut sensibles que les paramètres peuvent ensuite modifier davantage (ou désactiver complètement). Seul l’injection de paramètres impose un coût cognitif initial élevé pour l’utilisation de la bibliothèque (voir Convention sur la configuration). De bonnes API rendent les choses simples faciles et les choses complexes possibles. Le cas courant est que si vous avez chargé par exemple un surligneur de syntaxe, vous voudriez l’utiliser pour mettre en évidence la syntaxe, et si vous avez chargé un analyseur, vous le préféreriez à l’analyse avec des regex. Les cas de bord obscurs où vous ne voudriez pas mettre en évidence ou vous voulez fournir un analyseur différent peuvent toujours être possibles via des paramètres, mais ne devraient pas être le seul moyen.

Troisièmement, l’utilisateur final-développeur peut même ne pas être au courant de toutes les bibliothèques qui sont chargées, de sorte qu’il peut déjà avoir une bibliothèque chargée pour une certaine tâche mais ne pas le savoir. Le modèle de dépendances faibles fonctionne directement sur les modules chargés, il ne souffre donc pas de ce problème.

Comment cela pourrait-il fonctionner avec ESM ?

Certaines personnes (principalement d’autres auteurs de bibliothèques) *ont* compris de quoi je parlais et ont exprimé quelques idées sur la façon dont cela fonctionnerait.

Idée 1 : Un cache chargé de module global pourrait être un moyen de bas niveau pour l’implémenter, et quelque chose que CJS prend en charge apparemment.

Idée 2 : Un registre global sur lequel les modules peuvent s’enregistrer, soit avec un identifiant, soit un hachage SHA
Idée 3 : Un import.whenDefined(moduleURL) promesse, même si cela rend difficile de gérer le fait que le module n’est pas présent du tout, ce qui est tout l’intérêt.

Idée 4 : Surveillance <link rel="modulepreload">. Le problème est que tous les modules ne sont pas chargés de cette façon.

Idée 5 : Je pensais à une fonction comme import() qui se résout avec le module (comme pour une importation dynamique normale) uniquement lorsque le module est déjà chargé, ou le rejette lorsqu’il ne l’est pas (ce qui peut être intercepté). En fait, il pourrait même utiliser la même notation fonctionnelle, avec un second argument, comme ceci :

import("https://cool-library", {weak: true});

Presque toutes ces propositions souffrent de l’un des problèmes suivants.

Ceux qui sont Basé sur l’URL signifie que seuls les modules chargés à partir de la même URL seront reconnus. La même bibliothèque chargée sur un CDN vs localement ne serait pas reconnue comme la même bibliothèque.

Une façon de contourner cela consiste à exposer une liste d’URL, comme la première idée, et à permettre d’écouter les modifications qui y sont apportées. Ensuite, ces URL peuvent être inspectées et celles qui pourrait appartiennent au module que nous recherchons peuvent être inspectés plus avant en important et en inspectant dynamiquement leurs exportations (l’importation de modules déjà importés est une opération assez bon marché, le navigateur déduplique la requête).

Ceux qui sont basé sur un identifiant, dépendent du module pour s’enregistrer avec un identifiant, donc seuls les modules qui veulent être exposés le seront. C’est la situation la plus proche de l’ancienne situation mondiale, mais elle souffrirait pendant la période de transition jusqu’à ce que la plupart des modules l’utilisent. Et bien sûr, il y a un risque d’affrontements. Bien que l’API puisse s’en occuper, en utilisant essentiellement une table de hachage et en ajoutant tous les modules qui s’enregistrent avec le même identifiant sous le même “seau”. Le code lisant le registre serait alors responsable du filtrage.