Emulazione di React e JSX in Vanilla JS

Pubblicato: 2022-03-11

Poche persone non amano i framework, ma anche se sei uno di loro, dovresti prenderne nota e adottare le funzionalità che rendono la vita un po' più facile.

In passato ero contrario all'uso dei framework. Tuttavia, ultimamente, ho avuto l'esperienza di lavorare con React e Angular in alcuni dei miei progetti. Le prime due volte che ho aperto il mio editor di codice e ho iniziato a scrivere codice in Angular mi è sembrato strano e innaturale; soprattutto dopo più di dieci anni di programmazione senza l'utilizzo di alcun framework. Dopo un po', ho deciso di impegnarmi ad apprendere queste tecnologie. Molto rapidamente è emersa una grande differenza: era così facile manipolare il DOM, così facile regolare l'ordine dei nodi quando necessario e non ci volevano pagine e pagine di codice per costruire un'interfaccia utente.

Anche se preferisco ancora la libertà di non essere attaccato a un framework oa un'architettura, non potevo ignorare il fatto che creare elementi DOM in uno è molto più conveniente. Così ho iniziato a cercare modi per emulare l'esperienza in vanilla JS. Il mio obiettivo è estrarre alcune di queste idee da React e dimostrare come gli stessi principi possono essere implementati in JavaScript semplice (spesso indicato come JS vaniglia) per rendere un po' più facile la vita degli sviluppatori. Per fare ciò, creiamo una semplice app per sfogliare i progetti GitHub.

Semplice app di ricerca GitHub

L'app che stiamo costruendo.

In qualunque modo stiamo costruendo un front-end utilizzando JavaScript, accederemo e manipoleremo il DOM. Per la nostra applicazione, dovremo costruire una rappresentazione di ciascun repository (miniatura, nome e descrizione) e aggiungerla al DOM come elemento di elenco. Utilizzeremo l'API di ricerca di GitHub per recuperare i nostri risultati. E, dal momento che stiamo parlando di JavaScript, cerchiamo nei repository JavaScript. Quando interroghiamo l'API, otteniamo la seguente risposta JSON:

 { "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'approccio di Reagire

React rende molto semplice scrivere elementi HTML nella pagina ed è una delle funzionalità che ho sempre voluto avere durante la scrittura di componenti in puro JavaScript. React utilizza JSX, che è molto simile al normale HTML.

Tuttavia, non è quello che legge il browser.

Sotto il cofano, React trasforma JSX in un mucchio di chiamate a una funzione React.createElement . Diamo un'occhiata a un esempio di JSX che utilizza un elemento dell'API GitHub e vediamo in cosa si traduce.

 <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 è molto semplice. Scrivi un normale codice HTML e inserisci i dati dall'oggetto aggiungendo parentesi graffe. Il codice JavaScript tra parentesi verrà eseguito e il valore verrà inserito nel DOM risultante. Uno dei vantaggi di JSX è che React crea un DOM virtuale (una rappresentazione virtuale della pagina) per tenere traccia di modifiche e aggiornamenti. Invece di riscrivere l'intero HTML, React modifica il DOM della pagina ogni volta che le informazioni vengono aggiornate. Questo è uno dei problemi principali per cui React è stata creata.

approccio jQuery

Gli sviluppatori usavano molto jQuery. Vorrei menzionarlo qui perché è ancora popolare e anche perché è abbastanza vicino alla soluzione in puro JavaScript. jQuery ottiene un riferimento a un nodo DOM (o una raccolta di nodi DOM) interrogando il DOM. Avvolge anche quel riferimento con varie funzionalità per modificarne il contenuto.

Mentre jQuery ha i suoi strumenti di costruzione DOM, la cosa che vedo più spesso in-the-wild è solo la concatenazione HTML. Ad esempio, possiamo inserire codice HTML nei nodi selezionati chiamando la funzione html() . Secondo la documentazione di jQuery, se vogliamo modificare il contenuto di un nodo div con la classe demo-container possiamo farlo in questo modo:

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

Questo approccio semplifica la creazione di elementi DOM; tuttavia, quando è necessario aggiornare i nodi, è necessario interrogare i nodi di cui abbiamo bisogno o (più comunemente) tornare a ricreare l'intero snippet ogni volta che è necessario un aggiornamento.

Approccio API DOM

JavaScript nei browser ha un'API DOM integrata che ci dà accesso diretto alla creazione, modifica ed eliminazione dei nodi in una pagina. Ciò si riflette nell'approccio di React e, utilizzando l'API DOM, ci avviciniamo di un passo ai vantaggi di tale approccio. Stiamo modificando solo gli elementi della pagina che effettivamente necessitano di essere modificati. Tuttavia, React tiene traccia anche di un DOM virtuale separato. Confrontando le differenze tra il DOM virtuale e quello reale, React è quindi in grado di identificare quali parti richiedono la modifica.

Questi passaggi aggiuntivi a volte sono utili, ma non sempre, e la manipolazione diretta del DOM può essere più efficiente. Possiamo creare nuovi nodi DOM usando la funzione _document.createElement_ , che restituirà un riferimento al nodo creato. Tenere traccia di questi riferimenti ci offre un modo semplice per modificare solo i nodi che contengono la parte che deve essere aggiornata.

Utilizzando la stessa struttura e origine dati dell'esempio JSX, possiamo costruire il DOM nel modo seguente:

 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);

Se l'unica cosa che hai in mente è l'efficienza dell'esecuzione del codice, allora questo approccio è molto buono. Tuttavia, l'efficienza non si misura solo nella velocità di esecuzione, ma anche nella facilità di manutenzione, scalabilità e plasticità. Il problema con questo approccio è che è molto dettagliato e talvolta contorto. Abbiamo bisogno di scrivere un mucchio di chiamate di funzione anche se stiamo semplicemente costruendo una struttura di base. Il secondo grande svantaggio è il numero di variabili create e tracciate. Diciamo che un componente su cui stai lavorando contiene i suoi 30 elementi DOM, dovrai creare e utilizzare 30 diversi elementi e variabili DOM. Puoi riutilizzarne alcuni e fare un po' di giocoleria a scapito della manutenibilità e della plasticità, ma può diventare davvero disordinato, molto rapidamente.

Un altro svantaggio significativo è dovuto al numero di righe di codice che devi scrivere. Con il tempo, diventa sempre più difficile spostare elementi da un genitore all'altro. Questa è una cosa che apprezzo molto di React. Posso visualizzare la sintassi JSX e ottenere in pochi secondi quale nodo è contenuto, dove e modificare se necessario. E, anche se all'inizio potrebbe sembrare che non sia un grosso problema, la maggior parte dei progetti ha cambiamenti costanti che ti faranno cercare un modo migliore.

La soluzione proposta

Lavorare direttamente con il DOM funziona e porta a termine il lavoro, ma rende anche la costruzione della pagina molto prolissa, specialmente quando dobbiamo aggiungere attributi HTML e nidificare nodi. Quindi, l'idea sarebbe quella di catturare alcuni dei vantaggi di lavorare con tecnologie come JSX e semplificarci la vita. I vantaggi che stiamo cercando di replicare sono i seguenti:

  1. Scrivi codice nella sintassi HTML in modo che la creazione di elementi DOM diventi facile da leggere e modificare.
  2. Dal momento che non stiamo usando un equivalente di DOM virtuale come nel caso di React, dobbiamo avere un modo semplice per indicare e tenere traccia dei nodi che ci interessano.

Ecco una semplice funzione che lo farebbe usando uno snippet 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'idea è semplice ma potente; inviamo alla funzione l'HTML che vogliamo creare come stringa, nella stringa HTML aggiungiamo un attributo var ai nodi che vogliamo avere i riferimenti creati per noi. Il secondo parametro è un oggetto in cui verranno archiviati quei riferimenti. Se non specificato creeremo una proprietà "nodes" sul nodo restituito o sul frammento di documento (nel caso in cui il nodo della gerarchia più alta sia più di uno). Tutto è realizzato in meno di 60 righe di codice.

La funzione funziona in tre fasi:

  1. Crea un nuovo nodo vuoto e usa innerHTML in quel nuovo nodo per creare l'intera struttura DOM.
  2. Iterare sui nodi e se l'attributo var esiste, aggiungere una proprietà nell'oggetto scope che punta al nodo con quell'attributo.
  3. Restituisce il nodo in alto nella gerarchia o un frammento di documento nel caso ce ne siano più di uno.

Quindi, come appare ora il nostro codice per il rendering dell'esempio?

 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;

Innanzitutto, definiamo l'oggetto (UI) in cui memorizzeremo i riferimenti ai nodi creati. Quindi componiamo il modello HTML che useremo, come una stringa, contrassegnando i nodi di destinazione con un attributo "var". Successivamente, chiamiamo la funzione Browser.DOM con il template e l'oggetto vuoto che memorizzerà i riferimenti. Infine, utilizziamo i riferimenti archiviati per posizionare i dati all'interno dei nodi.

Questo approccio separa anche la costruzione della struttura DOM e l'inserimento dei dati in passaggi separati che aiutano a mantenere il codice organizzato e ben strutturato. Questo ci consente di creare separatamente la struttura DOM e di compilare (o aggiornare) i dati quando diventano disponibili.

Conclusione

Sebbene ad alcuni di noi non piaccia l'idea di passare ai framework e di cedere il controllo, è importante riconoscere i vantaggi che tali framework apportano. C'è un motivo per cui sono così popolari.

E mentre un framework potrebbe non essere sempre adatto al tuo stile o alle tue esigenze, alcune funzionalità e tecniche possono essere adottate, emulate o talvolta anche disaccoppiate da un framework. Alcune cose andranno sempre perse nella traduzione, ma molto può essere guadagnato e utilizzato a una piccola frazione del costo che un framework comporta.