Il Santo Graal dei componenti riutilizzabili: elementi personalizzati, Shadow DOM e NPM

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Questo articolo esamina l'aumento dell'HTML con componenti che hanno funzionalità e stili incorporati. Impareremo anche come rendere questi elementi personalizzati riutilizzabili tra i progetti usando NPM.

Anche per i componenti più semplici, il costo del lavoro umano potrebbe essere stato significativo. I team UX eseguono test di usabilità. Una serie di parti interessate deve firmare il progetto.

Gli sviluppatori conducono test AB, audit di accessibilità, test unitari e controlli cross-browser. Una volta che hai risolto un problema, non vuoi ripetere quello sforzo . Creando una libreria di componenti riutilizzabile (piuttosto che costruire tutto da zero), possiamo utilizzare continuamente gli sforzi passati ed evitare di rivisitare le sfide di progettazione e sviluppo già risolte.

Uno screenshot del sito Web dei componenti materiali di Google, che mostra vari componenti.
Anteprima ampia

Costruire un arsenale di componenti è particolarmente utile per aziende come Google che possiedono un considerevole portafoglio di siti Web che condividono tutti un marchio comune. Codificando la propria interfaccia utente in widget componibili, le aziende più grandi possono accelerare i tempi di sviluppo e ottenere la coerenza della progettazione visiva e dell'interazione con l'utente in tutti i progetti. C'è stato un aumento di interesse per le guide di stile e le librerie di modelli negli ultimi anni. Dato che più sviluppatori e designer sono distribuiti su più team, le grandi aziende cercano di raggiungere la coerenza. Possiamo fare di meglio dei semplici campioni di colore. Quello di cui abbiamo bisogno è un codice facilmente distribuibile .

Condivisione e riutilizzo del codice

Copiare e incollare manualmente il codice è semplice. Mantenere aggiornato quel codice, tuttavia, è un incubo di manutenzione. Molti sviluppatori, quindi, si affidano a un gestore di pacchetti per riutilizzare il codice tra i progetti. Nonostante il nome, il Node Package Manager è diventato la piattaforma senza rivali per la gestione dei pacchetti front-end . Attualmente sono presenti oltre 700.000 pacchetti nel registro NPM e miliardi di pacchetti vengono scaricati ogni mese. Qualsiasi cartella con un file package.json può essere caricata in NPM come pacchetto condivisibile. Sebbene NPM sia principalmente associato a JavaScript, un pacchetto può includere CSS e markup. NPM semplifica il riutilizzo e, soprattutto, l' aggiornamento del codice. Invece di dover modificare il codice in una miriade di posti, cambi il codice solo nel pacchetto.

Altro dopo il salto! Continua a leggere sotto ↓

Il problema del markup

Sass e Javascript sono facilmente portabili con l'uso di istruzioni import. I linguaggi di creazione di modelli danno all'HTML la stessa capacità: i modelli possono importare altri frammenti di HTML sotto forma di parziali. Puoi scrivere il markup per il tuo footer, ad esempio, solo una volta, quindi includerlo in altri modelli. Dire che esiste una molteplicità di linguaggi di modellizzazione sarebbe un eufemismo. Legarti a uno solo limita fortemente la potenziale riutilizzabilità del tuo codice. L'alternativa è copiare e incollare il markup e utilizzare NPM solo per stili e javascript.

Questo è l'approccio adottato dal Financial Times con la sua libreria di componenti Origami . Nel suo discorso "Non puoi semplicemente renderlo più simile a Bootstrap?" Alice Bartlett ha concluso che "non esiste un buon modo per consentire alle persone di includere modelli nei loro progetti". Parlando della sua esperienza nel mantenere una libreria di componenti presso Lonely Planet, Ian Feather ha ribadito i problemi con questo approccio:

“Una volta copiato quel codice, stanno essenzialmente tagliando una versione che deve essere mantenuta a tempo indeterminato. Quando hanno copiato il markup per un componente funzionante, a quel punto aveva un collegamento implicito a un'istantanea del CSS. Se poi aggiorni il modello o refactoring del CSS, devi aggiornare tutte le versioni del modello sparse per il tuo sito".

Una soluzione: componenti Web

I componenti Web risolvono questo problema definendo il markup in JavaScript. L'autore di un componente è libero di modificare markup, CSS e Javascript. Il consumatore del componente può trarre vantaggio da questi aggiornamenti senza dover passare a strascico attraverso un progetto che altera manualmente il codice. La sincronizzazione con le ultime modifiche a livello di progetto può essere ottenuta con un conciso npm update tramite terminale. Solo il nome del componente e la relativa API devono rimanere coerenti.

Installare un componente Web è semplice come digitare npm install component-name in un terminale. Il Javascript può essere incluso con un'istruzione import:

 <script type="module"> import './node_modules/component-name/index.js'; </script>

Quindi puoi utilizzare il componente ovunque nel tuo markup. Ecco un semplice componente di esempio che copia il testo negli appunti.

Guarda la demo del componente web Pen Simple di CSS GRID (@cssgrid) su CodePen.

Guarda la demo del componente web Pen Simple di CSS GRID (@cssgrid) su CodePen.

Un approccio incentrato sui componenti allo sviluppo front-end è diventato onnipresente, introdotto dal framework React di Facebook. Inevitabilmente, data la pervasività dei framework nei moderni flussi di lavoro front-end, un certo numero di aziende ha creato librerie di componenti utilizzando il proprio framework prescelto. Tali componenti sono riutilizzabili solo all'interno di quel particolare quadro.

Un componente del Carbon Design System di IBM
Un componente del Carbon Design System di IBM. Da utilizzare solo nelle applicazioni React. Altri esempi significativi di librerie di componenti integrate in React includono Atlaskit di Atlassian e Polaris di Shopify. (Grande anteprima)

È raro che un'azienda di grandi dimensioni abbia un front-end uniforme e la replica da un framework all'altro non è raro. Le strutture vanno e vengono. Per consentire la massima quantità di potenziale riutilizzo tra i progetti, abbiamo bisogno di componenti indipendenti dal framework .

Uno screenshot da npmjs.com che mostra componenti che fanno la stessa cosa costruiti esclusivamente per particolari framework javascript.
La ricerca di componenti tramite npmjs.com rivela un ecosistema Javascript frammentato. (Grande anteprima)
Un grafico che illustra la popolarità dei framework nel tempo. Ember, Knockout e Backbone sono diventati popolari, sostituiti da nuove offerte.
La popolarità in continua evoluzione dei framework nel tempo. (Grande anteprima)
"Ho creato applicazioni web utilizzando: Dojo, Mootools, Prototype, jQuery, Backbone, Thorax e React nel corso degli anni... Mi sarebbe piaciuto poter portare quel componente Dojo killer che ho asservito con me al mio React app di oggi.”

— Dion Almaer, Direttore dell'ingegneria, Google

Quando parliamo di un componente web, parliamo della combinazione di un elemento personalizzato con un DOM ombra. Gli elementi personalizzati e lo shadow DOM fanno parte sia della specifica W3C DOM che dello standard WHATWG DOM, il che significa che i componenti web sono uno standard web . Gli elementi personalizzati e il DOM ombra sono finalmente impostati per ottenere il supporto cross-browser quest'anno. Utilizzando una parte standard della piattaforma web nativa, garantiamo che i nostri componenti possano sopravvivere al ciclo in rapido movimento di ristrutturazioni front-end e ripensamenti architettonici. I componenti Web possono essere utilizzati con qualsiasi linguaggio di creazione di modelli e qualsiasi framework front-end: sono veramente compatibili tra loro e interoperabili. Possono essere utilizzati ovunque, da un blog Wordpress a un'applicazione a pagina singola.

Il progetto Custom Elements Everywhere di Rob Dodson documenta l'interoperabilità dei componenti Web con vari framework Javascript lato client.
Il progetto Custom Elements Everywhere di Rob Dodson documenta l'interoperabilità dei componenti Web con vari framework Javascript lato client. Si spera che React, il valore anomalo qui, risolva questi problemi con React 17. (Anteprima ampia)

Realizzazione di un componente Web

Definizione di un elemento personalizzato

È sempre stato possibile creare nomi di tag e far apparire il loro contenuto sulla pagina.

 <made-up-tag>Hello World!</made-up-tag>

HTML è progettato per essere tollerante agli errori. Quanto sopra verrà visualizzato, anche se non è un elemento HTML valido. Non c'è mai stato un buon motivo per farlo: deviare dai tag standardizzati è stata tradizionalmente una cattiva pratica. Definendo un nuovo tag utilizzando l'API dell'elemento personalizzato, tuttavia, possiamo aumentare l'HTML con elementi riutilizzabili che hanno funzionalità integrate. La creazione di un elemento personalizzato è molto simile alla creazione di un componente in React, ma qui si estendeva HTMLElement .

 class ExpandableBox extends HTMLElement { constructor() { super() } }

Una chiamata senza parametri a super() deve essere la prima istruzione nel costruttore. Il costruttore deve essere utilizzato per impostare lo stato iniziale e i valori predefiniti e per configurare eventuali listener di eventi. Un nuovo elemento personalizzato deve essere definito con un nome per il suo tag HTML e la classe corrispondente degli elementi:

 customElements.define('expandable-box', ExpandableBox)

È una convenzione per mettere in maiuscolo i nomi delle classi. La sintassi del tag HTML è, tuttavia, più di una convenzione. E se i browser volessero implementare un nuovo elemento HTML e volessero chiamarlo espandibile-box? Per evitare conflitti di denominazione, nessun nuovo tag HTML standardizzato includerà un trattino. Al contrario, i nomi degli elementi personalizzati devono includere un trattino.

 customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid

Ciclo di vita dell'elemento personalizzato

L'API offre quattro reazioni degli elementi personalizzati: funzioni che possono essere definite all'interno della classe che verrà automaticamente chiamata in risposta a determinati eventi nel ciclo di vita di un elemento personalizzato.

connectedCallback viene eseguito quando l'elemento personalizzato viene aggiunto al DOM.

 connectedCallback() { console.log("custom element is on the page!") }

Ciò include l'aggiunta di un elemento con Javascript:

 document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”

oltre a includere semplicemente l'elemento all'interno della pagina con un tag HTML:

 <expandable-box></expandable-box> // "custom element is on the page"

Qualsiasi lavoro che implichi il recupero di risorse o il rendering dovrebbe essere qui.

DisconnectedCallback viene eseguito quando l'elemento personalizzato viene rimosso dal DOM.

 disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"

adoptedCallback viene eseguito quando l'elemento personalizzato viene adottato in un nuovo documento. Probabilmente non devi preoccuparti di questo troppo spesso.

attributeChangedCallback viene eseguito quando un attributo viene aggiunto, modificato o rimosso. Può essere utilizzato per ascoltare le modifiche sia agli attributi nativi standardizzati come disabled o src , sia a quelli personalizzati che creiamo. Questo è uno degli aspetti più potenti degli elementi personalizzati in quanto consente la creazione di un'API di facile utilizzo.

Attributi degli elementi personalizzati

Ci sono moltissimi attributi HTML. In modo che il browser non perda tempo a chiamare il nostro attributeChangedCallback quando viene modificato un attributo, dobbiamo fornire un elenco delle modifiche agli attributi che vogliamo ascoltare. Per questo esempio, siamo interessati solo a uno.

 static get observedAttributes() { return ['expanded'] }

Quindi ora il nostro attributeChangedCallback verrà chiamato solo quando cambiamo il valore dell'attributo espanso sull'elemento personalizzato, poiché è l'unico attributo che abbiamo elencato.

Gli attributi HTML possono avere valori corrispondenti (think href, src, alt, value ecc.) mentre altri sono veri o falsi (ad esempio disabilitato, selezionato, richiesto ). Per un attributo con un valore corrispondente, includeremmo quanto segue nella definizione della classe dell'elemento personalizzato.

 get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }

Per il nostro elemento di esempio, l'attributo sarà true o false, quindi la definizione di getter e setter è leggermente diversa.

 get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }

Ora che il boilerplate è stato risolto, possiamo utilizzare attributeChangedCallback .

 attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }

Tradizionalmente, la configurazione di un componente Javascript avrebbe comportato il passaggio di argomenti a una funzione init . Utilizzando l' attributeChangedCallback , è possibile creare un elemento personalizzato configurabile solo con il markup.

Shadow DOM ed elementi personalizzati possono essere utilizzati separatamente e potresti trovare elementi personalizzati utili da soli. A differenza dei DOM ombra, possono essere riempiti con poli. Tuttavia, le due specifiche funzionano bene insieme.

Allegare markup e stili con Shadow DOM

Finora abbiamo gestito il comportamento di un elemento personalizzato. Per quanto riguarda il markup e gli stili, tuttavia, il nostro elemento personalizzato equivale a un <span> vuoto senza stile. Per incapsulare HTML e CSS come parte del componente, dobbiamo allegare un DOM ombra. È meglio farlo all'interno della funzione di costruzione.

 class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }

Non preoccuparti di capire cosa significa la modalità: devi includere la sua piastra standard, ma praticamente vorrai sempre open . Questo semplice componente di esempio renderà semplicemente il testo "ciao mondo". Come la maggior parte degli altri elementi HTML, un elemento personalizzato può avere figli, ma non per impostazione predefinita. Finora l'elemento personalizzato sopra che abbiamo definito non visualizzerà alcun figlio sullo schermo. Per visualizzare qualsiasi contenuto tra i tag, dobbiamo utilizzare un elemento slot .

 shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `

Possiamo usare un tag di stile per applicare alcuni CSS al componente.

 shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`

Questi stili si applicheranno solo al componente, quindi siamo liberi di utilizzare i selettori di elementi senza che gli stili influiscano su nient'altro della pagina. Ciò semplifica la scrittura di CSS, rendendo superflue le convenzioni di denominazione come BEM.

Pubblicazione di un componente su NPM

I pacchetti NPM vengono pubblicati tramite la riga di comando. Apri una finestra di terminale e spostati in una directory che vorresti trasformare in un pacchetto riutilizzabile. Quindi digita i seguenti comandi nel terminale:

  1. Se il tuo progetto non ha già un package.json, npm init ti guiderà attraverso la generazione di uno.
  2. npm adduser collega la tua macchina al tuo account NPM. Se non hai un account preesistente, ne creerà uno nuovo per te.
  3. npm publish
I pacchetti NPM vengono pubblicati tramite la riga di comando
Anteprima ampia

Se tutto è andato bene, ora hai un componente nel registro NPM, pronto per essere installato e utilizzato nei tuoi progetti e condiviso con il mondo intero.

Un esempio di componente nel registro NPM, pronto per essere installato e utilizzato nei propri progetti.
Anteprima ampia

L'API dei componenti Web non è perfetta. Gli elementi personalizzati non sono attualmente in grado di includere dati negli invii di moduli. La storia del miglioramento progressivo non è eccezionale. Gestire l'accessibilità non è così facile come dovrebbe essere.

Sebbene originariamente annunciato nel 2011, il supporto del browser non è ancora universale. Il supporto per Firefox è previsto entro la fine dell'anno. Tuttavia, alcuni siti web di alto profilo (come Youtube) ne stanno già facendo uso. Nonostante le loro attuali carenze, per i componenti universalmente condivisibili sono l'unica opzione e in futuro possiamo aspettarci interessanti aggiunte a ciò che hanno da offrire.