Chorda. Essayer de le faire de façon déclarative

Chaque fois que je dois m'asseoir pour créer une nouvelle application, je tombe dans une stupeur facile. Ma tête tourne de la nécessité de choisir la bibliothèque ou le framework à prendre ce temps. La dernière fois que j'ai écrit sur la bibliothèque X, mais maintenant le framework Y a grandi et a été transféré, et il y a toujours un Kit UI cool Z, et beaucoup de travail a été laissé par les projets précédents.


À partir d'un certain point, j'ai réalisé que le cadre n'a pas vraiment d'importance - ce dont j'ai besoin, je peux le faire sur n'importe lequel d'entre eux. Ici, il semble que vous devriez être heureux, prendre quelque chose avec le maximum d'étoiles sur le github et se calmer. Mais tout de même, un désir irrésistible naît constamment de faire quelque chose de lui-même, son propre vélo. Eh bien. Quelques réflexions générales sur ce sujet et un cadre appelé Chorda vous attendent sous la coupe.


En fait, le problème n'est pas que la décision de quelqu'un d'autre soit mauvaise ou inefficace. Non. Le problème est que la décision de quelqu'un d'autre nous fait penser d'une manière qui peut ne pas nous convenir. Mais attendez. Que signifie «pratique-incommode» et comment cela peut-il même affecter le développement? Rappelons qu'il existe une chose telle que DX, en fait - un ensemble de pratiques personnelles établies et généralement acceptées. De là, nous pouvons dire que cela nous convient lorsque notre propre DX coïncide avec le DX de l'auteur de la bibliothèque ou du framework. Et dans le cas où ils divergent, l'inconfort même, l'irritation et la recherche de quelque chose de nouveau surviennent.


Un peu d'histoire


Lorsque vous développez une interface utilisateur pour une application d'entreprise, vous êtes confronté à un grand nombre de formulaires utilisateur. Et un jour, une pensée brillante me vient à l'esprit: pourquoi est-ce que je crée un formulaire Web à chaque fois que je peux simplement lister les champs dans JSON et alimenter la structure résultante au générateur? Et, bien que dans le monde de l'entreprise sanglante, une telle approche ne fonctionne pas trop bien (pourquoi donc, c'est une conversation séparée), mais l'idée de passer d'un style impératif à un style déclaratif n'est généralement pas mauvaise. La preuve en est le grand nombre de générateurs de formulaires Web, de pages et même de sites entiers qui peuvent être facilement trouvés sur le Web.


Donc, à un moment donné, je n'étais pas étranger au désir d'améliorer mon code en raison de la transition vers la déclarativité. Mais dès que nous avions besoin non seulement d'éléments html standard, mais de composants de widget complexes et interactifs, nous ne pouvions pas nous en sortir avec un simple générateur. Les exigences de réutilisation, d'intégrabilité, d'extensibilité, etc. du code s'y sont rapidement ajoutées. Développer votre propre bibliothèque de composants avec une API déclarative ne s'est pas fait attendre.


Mais ici, le bonheur n'est pas arrivé. La meilleure situation reflétera probablement l'opinion de mon collègue, qui devait utiliser la bibliothèque créée. Il a regardé les exemples, la documentation et a dit: "La bibliothèque est cool. Belle, dynamique. Mais comment puis-je faire une application à partir de tout cela?" Et il avait raison. Il s'est avéré que fabriquer un composant n'est pas la même chose que combiner plusieurs composants ensemble et les faire fonctionner de manière transparente.


Beaucoup de temps s'est écoulé depuis lors. Et quand une fois de plus j'ai été visité par le désir de rassembler les pensées et les développements, j'ai décidé d'agir un peu différemment et de ne pas aller de bas en haut, mais de haut en bas.


Gestion des applications == Gestion des états


Je suis plus habitué à considérer l'application comme une machine à états finis avec un ensemble d'états clones. Et le travail de l'application comme un ensemble de transitions d'un état à un autre, dans lequel le changement de modèle conduit à la création d'une nouvelle version de la vue. À l'avenir , j'appellerai certaines données fixes (un objet, un tableau, un type primitif, etc.) liées à leur seule représentation - un document .


Il y a un problème évident - pour de nombreuses valeurs du modèle, il est nécessaire de décrire de nombreuses options pour le document. Deux approches sont généralement utilisées ici:


  1. Modèles. Nous utilisons notre langage de balisage préféré et le complétons avec des directives de branchement et de bouclage.
  2. Les fonctions Nous décrivons dans nos fonctions nos branches et boucles dans notre langage de programmation préféré.

En règle générale, ces deux approches sont déclarées déclaratives. Le premier est considéré comme déclaratif, car il est basé sur, quoique légèrement développé, mais sur les règles du langage de balisage. Le second - parce qu'il se concentre sur la composition des fonctions, dont un certain nombre agissent comme des règles. Ce qui est remarquable, il n'y a pas de frontière claire entre les modèles et les fonctions maintenant.


D'une part, j'aime les modèles, mais d'autre part, je voulais en quelque sorte utiliser les fonctionnalités de javascript. Par exemple, quelque chose comme ceci:


createFromConfig({ data: { name: 'Alice' }, tag: 'div', class: 'clickable box', onClick: function () { alert('Click') } }) 

Le résultat est une configuration JS qui décrit un état spécifique entier. Pour décrire les nombreux états, il sera nécessaire de réaliser l'extensibilité de cette configuration. Et quel est le moyen le plus pratique de rendre un ensemble d'options extensible? Nous n'inventerons rien ici - les options de surcharge existent depuis longtemps. Comment cela fonctionne peut être vu dans l'exemple de Vue avec son API Options. Mais, contrairement à la même Vue, je me demandais si l'état complet, y compris les données et le document, pouvait être décrit de la même manière.


Structure d'application et caractère déclaratif


Le terme «composant» est devenu trop vague, surtout après l’apparition de ce que l’on appelle composants fonctionnels. Alors que nous passons à la structure de l'application, j'appellerai le composant un élément structurel .

Très rapidement, je suis arrivé à la conclusion que l'élément structurel (composant) n'est pas un élément de document, mais une entité, qui:


  1. combine des données et des documents (liaison et événements)
  2. connecté à d'autres entités similaires (structure arborescente)

Comme je l'ai souligné précédemment, si vous percevez l'application comme un ensemble d'états, alors pour ces états, vous devez avoir une méthode de description. De plus, il est nécessaire de trouver une telle méthode pour qu'elle ne contienne pas d'opérateurs impératifs "faux". Nous parlons de ces éléments très auxiliaires qui sont introduits dans les modèles - #if , #elsif , v-for , etc. Je pense que beaucoup de gens connaissent déjà la solution - il est nécessaire de transférer la logique au modèle, en laissant au niveau de la présentation une API qui vous permet de contrôler les éléments structurels à travers des types de données simples.


Par gestion, je comprends la présence de variabilité et de cyclicité.


Variabilité (if-else)


Voyons comment vous pouvez contrôler les options d'affichage en utilisant l'exemple d'un composant de carte dans Chorda:


 const isHeaderOnly = true const card = new Html({ $header: { /*  */ }, $footer: { /*  */ }, components: {header: true, footer: !isHeaderOnly} //    }) 

En définissant la valeur de l'option des composants , vous pouvez contrôler les composants affichés. Et lors de la liaison des composants avec le stockage réactif, nous obtenons que notre structure passera sous la gestion des données. Il y a une mise en garde: l'objet est utilisé comme valeur et les clés ne sont pas ordonnées, ce qui impose certaines restrictions sur les composants .


Cycle (pour)


L'utilisation de données dont la quantité n'est connue qu'au moment de l'exécution nécessitera une itération sur les listes.


 const drinks = ['Coffee', 'Tea', 'Milk'] const html = new Html({ html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: drinks }) 

La valeur de l'option items est Array, respectivement, nous obtenons un ensemble ordonné de composants. La liaison d' éléments au stockage, comme dans le cas des composants, transférera le contrôle aux données.


Les éléments structurels sont connectés les uns aux autres dans une hiérarchie arborescente. Si nous combinons les exemples précédents, alors pour afficher la liste dans le corps de la carte, nous obtenons ce qui suit:


 //   const state = { struct: { header: true, footer: false, }, drinks: ['Coffee', 'Tea', 'Milk'] } //  const card = new Html({ $header: { /*  */ }, $content: { html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: state.drinks }, $footer: { /*  */ }, components: state.struct }) 

De cette façon, la structure de données de l'application est créée. Il suffit d'avoir deux types de générateurs - basés sur Object et basés sur Array. Il ne reste plus qu'à comprendre comment se produit la transformation des éléments structurels en document.


Quand tout est déjà inventé pour nous


En général, je suis en faveur du fait que le système de rendu des documents devrait être implémenté au niveau du navigateur (bien qu'au moins le même VDOM). Et notre tâche ne sera que de le connecter soigneusement à l'arborescence des composants. Après tout, quelle que soit la vitesse de la bibliothèque, le navigateur l'a de toute façon.


Honnêtement, j'ai essayé de faire ma fonction de rendu un jour, mais après un certain temps, j'ai abandonné, car je ne peux pas dessiner plus vite que VanillaJS (malheureusement!). Il est maintenant à la mode d'utiliser VDOM pour le rendu, et ses implémentations sont peut-être même abondantes. Donc, plus une implémentation de plus de l'arborescence virtuelle, j'ai décidé de ne pas l'ajouter à la tirelire du github - le prochain framework suffit.


Initialement, un adaptateur pour la bibliothèque Maquette a été créé à Chorda pour le rendu, mais dès que des tâches «du monde réel» ont commencé à apparaître, il s'est avéré qu'il était plus pratique d'avoir un tiroir sur React. Dans ce cas, par exemple, vous pouvez simplement utiliser les React DevTools existants et ne pas écrire les vôtres.


Pour connecter VDOM avec des éléments structurels, vous avez besoin d'une telle disposition . Elle peut être appelée fonction documentaire d'un élément structurel. Ce qui est important, c'est une fonction pure.


Prenons un exemple avec une carte qui a un en-tête, un corps et un sous-sol. Il a déjà été mentionné que les composants ne sont pas commandés, c'est-à-dire si nous commençons à allumer / éteindre les composants pendant le fonctionnement, ils apparaîtront à chaque fois dans une nouvelle commande. Voyons comment cela est résolu par la mise en page:


 function orderedByKeyLayout (h, type, props, components) { return h(type, props, components.sort((a, b) => a.key - b.key).map(c => c.render())) } const html = new Html({ $header: {}, $content: {}, $footer: {}, layout: orderedByKeyLayout //     }) 

La disposition vous permet de configurer le soi-disant L'élément hôte auquel le composant est associé et ses enfants ( éléments et composants ). Habituellement, une mise en page standard suffit également, mais dans certains cas, la mise en page nécessite la présence d'éléments wrapper (par exemple, pour les grilles) ou l'affectation de classes spéciales, que nous ne voulons pas prendre au niveau des composants.


Une pincée de réactivité


Après avoir déclaré et dessiné la structure des composants, nous obtenons un état correspondant à un ensemble de données spécifique. Ensuite, nous devons décrire les nombreux ensembles de données et la réaction à leur changement.


Lorsque je travaillais avec des données, je n'aimais pas deux choses:


  • Immunité. Une bonne chose pour suivre les changements est le versioning pour les pauvres, ce qui fonctionne très bien sur les objets primitifs et plats. Mais dès que la structure devient plus complexe et que le nombre d'investissements augmente, il devient difficile de maintenir l'immunité d'un objet complexe.
  • Remplacement. Si je mets un objet dans l'entrepôt de données, lorsque je le demande, je peux en renvoyer une copie ou un autre objet ou proxy en général qui a une similitude structurelle avec lui.

Je voulais avoir un référentiel qui se comporte comme immuable, mais à l'intérieur il contient des données mutables, qui maintiennent également la persistance des liens. Dans le cas idéal, cela ressemblerait à ceci: je crée un référentiel, j'y écris un objet vide, je commence à saisir des données à partir du formulaire de demande, et après avoir cliqué sur le bouton soumettre, j'obtiens le même objet (le même lien!) Avec les propriétés remplies. J'appelle ce cas idéal, car il n'arrive pas souvent que le modèle de stockage corresponde au modèle de présentation.


Une autre tâche qui doit être résolue consiste à fournir des données du stockage aux éléments structurels. Encore une fois, nous n'inventerons rien et utiliserons l'approche de la connexion à un contexte commun. Dans le cas de Chorda, nous n'avons pas accès au contexte lui-même, mais seulement à son affichage, appelé le scope . De plus, la portée du composant est le contexte de ses composants enfants. Cette approche vous permet d'affiner, d'étendre ou de remplacer les données connexes à n'importe quel niveau de notre application, et ces modifications seront isolées.


Un exemple de la façon dont les données contextuelles sont réparties sur une arborescence de composants:


 const html = new Html({ //     scope: { drink: 'Coffee' }, $component1: { scope: { cups: 2 }, $content: { $myDrink: { //      ,    drinkChanged: function (v) { //    drink   text this.opt('text', v) } }, $numCups: { cupsChanged: function (v) { this.opt('text', v + ' cups') } } } }, $component2: { scope: { drink: 'Tea' //      drink }, drinkChanged: function (v) { //    drink   text this.opt('text', v) } } }) //    // <div> // <div> // <div> // <div>Coffee</div> // <div>2 cups</div> // </div> // </div> // <div>Tea</div> // </div> 

Le moment le plus difficile à comprendre est que chaque composant a son propre contexte, et non celui déclaré tout en haut de la structure, comme nous le faisons habituellement lorsque nous travaillons avec des modèles.


Qu'en est-il de la surcharge d'options?


Vous êtes sûrement confronté à une situation où il y a un grand composant et il est nécessaire de changer un petit composant imbriqué quelque part profondément à l'intérieur. Ils disent que la granulation et la composition devraient aider ici. Et aussi, que les composants et l'architecture doivent être conçus tout de suite. La situation devient très triste si la grande composante n'est pas la vôtre, mais fait partie d'une bibliothèque développée par une autre équipe ou même une communauté indépendante. Et s'ils pouvaient facilement apporter des modifications au composant de base, même s'ils n'étaient pas initialement prévus?


Habituellement, les composants des bibliothèques sont conçus comme des classes, puis ils peuvent être utilisés comme base pour créer de nouveaux composants. Mais voici une petite fonctionnalité cachée que je n'ai jamais aimé: parfois nous créons une classe juste pour l'appliquer en un seul endroit. C'est bizarre. Par exemple, j'ai l'habitude d'utiliser des classes pour taper, de créer des relations entre des groupes d'objets et de ne pas les utiliser pour résoudre le problème de décomposition.


Voyons comment les classes fonctionnent avec la configuration dans Chorda.


 //      class Card extends Html { config () { return { css: 'box', $header: {}, $content: {}, $footer: {} } } } const html = new Html({ css: 'panel', $card: { as: Card, $header: { //       title $title: { css: 'title', text: 'Card title' } } } }) 

J'aime plus cette option que la création d'une classe TitledCard spéciale qui ne sera utilisée qu'une seule fois. Et si vous devez faire partie des options, vous pouvez utiliser le mécanisme d'impureté. Eh bien, personne n'a annulé Object.assign.


À Chorda, une classe est essentiellement un conteneur pour la configuration et joue le rôle d'une sorte spéciale d'impureté.


Pourquoi un autre cadre?


Je répète qu'à mon avis, le cadre porte davantage sur la façon de penser et l'expérience que sur la technologie. Mes habitudes et DX ont demandé une déclarativité dans JS, que je n'ai pas pu trouver dans d'autres solutions. Mais l'implémentation d'une fonctionnalité en a tiré de nouvelles, et après un certain temps, elles ont tout simplement cessé de s'intégrer dans le cadre d'une bibliothèque spécialisée.


À l'heure actuelle, Chorda est en développement actif. Les directions principales sont déjà visibles, mais les détails changent constamment.


Merci d'avoir lu jusqu'au bout. Je serais ravi de revoir.


Où puis-je voir?


La documentation


Sources GitHub


Exemples CodePen

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


All Articles