Émuler React et JSX dans Vanilla JS

Publié: 2022-03-11

Peu de gens n'aiment pas les frameworks, mais même si vous en faites partie, vous devriez en prendre note et adopter les fonctionnalités qui facilitent un peu la vie.

J'étais opposé à l'utilisation de frameworks dans le passé. Cependant, ces derniers temps, j'ai eu l'expérience de travailler avec React et Angular dans certains de mes projets. Les premières fois où j'ai ouvert mon éditeur de code et commencé à écrire du code dans Angular, cela m'a semblé bizarre et contre nature; surtout après plus de dix ans de codage sans utiliser aucun framework. Au bout d'un moment, j'ai décidé de m'engager dans l'apprentissage de ces technologies. Très rapidement, une grande différence est apparue : il était si facile de manipuler le DOM, si facile d'ajuster l'ordre des nœuds en cas de besoin, et il n'a pas fallu des pages et des pages de code pour créer une interface utilisateur.

Bien que je préfère toujours la liberté de ne pas être attaché à un framework ou à une architecture, je ne pouvais pas ignorer le fait que créer des éléments DOM dans un est beaucoup plus pratique. J'ai donc commencé à chercher des moyens d'imiter l'expérience de vanilla JS. Mon objectif est d'extraire certaines de ces idées de React et de démontrer comment les mêmes principes peuvent être implémentés en JavaScript simple (souvent appelé vanilla JS) pour faciliter un peu la vie des développeurs. Pour ce faire, construisons une application simple pour parcourir les projets GitHub.

Application de recherche GitHub simple

L'application que nous construisons.

Quelle que soit la manière dont nous construisons un frontal en utilisant JavaScript, nous accéderons et manipulerons le DOM. Pour notre application, nous devrons construire une représentation de chaque référentiel (vignette, nom et description) et l'ajouter au DOM en tant qu'élément de liste. Nous utiliserons l'API de recherche GitHub pour récupérer nos résultats. Et, puisque nous parlons de JavaScript, cherchons dans les référentiels JavaScript. Lorsque nous interrogeons l'API, nous obtenons la réponse JSON suivante :

 { "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }

L'approche de React

React rend très simple l'écriture d'éléments HTML dans la page et est l'une des fonctionnalités que j'ai toujours voulu avoir lors de l'écriture de composants en JavaScript pur. React utilise JSX, qui est très similaire au HTML normal.

Cependant, ce n'est pas ce que le navigateur lit.

Sous le capot, React transforme JSX en un tas d'appels à une fonction React.createElement . Examinons un exemple de JSX utilisant un élément de l'API GitHub et voyons en quoi cela se traduit.

 <div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
 ; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );

JSX est très simple. Vous écrivez du code HTML normal et injectez des données à partir de l'objet en ajoutant des accolades. Le code JavaScript entre parenthèses sera exécuté et la valeur sera insérée dans le DOM résultant. L'un des avantages de JSX est que React crée un DOM virtuel (une représentation virtuelle de la page) pour suivre les modifications et les mises à jour. Au lieu de réécrire tout le HTML, React modifie le DOM de la page chaque fois que les informations sont mises à jour. C'est l'un des principaux problèmes que React a été créé pour résoudre.

Approche jQuery

Les développeurs utilisaient beaucoup jQuery. J'aimerais le mentionner ici parce qu'il est toujours populaire et aussi parce qu'il est assez proche de la solution en JavaScript pur. jQuery obtient une référence à un nœud DOM (ou à une collection de nœuds DOM) en interrogeant le DOM. Il enveloppe également cette référence avec diverses fonctionnalités pour modifier son contenu.

Bien que jQuery ait ses propres outils de construction DOM, ce que je vois le plus souvent dans la nature, c'est simplement la concaténation HTML. Par exemple, nous pouvons insérer du code HTML dans les nœuds sélectionnés en appelant la fonction html() . Selon la documentation de jQuery, si nous voulons modifier le contenu d'un nœud div avec la classe demo-container nous pouvons le faire comme ceci :

 $( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

Cette approche facilite la création d'éléments DOM ; cependant, lorsque nous devons mettre à jour des nœuds, nous devons interroger les nœuds dont nous avons besoin ou (plus communément) revenir à recréer l'intégralité de l'extrait chaque fois qu'une mise à jour est requise.

Approche API DOM

JavaScript dans les navigateurs a une API DOM intégrée qui nous donne un accès direct à la création, la modification et la suppression des nœuds d'une page. Cela se reflète dans l'approche de React, et en utilisant l'API DOM, nous nous rapprochons un peu plus des avantages de cette approche. Nous modifions uniquement les éléments de la page qui nécessitent réellement d'être modifiés. Cependant, React garde également une trace d'un DOM virtuel séparé. En comparant les différences entre le DOM virtuel et le DOM réel, React est alors en mesure d'identifier les parties nécessitant une modification.

Ces étapes supplémentaires sont parfois utiles, mais pas toujours, et la manipulation directe du DOM peut être plus efficace. Nous pouvons créer de nouveaux nœuds DOM en utilisant la fonction _document.createElement_ , qui renverra une référence au nœud créé. Garder une trace de ces références nous donne un moyen facile de modifier uniquement les nœuds qui contiennent la partie qui doit être mise à jour.

En utilisant la même structure et la même source de données que dans l'exemple JSX, nous pouvons construire le DOM de la manière suivante :

 var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);

Si la seule chose qui vous préoccupe est l'efficacité de l'exécution du code, alors cette approche est très bonne. Cependant, l'efficacité ne se mesure pas seulement à la vitesse d'exécution, mais aussi à la facilité de maintenance, à l'évolutivité et à la plasticité. Le problème avec cette approche est qu'elle est très verbeuse et parfois alambiquée. Nous devons écrire un tas d'appels de fonction même si nous construisons simplement une structure de base. Le deuxième gros inconvénient est le grand nombre de variables créées et suivies. Disons qu'un composant sur lequel vous travaillez contient 30 éléments DOM, vous devrez créer et utiliser 30 éléments et variables DOM différents. Vous pouvez réutiliser certains d'entre eux et jongler au détriment de la maintenabilité et de la plasticité, mais cela peut devenir très compliqué, très rapidement.

Un autre inconvénient important est dû au nombre de lignes de code que vous devez écrire. Avec le temps, il devient de plus en plus difficile de déplacer des éléments d'un parent à un autre. C'est une chose que j'apprécie vraiment de React. Je peux afficher la syntaxe JSX et obtenir en quelques secondes quel nœud est contenu, où et le modifier si nécessaire. Et, bien qu'il puisse sembler que ce n'est pas un gros problème au début, la plupart des projets ont des changements constants qui vous feront chercher une meilleure façon.

Solution proposée

Travailler directement avec le DOM fonctionne et fait le travail, mais cela rend également la construction de la page très détaillée, en particulier lorsque nous devons ajouter des attributs HTML et des nœuds imbriqués. Donc, l'idée serait de capturer certains des avantages de travailler avec des technologies comme JSX et de nous simplifier la vie. Les avantages que nous essayons de reproduire sont les suivants :

  1. Écrivez du code dans la syntaxe HTML afin que la création d'éléments DOM devienne facile à lire et à modifier.
  2. Comme nous n'utilisons pas d'équivalent du DOM virtuel comme dans le cas de React, nous devons disposer d'un moyen simple d'indiquer et de suivre les nœuds qui nous intéressent.

Voici une fonction simple qui accomplirait cela en utilisant un extrait de code HTML.

 Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }

L'idée est simple mais puissante; nous envoyons à la fonction le code HTML que nous voulons créer sous forme de chaîne, dans la chaîne HTML, nous ajoutons un attribut var aux nœuds que nous voulons créer pour nous. Le deuxième paramètre est un objet dans lequel ces références seront stockées. S'il n'est pas spécifié, nous créerons une propriété "nœuds" sur le nœud ou le fragment de document renvoyé (au cas où le nœud de la hiérarchie la plus élevée est plus d'un). Tout est accompli en moins de 60 lignes de code.

La fonction fonctionne en trois étapes :

  1. Créez un nouveau nœud vide et utilisez innerHTML dans ce nouveau nœud pour créer la structure DOM entière.
  2. Itérez sur les nœuds et si l'attribut var existe, ajoutez une propriété dans l'objet de portée qui pointe vers le nœud avec cet attribut.
  3. Renvoie le nœud supérieur de la hiérarchie, ou un fragment de document s'il y en a plusieurs.

Alors, à quoi ressemble notre code pour le rendu de l'exemple maintenant ?

 var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;

Tout d'abord, nous définissons l'objet (UI) où nous stockerons les références aux nœuds créés. Nous composons ensuite le modèle HTML que nous allons utiliser, sous forme de chaîne, en marquant les nœuds cibles avec un attribut "var". Après cela, nous appelons la fonction Browser.DOM avec le modèle et l'objet vide qui stockera les références. Enfin, nous utilisons les références stockées pour placer les données à l'intérieur des nœuds.

Cette approche sépare également la construction de la structure DOM et l'insertion des données dans des étapes distinctes, ce qui aide à garder le code organisé et bien structuré. Cela nous permet de créer séparément la structure DOM et de remplir (ou de mettre à jour) les données lorsqu'elles sont disponibles.

Conclusion

Bien que certains d'entre nous n'aiment pas l'idée de passer aux frameworks et de céder le contrôle, il est important que nous reconnaissions les avantages que ces frameworks apportent. Il y a une raison pour laquelle ils sont si populaires.

Et bien qu'un framework ne convienne pas toujours à votre style ou à vos besoins, certaines fonctionnalités et techniques peuvent être adoptées, émulées ou parfois même découplées d'un framework. Certaines choses seront toujours perdues lors de la traduction, mais beaucoup peut être gagné et utilisé à une infime fraction du coût d'un framework.