Extensions extensibles en JavaScript

Bonjour, Habr!

Nous attirons votre attention sur l'exemplaire supplémentaire tant attendu du livre " JavaScript expressif ", qui vient de l'imprimerie.


Pour ceux qui ne connaissent pas le travail de l'auteur du livre (pour toute la nature encyclopédique, les débutants l'apprécieront également) - nous vous suggérons de vous familiariser avec l'article de son blog; L'article présente des réflexions sur l'organisation des extensions en JavaScript.

De nos jours, il est devenu à la mode de structurer de grands systèmes sous la forme de nombreux packages séparés. L'idée directrice sous-jacente à cette approche est qu'il est préférable de ne pas limiter les gens à une fonctionnalité spécifique (que vous proposez) en implémentant une fonctionnalité, mais de fournir cette fonctionnalité sous la forme d'un package séparé qu'une personne peut télécharger avec le package système de base.
Pour ce faire, en termes généraux, vous aurez besoin de ...

  • La possibilité de ne pas télécharger les fonctionnalités dont vous n'avez pas besoin est particulièrement utile sur les systèmes clients.
  • La possibilité de remplacer par une autre implémentation un élément de fonctionnalité qui ne répond pas à vos objectifs. Ainsi, la charge sur les modules du noyau est également réduite - il n'est pas nécessaire de couvrir tous les cas pratiques possibles avec leur aide.
  • Vérification des interfaces du noyau dans des conditions réelles - en implémentant des fonctionnalités de base au-dessus de l'interface face au client, vous êtes obligé de rendre cette interface au moins si puissante qu'elle peut prendre en charge ces fonctionnalités. Ainsi, vous pouvez être sûr qu'il sera possible de s'appuyer sur des choses similaires écrites par des développeurs tiers.
  • Isolement entre les parties du système. Les participants au projet peuvent simplement rechercher le package qui les intéresse. Les packages peuvent être versionnés, marqués comme indésirables ou remplacés sans affecter le code principal.

Cette approche est généralement associée à une complexité accrue. Pour faciliter le démarrage des utilisateurs, vous pouvez leur fournir un package wrapper dans lequel «tout est inclus», mais tôt ou tard, ils devront probablement se débarrasser de ce shell et installer et configurer des packages auxiliaires spécifiques, ce qui est parfois plus difficile que de simplement passer à une autre caractéristique de la bibliothèque monolithique.

Dans cet article, nous allons essayer de discuter des options de mécanismes d'extension qui prennent en charge «l'extensibilité à grande échelle» et vous permettent de créer de nouveaux points d'extension là où cela n'a pas été fourni.

Extensibilité


Que voulons-nous d'un système extensible? Tout d'abord, bien sûr, il devrait avoir la possibilité d'étendre ses propres capacités à l'aide de code externe.

Mais cela ne suffit pas. Permettez-moi de m'écarter d'un problème stupide que j'ai rencontré une fois. Je développe un éditeur de code. Dans l'une des versions antérieures de cet éditeur, vous pouviez définir le style d'une ligne spécifique dans le code client. C'était super - une disposition de ligne sélective.

À l'exception du cas où des tentatives de modification de l'apparence de la ligne sont effectuées immédiatement à partir de deux sections du code - et ces tentatives commencent à aboutir. La deuxième extension appliquée à la ligne remplace le style de la première extension. Ou, lorsque le premier code essaie de supprimer la conception qu'il a faite quelque part dans la partie suivante du code, il écrase le style introduit par le deuxième fragment de code.

Nous avons réussi à trouver une solution, offrant la possibilité d'ajouter (et de supprimer) l'extension, et non de la définir, afin que les deux extensions puissent interagir avec la même ligne sans saboter le travail de l'autre.

Dans un sens plus général, il est nécessaire de s'assurer que les extensions peuvent être combinées, même si elles ignorent complètement l'existence les unes des autres - sans provoquer de conflits entre elles.

Pour ce faire, vous devez vous assurer qu'un nombre illimité d'acteurs peut affecter chaque point d'expansion. Le traitement exact de plusieurs effets dépend de la situation. Voici quelques approches qui pourraient vous être utiles:

  • Tous prennent effet. Par exemple, lors de l'ajout d'une classe CSS à un élément ou lors de l'affichage d'un widget, ces deux fonctionnalités sont ajoutées en même temps. Souvent, ils devront toujours être triés d'une manière ou d'une autre: les widgets doivent être affichés dans une séquence prévisible et bien définie.
  • Ils s'alignent sous la forme d'un convoyeur. Un exemple est un gestionnaire qui peut filtrer les modifications qui sont ajoutées à un document avant qu'elles ne soient apportées. Chaque modification est d'abord transmise à un gestionnaire, qui, à son tour, peut également la modifier. Dans ce cas, la commande n'est pas critique, mais elle peut faire la différence.
  • Vous pouvez appliquer une approche du premier arrivé, premier servi aux gestionnaires d'événements. Chaque gestionnaire a une chance de servir l'événement jusqu'à ce que l'un d'eux dise qu'il l'a déjà traité, après quoi les gestionnaires faisant la queue ne sont plus interrogés.
  • Il arrive également que vous ayez vraiment besoin de choisir une valeur spécifique - par exemple, déterminer la valeur d'un paramètre de configuration spécifique. Il peut être conseillé d'utiliser un certain opérateur (par exemple, logique et, logique ou, minimum ou maximum) pour limiter le nombre de valeurs d'entrée pour une position. Par exemple, un éditeur peut passer en mode lecture seule si une extension le commande. Vous pouvez soit définir la valeur maximale du document, soit le nombre minimal de valeurs signalées à cette option.

Dans de nombreux cas, l'ordre est important. Cela signifie que la priorité des effets appliqués doit être contrôlable et prévisible.

C'est sur ce front que les systèmes d'extension impératifs basés sur l'utilisation d'effets secondaires sont généralement incapables de faire face. Par exemple, l'opération addEventListener effectuée par le modèle DOM du navigateur entraîne l'appel exact des gestionnaires d'événements dans l'ordre dans lequel ils ont été enregistrés. Ceci est normal si tous les appels sont contrôlés par un seul système, ou si l'ordre des opérations n'est vraiment pas important, cependant, lorsque vous devez gérer de nombreux fragments logiciels qui ajoutent indépendamment des gestionnaires, il peut être très difficile de prédire lesquels seront appelés en premier lieu.

Une approche simple


Pour vous donner un exemple simple: j'ai d'abord appliqué une stratégie modulaire à ProseMirror, un système d'édition de texte riche. Le cœur de ce système, en soi, est, en principe, inutile - il repose entièrement sur des packages supplémentaires qui décrivent la structure des documents, les liaisons de clés et l'historique des annulations. Bien qu'il soit vraiment un peu difficile à utiliser ce système, il a été adopté dans les produits qui nécessitent une conception de texte personnalisée, qui n'est pas disponible dans les éditeurs classiques.

Le mécanisme d'extension utilisé dans ProseMirror est relativement simple. Lors de la création de l'éditeur, le code client indique un seul tableau d'objets connectés. Chacun de ces plugins peut en quelque sorte affecter le travail de l'éditeur, par exemple, ajouter des éléments de données d'état ou gérer des événements d'interface.

Tous ces aspects sont conçus pour fonctionner avec un tableau de valeurs de configuration en utilisant les stratégies décrites dans la section précédente. Par exemple, lors de la spécification de plusieurs affectations de clés, l'ordre dans lequel les instances de plug-in de mappage de clés sont spécifiées détermine leur priorité. Le premier keymap qui sait comment le gérer reçoit une clé spécifique lors du traitement.

Habituellement, ce mécanisme est assez puissant et il est activement utilisé. Cependant, à un certain stade, cela devient compliqué et il devient inconfortable de travailler avec.

  • Si le plugin a de nombreux effets, vous pouvez soit espérer que dans cet ordre ils seront appliqués à d'autres plugins, soit vous devrez les diviser en plus petits plugins afin de pouvoir les organiser correctement.
  • En général, l'organisation des plugins devient très sensible, car l'utilisateur final ne comprend pas toujours quels plugins peuvent affecter le fonctionnement d'autres plugins s'ils obtiennent une priorité plus élevée. Toutes les erreurs n'apparaissent généralement qu'au moment de l'exécution, lors de l'utilisation de fonctionnalités spécifiques - par conséquent, elles sont faciles à manquer.
  • Les plugins basés sur d'autres plugins devraient documenter ce fait - et il reste à espérer que les utilisateurs n'oublieront pas d'activer leurs dépendances (dans le bon ordre).

CodeMirror dans la version 6 est un éditeur réécrit du même nom . Dans la sixième version, j'essaie de développer une approche modulaire. Cela nécessite un système d'extension plus expressif. Examinons certains des défis associés à la conception d'un tel système.

Commande


Il est facile de concevoir un système qui offre un contrôle complet sur l'ordre des extensions. Mais il est très difficile de concevoir un tel système, qui en même temps sera agréable à utiliser et vous permettra de combiner le code d'extensions indépendantes sans intervention manuelle approfondie et approfondie.

Quand il s'agit de commander, il tire pour appliquer des valeurs de priorité. Un exemple similaire est la propriété CSS z-index , qui vous permet de définir un nombre qui indique la profondeur de l'élément sur la pile.

Étant donné que les feuilles de style ont parfois des valeurs d' z-index ridiculement grandes, il est évident que cette façon d'indiquer la priorité est problématique. Un module particulier «ne sait pas» individuellement quelles valeurs de priorité indiquent d'autres modules. Les options ne sont que des points dans une plage de nombres indéfinie. Vous pouvez spécifier des valeurs prohibitives (ou des valeurs profondément négatives), en espérant atteindre les extrémités de cette échelle, mais tout le reste est un jeu de devinettes.

Cette situation peut être quelque peu améliorée en définissant un ensemble limité de catégories de priorités clairement définies afin que les extensions puissent être classées selon le «niveau» approximatif de leur priorité. Mais vous devez encore en quelque sorte rompre les liens au sein de ces catégories.

Regroupement et déduplication


Comme je l'ai mentionné ci-dessus, une fois que vous commencez à compter sérieusement sur les extensions, des situations peuvent survenir dans lesquelles certaines extensions en utiliseront d'autres. La gestion des dépendances mutuelles n'est pas très évolutive, il serait donc intéressant de pouvoir extraire un groupe d'extensions à la fois.

Cependant, non seulement que, dans ce cas, le problème de la commande aggravera encore plus; un autre problème se posera. De nombreuses autres extensions peuvent dépendre d'une extension particulière à la fois, et si vous les représentez comme des valeurs, alors la situation avec plusieurs téléchargements de la même extension peut très bien se produire. Dans certains cas, par exemple, lors de l'attribution de clés ou de la gestion des gestionnaires d'événements, cela est normal. Dans d'autres, par exemple, lors du suivi de l'historique des annulations ou de l'utilisation d'une bibliothèque d'infobulles, une telle approche serait un gaspillage de ressources avec le risque de casser quelque chose.

Par conséquent, en permettant la composition des extensions, nous sommes obligés de passer au système d'extension de la complexité associée à la gestion des dépendances. Vous devez être en mesure de reconnaître les extensions qui ne doivent pas être dupliquées et de télécharger une seule instance de chacune d'entre elles.

Cependant, dans la plupart des cas, les extensions peuvent être configurées et toutes les instances d'une extension particulière seront quelque peu différentes les unes des autres, nous ne pouvons pas simplement prendre une instance de l'extension et l'utiliser - nous devrons les combiner de manière significative (ou signaler une erreur, lorsque cela n'est pas possible).

Projet


Je décrirai ici ce qui a été fait dans CodeMirror 6. Je propose cet exemple comme solution, et non comme la seule vraie solution. Il est possible que ce système se développe davantage à mesure que la bibliothèque se stabilise.

La principale primitive de cette approche est appelée comportement. Les comportements ne sont que les éléments que vous pouvez développer en indiquant des valeurs. À titre d'exemple, considérons le comportement d'un champ d'état, où, à l'aide d'extensions, vous pouvez ajouter de nouveaux champs, en donnant une description de chaque champ. Ou le comportement d'un gestionnaire d'événements basé sur un navigateur, où vous pouvez ajouter vos propres gestionnaires à l'aide d'extensions.

Du point de vue du comportement du consommateur, les comportements qui sont configurés dans une instance particulière de l'éditeur donnent une séquence ordonnée de valeurs, où les valeurs avec une priorité plus élevée viennent en premier. Chaque comportement a un type et ses valeurs doivent correspondre à ce type.

Un comportement est représenté comme une valeur utilisée à la fois pour déclarer une instance d'un comportement et pour faire référence aux valeurs que le comportement peut avoir. Par exemple, une extension qui définit l'arrière-plan d'un numéro de ligne peut définir un comportement qui permet à un autre code d'ajouter de nouveaux marqueurs à cet arrière-plan.

Une extension est une valeur qui peut être utilisée lors de la configuration de l'éditeur. Un tableau d'extensions est signalé lors de l'initialisation. Chaque extension est autorisée dans zéro ou plusieurs comportements.

Le type d'extension le plus simple est une instance de comportement. En définissant une valeur pour ce comportement, nous obtenons en réponse la valeur de l'extension qui implémente ce comportement.
Une séquence d'extensions peut également être regroupée en une seule extension. Par exemple, dans la configuration de l'éditeur pour un langage de programmation donné, un certain nombre d'autres extensions peuvent être extraites, en particulier, une grammaire pour analyser et mettre en surbrillance le langage, des informations sur la façon de mettre en retrait et également une source d'informations sur l'achèvement qui complète intelligemment le code dans ce langage. Vous obtenez donc une extension de langue, qui a simplement collecté toutes les extensions nécessaires qui donnent la valeur cumulée.

Décrivant une version simple d'un tel système, nous pourrions nous arrêter à cela et simplement adapter les extensions imbriquées dans un seul tableau d'extensions pour les comportements. Ensuite, ils pourraient être regroupés par type de comportement et obtenir des séquences ordonnées de valeurs de comportement.

Cependant, nous n'avons toujours pas trouvé de déduplication, et nous avons besoin d'un contrôle plus complet sur la commande.

Les valeurs du troisième type incluent des extensions uniques ; il s'agit du mécanisme permettant d'assurer la déduplication. Les extensions que vous ne souhaitez pas instancier deux fois dans le même éditeur ne sont que cela. Pour définir une telle extension, un type de spécification est spécifié, c'est-à-dire le type de valeur de configuration attendu par le constructeur de l'extension, et une fonction d'instanciation qui prend un tableau de ces valeurs de spécification et renvoie l'extension.
Les extensions uniques compliquent le processus de résolution d'une collection d'extensions en un ensemble de comportements. Tant qu'il y a des extensions uniques dans l'ensemble aligné d'extensions, le mécanisme de résolution doit sélectionner le type d'extension unique, collecter toutes ses instances, appeler la fonction d'instanciation avec leurs valeurs de spécification et les remplacer par le résultat (dans une instance).

(Il y a un autre problème - ils doivent être résolus dans un certain ordre. Si vous activez d'abord l'extension unique X, mais ensuite l'extension Y se résout en un autre X, ce sera une erreur, car toutes les instances de X doivent être combinées ensemble. Puisque l'instanciation des extensions est une opération pure, le système, face à lui, l'exécute par essais et erreurs, redémarre le processus - et enregistre les informations clarifiées.)
Enfin, parlons de priorité. L'approche de base dans ce cas consiste à conserver l'ordre dans lequel les extensions ont été signalées. Les extensions composées sont alignées et intégrées dans cet ordre exactement à la position où elles se rencontrent pour la première fois. Le résultat de la résolution d'une extension unique est également inséré à l'endroit où il se produit pour la première fois.

Mais les extensions peuvent affecter certaines de leurs sous-extensions à une catégorie avec une priorité différente. Le système détermine les types de ces catégories: restauration (prend effet après que d'autres choses se produisent), par défaut, développez (une priorité plus élevée que la masse) et redéfinissez (peut-être devrait être situé tout en haut). La commande réelle s'effectue d'abord par catégorie, puis par position de départ.

Ainsi, une extension avec une affectation de clé de faible priorité et un gestionnaire d'événements avec une priorité normale peuvent nous donner une extension composée construite sur la base d'une extension avec une affectation de clé (dans ce cas, vous n'avez pas besoin de savoir quels comportements y sont inclus, avec la priorité «rollback» plus une instance de comportement gestionnaire d'événements.

La principale réalisation semble être que nous avons acquis la capacité de combiner des extensions, indépendamment de ce qui se fait à l'intérieur de chacune d'elles. Dans les extensions que nous avons modélisées jusqu'à présent, parmi lesquelles: deux systèmes d'analyse avec le même comportement syntaxique, la coloration syntaxique, le service d'indentation intelligent, l'historique des annulations, l'arrière-plan du numéro de ligne, la fermeture automatique des parenthèses, l'affectation des touches et la sélection multiple - tout fonctionne bien.

Pour utiliser un tel système, il faut vraiment maîtriser plusieurs nouveaux concepts, et c'est définitivement plus compliqué que les systèmes impératifs traditionnels acceptés dans la communauté JavaScript (appeler une méthode pour ajouter / supprimer un effet). Cependant, la possibilité de lier correctement les extensions semble justifier ces coûts.

Source: https://habr.com/ru/post/fr484470/


All Articles