Sfântul Graal al componentelor reutilizabile: elemente personalizate, Shadow DOM și NPM
Publicat: 2022-03-10Chiar și pentru cele mai simple componente, costul muncii uman poate fi semnificativ. Echipele UX fac teste de utilizare. O serie de părți interesate trebuie să aprobe designul.
Dezvoltatorii efectuează teste AB, audituri de accesibilitate, teste unitare și verificări între browsere. Odată ce ați rezolvat o problemă, nu doriți să repetați acest efort . Prin construirea unei biblioteci de componente reutilizabile (în loc să construim totul de la zero), putem folosi în mod continuu eforturile anterioare și putem evita să revizuim provocările de proiectare și dezvoltare deja rezolvate.
Construirea unui arsenal de componente este deosebit de utilă pentru companii precum Google, care dețin un portofoliu considerabil de site-uri web care au un brand comun. Codificându-și interfața de utilizare în widget-uri componabile, companiile mai mari pot accelera timpul de dezvoltare și pot obține coerența atât a designului vizual, cât și a interacțiunii cu utilizatorul în cadrul proiectelor. A existat o creștere a interesului pentru ghidurile de stil și bibliotecile de modele în ultimii câțiva ani. Având în vedere mai mulți dezvoltatori și designeri răspândiți în mai multe echipe, companiile mari caută să obțină consistență. Putem face mai bine decât simplele mostre de culoare. Ceea ce avem nevoie este un cod ușor de distribuit .
Partajarea și reutilizarea codului
Copierea și inserarea manuală a codului este fără efort. Totuși, păstrarea la zi a acestui cod este un coșmar de întreținere. Mulți dezvoltatori, prin urmare, se bazează pe un manager de pachete pentru a reutiliza codul în cadrul proiectelor. În ciuda numelui său, Node Package Manager a devenit platforma de neegalat pentru gestionarea pachetelor front-end . În prezent există peste 700.000 de pachete în registrul NPM și miliarde de pachete sunt descărcate în fiecare lună. Orice folder cu un fișier package.json poate fi încărcat în NPM ca pachet care poate fi partajat. În timp ce NPM este asociat în principal cu JavaScript, un pachet poate include CSS și markup. NPM facilitează reutilizarea și, mai important, actualizarea codului. În loc să trebuiască să modificați codul în nenumărate locuri, schimbați codul numai în pachet.
Problema marcajului
Sass și Javascript sunt ușor de portat cu ajutorul instrucțiunilor de import. Limbile de șabloane oferă HTML aceeași abilitate - șabloanele pot importa alte fragmente de HTML sub formă de partiale. Puteți scrie marcajul pentru subsol, de exemplu, o singură dată, apoi îl puteți include în alte șabloane. A spune că există o multitudine de limbi tip șabloane ar fi un eufemism. A te lega doar de unul limitează sever posibilitatea de reutilizare a codului tău. Alternativa este să copiați și să lipiți marcajul și să utilizați NPM numai pentru stiluri și javascript.
Aceasta este abordarea adoptată de Financial Times cu biblioteca lor de componente Origami . În discursul ei „Nu poți să o faci mai mult ca Bootstrap?” Alice Bartlett a concluzionat că „nu există o modalitate bună de a permite oamenilor să includă șabloane în proiectele lor”. Vorbind despre experiența sa de a menține o bibliotecă de componente la Lonely Planet, Ian Feather a reiterat problemele legate de această abordare:
„Odată ce copiază acel cod, în esență taie o versiune care trebuie menținută pe termen nelimitat. Când au copiat marcajul pentru o componentă de lucru, avea o legătură implicită către un instantaneu al CSS la acel moment. Dacă apoi actualizați șablonul sau refactorizați CSS-ul, trebuie să actualizați toate versiunile șablonului împrăștiate pe site-ul dvs..”
O soluție: componente web
Componentele web rezolvă această problemă prin definirea de markup în JavaScript. Autorul unei componente este liber să modifice marcajul, CSS și Javascript. Consumatorul componentei poate beneficia de aceste upgrade-uri fără a fi nevoie să treacă manual printr-un proiect care modifică codul. Sincronizarea cu cele mai recente modificări la nivelul întregului proiect poate fi realizată cu o npm update
prin terminal. Doar numele componentei și API-ul acesteia trebuie să rămână consecvente.
Instalarea unei componente web este la fel de simplă ca și tastarea npm install component-name
într-un terminal. JavaScript poate fi inclus cu o instrucțiune de import:
<script type="module"> import './node_modules/component-name/index.js'; </script>
Apoi puteți utiliza componenta oriunde în marcaj. Iată un exemplu simplu de componentă care copiază textul în clipboard.
O abordare centrată pe componente a dezvoltării front-end a devenit omniprezentă, introdusă de cadrul React al Facebook. În mod inevitabil, având în vedere amploarea cadrelor în fluxurile de lucru front-end moderne, o serie de companii au construit biblioteci de componente folosind cadrul ales. Aceste componente sunt reutilizabile numai în cadrul respectiv.
Este rar ca o companie de dimensiuni mari să aibă un front-end uniform, iar replatoringul de la un cadru la altul nu este neobișnuit. Cadrele vin și pleacă. Pentru a permite o cantitate maximă de reutilizare potențială în cadrul proiectelor, avem nevoie de componente care să fie independente de cadru .
„Am construit aplicații web folosind: Dojo, Mootools, Prototype, jQuery, Backbone, Thorax și React de-a lungul anilor... Mi-ar fi plăcut să fi putut aduce acea componentă Dojo ucigașă pe care am folosit-o cu mine în React. aplicația de astăzi.”
— Dion Almaer, director de inginerie, Google
Când vorbim despre o componentă web, vorbim despre combinația unui element personalizat cu shadow DOM. Elementele personalizate și shadow DOM fac parte atât din specificația W3C DOM, cât și din standardul WHATWG DOM, ceea ce înseamnă că componentele web sunt un standard web . Elementele personalizate și shadow DOM sunt, în sfârșit , setate pentru a obține compatibilitate între browsere în acest an. Folosind o parte standard a platformei web native, ne asigurăm că componentele noastre pot supraviețui ciclului rapid de restructurare front-end și regândiri arhitecturale. Componentele web pot fi utilizate cu orice limbaj de șabloane și orice cadru front-end - sunt cu adevărat compatibile și interoperabile. Ele pot fi folosite peste tot, de la un blog Wordpress la o aplicație pe o singură pagină.
Realizarea unei componente web
Definirea unui element personalizat
Întotdeauna a fost posibil să se inventeze nume de etichete și să apară conținutul lor pe pagină.
<made-up-tag>Hello World!</made-up-tag>
HTML este conceput pentru a fi tolerant la erori. Cele de mai sus vor fi randate, chiar dacă nu este un element HTML valid. Nu a existat niciodată un motiv bun pentru a face acest lucru - abaterea de la etichetele standardizate a fost în mod tradițional o practică proastă. Prin definirea unei noi etichete folosind API-ul element personalizat, totuși, putem crește HTML cu elemente reutilizabile care au funcționalitate încorporată. Crearea unui element personalizat este la fel ca și crearea unei componente în React - dar aici am extins HTMLElement
.
class ExpandableBox extends HTMLElement { constructor() { super() } }
Un apel fără parametri la super()
trebuie să fie prima instrucțiune din constructor. Constructorul ar trebui folosit pentru a configura starea inițială și valorile implicite și pentru a configura orice ascultători de evenimente. Un nou element personalizat trebuie definit cu un nume pentru eticheta sa HTML și cu elementele clasei corespunzătoare:
customElements.define('expandable-box', ExpandableBox)
Este o convenție de a scrie cu majuscule numele claselor. Sintaxa etichetei HTML este, totuși, mai mult decât o convenție. Ce se întâmplă dacă browserele ar dori să implementeze un nou element HTML și ar fi vrut să-l numească expandable-box? Pentru a preveni coliziunile de denumire, nicio etichetă HTML standardizată nouă nu va include o liniuță. În schimb, numele elementelor personalizate trebuie să includă o liniuță.
customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid
Ciclul de viață al elementului personalizat
API-ul oferă patru reacții de elemente personalizate - funcții care pot fi definite în cadrul clasei care vor fi apelate automat ca răspuns la anumite evenimente din ciclul de viață al unui element personalizat.
ConnectedCallback este rulat atunci când elementul personalizat este adăugat la DOM.
connectedCallback() { console.log("custom element is on the page!") }
Aceasta include adăugarea unui element cu Javascript:
document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”
precum și pur și simplu includerea elementului în pagină cu o etichetă HTML:
<expandable-box></expandable-box> // "custom element is on the page"
Orice lucrare care implică preluarea resurselor sau randarea ar trebui să fie aici.
disconnectedCallback este rulat atunci când elementul personalizat este eliminat din DOM.
disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"
adoptedCallback
este rulat atunci când elementul personalizat este adoptat într-un document nou. Probabil că nu trebuie să vă faceți griji pentru aceasta prea des.
attributeChangedCallback
este rulat atunci când un atribut este adăugat, modificat sau eliminat. Poate fi folosit pentru a asculta modificările atât ale atributelor native standardizate, cum ar fi disabled sau src , cât și ale celor personalizate pe care le inventăm. Acesta este unul dintre cele mai puternice aspecte ale elementelor personalizate, deoarece permite crearea unui API ușor de utilizat.
Atribute personalizate ale elementelor
Există foarte multe atribute HTML. Pentru ca browserul să nu piardă timpul apelând attributeChangedCallback
-ul nostru atunci când orice atribut este modificat, trebuie să furnizăm o listă cu modificările atributelor pe care vrem să le ascultăm. Pentru acest exemplu, ne interesează doar unul.
static get observedAttributes() { return ['expanded'] }
Așadar, acum attributeChangedCallback
va fi apelat numai atunci când schimbăm valoarea atributului extins pe elementul personalizat, deoarece este singurul atribut pe care l-am enumerat.
Atributele HTML pot avea valori corespunzătoare (think href, src, alt, value etc), în timp ce altele sunt fie adevărate, fie false (de exemplu , dezactivate, selectate, obligatorii ). Pentru un atribut cu o valoare corespunzătoare, am include următoarele în definiția clasei elementului personalizat.
get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }
Pentru elementul nostru exemplu, atributul va fi fie adevărat, fie fals, așa că definirea getterului și a setterului este puțin diferită.
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') } }
Acum că boilerplate a fost rezolvat, putem folosi attributeChangedCallback
.
attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }
În mod tradițional, configurarea unei componente Javascript ar fi implicat transmiterea de argumente către o funcție init
. Prin utilizarea attributeChangedCallback
, este posibil să creați un element personalizat care să fie configurabil doar cu marcaj.
Shadow DOM și elementele personalizate pot fi utilizate separat și s-ar putea să găsiți elemente personalizate utile de la sine. Spre deosebire de shadow DOM, acestea pot fi umplute polivalent. Cu toate acestea, cele două specificații funcționează bine împreună.
Atașarea markupurilor și stilurilor cu Shadow DOM
Până acum, ne-am ocupat de comportamentul unui element personalizat. În ceea ce privește marcajul și stilurile, totuși, elementul nostru personalizat este echivalent cu un <span>
fără stil necompletat. Pentru a încapsula HTML și CSS ca parte a componentei, trebuie să atașăm un DOM umbră. Cel mai bine este să faceți acest lucru în funcția de constructor.
class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }
Nu vă faceți griji să înțelegeți ce înseamnă modul - trebuie să includeți planul său, dar aproape întotdeauna veți dori să open
. Acest exemplu simplu de componentă va reda doar textul „hello world”. La fel ca majoritatea celorlalte elemente HTML, un element personalizat poate avea copii, dar nu în mod implicit. Până acum, elementul personalizat de mai sus pe care l-am definit nu va afișa niciun copil pe ecran. Pentru a afișa orice conținut între etichete, trebuie să folosim un element slot
.
shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `
Putem folosi o etichetă de stil pentru a aplica unele CSS la componentă.
shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`
Aceste stiluri se vor aplica numai componentei, așa că suntem liberi să folosim selectoare de elemente fără ca stilurile să afecteze nimic altceva din pagină. Acest lucru simplifică scrierea CSS, făcând inutile convențiile de denumire precum BEM.
Publicarea unei componente pe NPM
Pachetele NPM sunt publicate prin linia de comandă. Deschideți o fereastră de terminal și mutați-vă într-un director pe care doriți să îl transformați într-un pachet reutilizabil. Apoi tastați următoarele comenzi în terminal:
- Dacă proiectul dvs. nu are deja un package.json,
npm init
vă va ghida prin generarea unuia. -
npm adduser
conectează mașina dvs. la contul dvs. NPM. Dacă nu aveți un cont preexistent, acesta va crea unul nou pentru dvs. -
npm publish
Dacă totul a decurs bine, acum aveți o componentă în registrul NPM, gata să fie instalată și utilizată în propriile proiecte - și partajată cu întreaga lume.
API-ul componentelor web nu este perfect. În prezent, elementele personalizate nu pot include date în trimiterile de formulare. Povestea îmbunătățirii progresive nu este grozavă. Abordarea accesibilității nu este atât de ușoară pe cât ar trebui să fie.
Deși anunțat inițial în 2011, suportul pentru browser încă nu este universal. Asistența pentru Firefox va avea loc la sfârșitul acestui an. Cu toate acestea, unele site-uri web de mare profil (cum ar fi Youtube) le folosesc deja. În ciuda deficiențelor lor actuale, pentru componentele care pot fi partajate universal , acestea sunt opțiunea singulară, iar în viitor ne putem aștepta la completări interesante la ceea ce au de oferit.