Les tres lliçons anteriors han resolt com un component Lit s'integra amb diferents consumidors: HTML pla, altres frameworks, i el propi servidor abans que el navegador entri en joc. Aquesta lliçó tanca el mòdul amb la pregunta que queda un cop un component —o una aplicació completa com TaskFlow— està a punt de sortir de l'entorn de desenvolupament: com empaquetar-lo, com decidir si es publica com a llibreria reutilitzable o es desplega com a aplicació, i si convé o no migrar el codi JavaScript escrit durant tot el curs a TypeScript.
Contingut
- Dos objectius diferents: publicar una llibreria davant de desplegar una aplicació
- Bundlers per a components Lit: Rollup, Vite i esbuild
- Empaquetar
<task-card>com a llibreria de components amb Rollup - Desplegar TaskFlow com a aplicació amb Vite
- La mida del bundle: per què Lit és deliberadament petit
- Migrar a TypeScript: decoradors davant de
static properties - Equivalència 1:1: què canvia i què no canvia
- Tancament del mòdul 8
- Dos objectius diferents: publicar una llibreria davant de desplegar una aplicació
Abans de parlar d'eines concretes, convé distingir dos escenaris que, encara que comparteixen vocabulari ("empaquetar", "build"), persegueixen objectius diferents i per tant requereixen configuracions diferents. El primer escenari és publicar una llibreria de components: per exemple, si <task-card> i <user-avatar> es consideressin prou genèrics i útils com per publicar-se a npm i que altres projectes —no només TaskFlow— els instal·lin com a dependència. En aquest cas, l'objectiu de l'empaquetatge és produir un mòdul que altres bundlers puguin, al seu torn, importar i tornar a empaquetar dins dels seus propis projectes, típicament sense incloure Lit dins del propi paquet (deixant que qui el consumeixi aporti la seva pròpia còpia de Lit com a dependència compartida).
El segon escenari és desplegar una aplicació completa: TaskFlow en conjunt, amb tots els seus components ja combinats, a punt de servir-se directament als usuaris finals des d'un servidor web o una xarxa de distribució de contingut (CDN). Aquí l'objectiu és precisament el contrari en un aspecte clau: el bundle final ha d'incloure tot el necessari per funcionar per si mateix —inclòs Lit— optimitzat per al navegador (codi minificat, divisió en fragments carregats només quan calen, gestió de memòria cau per nom de fitxer amb hash), sense esperar que qui la visiti hagi de resoldre cap dependència addicional pel seu compte.
Aquesta distinció determina, en gran mesura, quina eina convé en cada cas, tal com es veu als dos apartats següents.
- Bundlers per a components Lit: Rollup, Vite i esbuild
El propi equip de Lit recomana Rollup com a bundler de referència per publicar llibreries de components, i aquesta recomanació no és arbitrària: Rollup està dissenyat, des del seu origen, amb la producció de llibreries com a cas d'ús principal, generant un codi de sortida net i proper al d'entrada, sense el tipus de fragmentació orientada a aplicacions de pàgina completa que altres bundlers prioritzen. Eines oficials del propi ecosistema de Lit, com el generador de projectes @lit-labs/starter-kit (o el seu equivalent vigent en el moment de crear un projecte nou), configuren Rollup per defecte precisament amb aquest propòsit.
Vite, el bundler utilitzat durant tot aquest curs per al projecte de desenvolupament de TaskFlow, és l'alternativa natural per al segon escenari de l'apartat anterior: desplegar una aplicació completa. Vite utilitza, internament, Rollup per al seu propi procés de construcció de producció (vite build), però hi afegeix per sobre tota l'experiència de desenvolupament que ha acompanyat el curs —servidor amb recàrrega en calent, resolució ràpida de mòduls— i una configuració per defecte ja orientada a aplicacions, no a llibreries: divideix el codi en fragments, optimitza recursos estàtics, i genera un resultat a punt per servir directament sense passos addicionals.
esbuild apareix amb freqüència com una tercera opció, valorada sobretot per la seva velocitat de construcció, notablement superior a la de Rollup o a la de bundlers més antics, gràcies a estar escrit en Go en lloc de JavaScript. Vite, de fet, utilitza esbuild internament per a tasques de transformació al seu servidor de desenvolupament (encara que recorri a Rollup per al build de producció final), i esbuild també es pot utilitzar de forma directa com a bundler independent per a casos on la velocitat de construcció importa més que el control fi sobre la sortida que ofereix Rollup.
| Bundler | Millor encaix | Motiu |
|---|---|---|
| Rollup | Publicar una llibreria de components | Sortida neta, propera al codi font, pensada perquè altres bundlers la tornin a processar |
| Vite | Desplegar una aplicació completa | Experiència de desenvolupament ja utilitzada durant el curs, més una configuració de producció orientada a aplicacions (utilitza Rollup per sota) |
| esbuild | Casos on la velocitat de construcció és prioritària | Molt més ràpid que alternatives basades en JavaScript, a costa d'una mica menys de control fi sobre la sortida |
- Empaquetar
<task-card> com a llibreria de components amb Rollup
<task-card> com a llibreria de components amb RollupSi <task-card> s'extragués de TaskFlow per publicar-se com a paquet npm independent, una configuració mínima de Rollup podria veure's així:
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/task-card.js',
output: {
file: 'dist/task-card.js',
format: 'esm',
},
plugins: [resolve()],
};input assenyala el punt d'entrada —el propi fitxer del component—, i output.format: 'esm' indica que la sortida ha de continuar sent un mòdul ES, el format que el propi Lit i la resta de l'ecosistema modern de JavaScript esperen poder importar. El plugin @rollup/plugin-node-resolve permet que Rollup resolgui correctament l'import { LitElement, html, css } from 'lit' del propi component, localitzant el paquet lit dins de node_modules, exactament el mateix tipus de resolució de mòduls que Vite fa de forma transparent durant tot el curs, però que Rollup necessita configurar explícitament a través d'un plugin.
Un detall important per a aquest escenari: per defecte, aquesta configuració no inclou el codi de Lit dins del bundle resultant, atès que lit apareix declarat com a dependència (no inclosa directament) al package.json del paquet a publicar; qui instal·li task-card des de npm també instal·larà lit com a dependència transitiva, i el seu propi bundler serà qui decideixi com combinar-les, evitant que dos components diferents de dos paquets diferents, tots dos depenents de Lit, acabin incloent dues còpies separades i independents de la mateixa biblioteca a l'aplicació final que els consumeixi a tots dos.
- Desplegar TaskFlow com a aplicació amb Vite
Per a TaskFlow com a aplicació completa, la comanda de construcció de producció de Vite, ja configurada pel propi projecte creat a l'inici del curs, resol l'escenari contrari:
Aquesta comanda executa, per sota, vite build, que utilitza Rollup per generar un directori dist/ amb el resultat final: els fitxers JavaScript minificats, amb noms que inclouen un hash del contingut (útil per al control de memòria cau del navegador, ja que un canvi de contingut produeix automàticament un nom de fitxer diferent), les fulles d'estil extretes si correspon, i un index.html ja apuntant a aquests fitxers generats. A diferència de l'escenari de llibreria de l'apartat anterior, aquí sí que interessa incloure Lit dins del propi bundle final: TaskFlow, com a aplicació, no té cap consumidor extern que aporti la seva pròpia còpia de Lit; el bundle final ha de ser autosuficient, a punt per copiar-se a qualsevol servidor de fitxers estàtics o CDN i funcionar sense cap dependència addicional a resoldre en temps d'execució.
- La mida del bundle: per què Lit és deliberadament petit
La lliçó 01-01 ja va esmentar la mida reduïda de Lit com un dels seus avantatges davant d'un framework d'aplicació complet, i aquest és el moment del curs en què aquesta xifra cobra importància pràctica real: com més petita sigui la mida de la pròpia biblioteca inclosa al bundle final, menys temps triga el navegador a descarregar-la, analitzar-la i executar-la abans de poder definir el primer component, un factor que incideix directament en la finestra de "pàgina buida" descrita a la lliçó anterior sobre SSR, fins i tot en aplicacions que no utilitzen SSR en absolut.
Lit aconsegueix la seva mida reduïda —uns pocs kilobytes comprimits amb gzip, molt per sota de frameworks d'aplicació complets— per una decisió de disseny deliberada, no per casualitat: Lit es limita, de forma conscient, a resoldre el problema de les plantilles reactives i les actualitzacions eficients del DOM sobre Custom Elements estàndard, sense incloure un enrutador propi, sense un sistema de gestió d'estat global propi, sense les desenes d'utilitats addicionals que un framework d'aplicació complet sol incorporar per defecte. Cadascuna d'aquestes peces absents —enrutament, estat global— és, precisament, terreny que TaskFlow ha resolt durant el curs amb eines pròpies i minimalistes (@lit/context per a estat compartit, en lloc d'una llibreria de gestió d'estat global de propòsit general), en lloc de dependre de que Lit les inclogués de sèrie. Un bundler com Rollup o Vite pot, a més, aplicar tree-shaking —eliminar del bundle final qualsevol part d'una biblioteca que el codi de l'aplicació no arribi a utilitzar en cap punt—, i el propi disseny modular de Lit (funcions i classes exportades de forma independent, en lloc d'un únic objecte monolític que ho inclogui tot) afavoreix que aquest tree-shaking sigui efectiu, reduint encara més la mida final per a aplicacions que només utilitzen un subconjunt de les funcionalitats disponibles.
- Migrar a TypeScript: decoradors davant de
static properties
static propertiesTot el codi de TaskFlow, durant les set lliçons i mitja de curs anteriors a aquest mòdul, s'ha escrit en JavaScript pur, amb static properties com a forma de declarar propietats reactives. Lit ofereix, de forma opcional i sense cap obligació d'adoptar-la, una sintaxi alternativa basada en decoradors de TypeScript (o de JavaScript amb la proposta de decoradors habilitada mitjançant Babel), pensada per a qui prefereixi —o el projecte del qual ja utilitzi— TypeScript com a llenguatge principal:
// Versión con static properties (JavaScript, la usada durante todo el curso)
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
expandida: { state: true },
};
constructor() {
super();
this.expandida = false;
}
render() {
return html`<h3>${this.titulo}</h3>`;
}
}// Versión con decoradores (TypeScript)
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
@customElement('task-card')
class TaskCard extends LitElement {
@property({ type: String })
titulo = '';
@property({ type: String })
estado = '';
@state()
private expandida = false;
render() {
return html`<h3>${this.titulo}</h3>`;
}
}Tres decoradors cobreixen, en conjunt, el mateix que static properties més customElements.define(...) resolen a la versió de JavaScript: @customElement('task-card') substitueix directament la crida customElements.define('task-card', TaskCard) col·locada al final de cada fitxer de component durant tot el curs, registrant la classe amb aquest nom d'etiqueta al mateix punt on es declara la classe, en lloc d'una línia a part al final del fitxer; @property({...}) sobre un camp de la classe declara aquesta propietat com a reactiva i pública, amb les mateixes opcions de configuració (type, attribute, converter, reflect) ja estudiades des de la lliçó 03-01 per a l'objecte de configuració de static properties; i @state() substitueix l'opció { state: true }, marcant la propietat com a estat intern reactiu, no exposat com a atribut públic (tal com es va explicar a la lliçó 03-02).
- Equivalència 1:1: què canvia i què no canvia
El punt més important d'aquest apartat, i el motiu pel qual aquesta lliçó pot presentar TypeScript sense necessitat de repetir res de la resta del curs, és que la migració a decoradors és purament sintàctica: no canvia cap concepte de Lit estudiat fins ara, només la forma d'escriure'l. El cicle de renderitzat (mòdul 2), el sistema de propietats reactives i el seu cicle d'actualització (mòdul 3), els estils amb Shadow DOM (mòdul 4), la comunicació entre components (mòdul 5), el cicle de vida, els controladors reactius i els mixins (mòdul 6), i les directives incloent-hi @lit/context (mòdul 7) funcionen exactament igual, amb el mateix comportament en temps d'execució, tant si el component es va declarar amb static properties com amb decoradors. Un ReactiveController com ContadorTiempoRestanteController (lliçó 06-03) no necessita cap adaptació per utilitzar-se des d'un component escrit amb decoradors; una directiva personalitzada com resaltarSiUrgente (lliçó 07-02) tampoc.
El que sí que canvia és exclusivament la forma de declarar la classe i les seves propietats:
| Aspecte | JavaScript (static properties) |
TypeScript (decoradors) |
|---|---|---|
| Registrar l'element | customElements.define('task-card', TaskCard) al final del fitxer |
@customElement('task-card') just a sobre de la classe |
| Declarar una propietat pública | Entrada a l'objecte static properties |
@property({...}) sobre el camp corresponent |
| Declarar estat intern | { state: true } dins de static properties |
@state() sobre el camp corresponent |
| Valor inicial d'una propietat | Assignació al constructor |
Assignació directa a la declaració del camp (titulo = '') |
| Comprovació de tipus | Cap en temps de compilació; només la conversió de tipus en temps d'execució (mòdul 3) | TypeScript comprova, en temps de compilació, que el codi utilitzi cada propietat amb el tipus declarat |
Adoptar TypeScript i els decoradors és, per tant, una decisió que es pot prendre de forma independent per a cada projecte —o fins i tot migrar progressivament, component a component, atès que totes dues sintaxis poden convivir dins d'una mateixa aplicació mentre duri la transició—, sense que suposi reescriure cap de les decisions de disseny ja preses durant el curs; l'única cosa que canvia és la forma en què aquestes mateixes decisions s'expressen al codi, més el benefici afegit de la comprovació de tipus que TypeScript aporta per sobre, aliena per complet a Lit en si mateix.
- Tancament del mòdul 8
Amb aquesta lliçó es completa el mòdul 8. El recorregut ha anat de dins cap a fora: primer, com un component Lit s'utilitza directament en HTML pla gràcies a ser, en el fons, un Custom Element estàndard; després, com s'integra dins d'aplicacions construïdes amb altres frameworks —React, Vue i Angular—, cadascun amb els seus propis punts de fricció en tractar amb Custom Elements; a continuació, com es renderitza al servidor amb @lit-labs/ssr i Declarative Shadow DOM, perquè el contingut estigui visible fins i tot abans que el JavaScript s'executi; i, finalment, com s'empaqueta i publica —com a llibreria amb Rollup, o com a aplicació desplegable amb Vite— i què canvia, o més bé què no canvia, en migrar opcionalment a TypeScript.
Errors Habituals i Consells
- Incloure Lit dins del bundle d'una llibreria de components pensada per publicar-se a npm: com s'ha explicat a l'apartat 3, això obliga qualsevol consumidor que utilitzi diversos components de diferents paquets, tots depenents de Lit, a carregar diverses còpies independents de la mateixa biblioteca; declarar
litcom a dependència externa, no inclosa al propi bundle, evita aquest problema. - Utilitzar la configuració de Vite pensada per a aplicacions en publicar una llibreria, o viceversa: com s'ha vist als apartats 1 i 2, tots dos escenaris tenen objectius diferents (autosuficiència davant de reutilització externa per altres bundlers); utilitzar l'eina equivocada per a l'escenari equivocat sol traduir-se en un bundle de llibreria innecessàriament gran, o en una aplicació desplegada que continua esperant que el navegador resolgui dependències pel seu compte.
- Barrejar
static propertiesi decoradors dins de la mateixa classe: encara que totes dues sintaxis poden convivir en projectes diferents, o fins i tot en components diferents d'un mateix projecte en migració progressiva (apartat 7), combinar tots dos estils dins d'una única classe sol generar confusió sobre quin mecanisme està declarant realment cada propietat, i convé evitar-ho dins d'un mateix fitxer. - Adoptar TypeScript esperant que canviï el comportament en temps d'execució d'algun concepte de Lit: com s'ha insistit a l'apartat 7, la migració és purament sintàctica; si un component tenia un problema de disseny en JavaScript (per exemple, oblidar propagar
Base.propertiesen un mixin, com es va advertir a la lliçó 06-04), aquest mateix problema pot reaparèixer en TypeScript si no es corregeix explícitament, ja que els decoradors no substitueixen ni corregeixen automàticament cap de les pràctiques ja estudiades durant el curs.
Exercicis
- Reescriu la classe
TaskFilterde la lliçó 07-04 (amb el seuContextConsumeri els seus mètodesmanejarTexto/manejarEstado) utilitzant decoradors de TypeScript en lloc destatic properties, mantenint exactament el mateix comportament. - Explica, basant-te en l'apartat 3, quin problema concret evita declarar
litcom a dependència externa (no inclosa al bundle) en publicar<task-card>com a paquet npm independent, en un escenari en el qual una aplicació consumidora utilitzés també un altre component d'un tercer, igualment depenent de Lit. - Un company d'equip, després de llegir sobre Rollup i Vite a l'apartat 2, proposa utilitzar Rollup també per al build de producció de la pròpia aplicació TaskFlow, en lloc de
vite build, argumentant que "és l'eina recomanada per l'equip de Lit". Explica per què aquest raonament no és correcte per al cas concret de TaskFlow, basant-te en la distinció de l'apartat 1.
Solucions
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ContextConsumer } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';
@customElement('task-filter')
class TaskFilter extends LitElement {
private _filtro: ContextConsumer<typeof filtroContext, this>;
constructor() {
super();
this._filtro = new ContextConsumer(this, { context: filtroContext, subscribe: true });
}
get valorActual() {
return this._filtro.value ?? { texto: '', estado: 'todas', actualizar: () => {} };
}
manejarTexto(evento: Event) {
this.valorActual.actualizar({ texto: (evento.target as HTMLInputElement).value });
}
manejarEstado(estado: string) {
this.valorActual.actualizar({ estado });
}
render() {
const { texto, estado } = this.valorActual;
return html`
<div class="filtro">
<input
type="text"
placeholder="Buscar tarea…"
.value="${texto}"
@input="${this.manejarTexto}"
/>
<div class="filtro__botones">
${['todas', 'pendiente', 'hecha'].map(
(opcion) => html`
<button
class="${classMap({ activo: estado === opcion })}"
@click="${() => this.manejarEstado(opcion)}"
>
${{ todas: 'Todas', pendiente: 'Pendientes', hecha: 'Hechas' }[opcion]}
</button>
`
)}
</div>
</div>
`;
}
static styles = css`
.filtro__botones button.activo {
font-weight: bold;
border-bottom: 2px solid currentColor;
}
`;
}No hi ha cap @property ni @state en aquesta classe perquè, igual que a la versió original de la lliçó 07-04, TaskFilter no declara cap propietat reactiva pròpia: tot el seu estat visible (texto, estado) es llegeix directament de this._filtro.value a cada render(), exactament igual que a la versió en JavaScript.
- Si
<task-card>inclogués la seva pròpia còpia de Lit dins del bundle publicat a npm, i un component d'un tercer (per exemple, un<other-widget>d'un altre paquet, també construït amb Lit) fes el mateix, una aplicació que utilitzés tots dos acabaria carregant dues còpies completes i independents de Lit al navegador de l'usuari final: cadascuna amb el seu propi codi, el seu propi consum de memòria, i —més greu encara— sense cap garantia de que totes dues còpies reconeguin les mateixes instàncies de context (@lit/context) o de directives si, en algun punt, tots dos components necessitessin compartir alguna d'aquestes peces d'infraestructura entre si. Declarantlitcom a dependència externa del paquet, el bundler de l'aplicació consumidora resol una única còpia compartida de Lit, instal·lada una sola vegada a l'arbre de dependències, i tots dos components de tercers la reutilitzen sense duplicació. - La recomanació de Rollup de l'apartat 2 s'aplica específicament a l'escenari de publicar una llibreria de components reutilitzable per altres projectes, no al de desplegar una aplicació completa com TaskFlow. TaskFlow no té cap consumidor extern que torni a empaquetar el seu codi amb el seu propi bundler; el seu objectiu és generar directament el resultat final que se serveix als usuaris, amb tota l'experiència de desenvolupament (recàrrega en calent durant el curs) i la configuració de producció orientada a aplicacions (divisió en fragments, gestió de memòria cau per hash de fitxer) que Vite ja ofereix de fàbrica, i que a més utilitza Rollup per sota per al seu propi build de producció. Canviar a Rollup de forma directa per a TaskFlow no aportaria cap avantatge sobre
vite buildi només obligaria a reconstruir manualment bona part d'aquesta configuració d'aplicació que Vite ja resol per defecte.
Conclusió
Amb aquesta lliçó es tanca el mòdul 8: TaskFlow ja es pot integrar en HTML pla, dins d'altres frameworks, renderitzar-se al servidor, i empaquetar-se tant com a llibreria reutilitzable de components com a aplicació desplegable, amb l'opció addicional —purament sintàctica, sense canviar cap concepte ja estudiat— d'escriure's en TypeScript amb decoradors en lloc de static properties. Amb TaskFlow ja integrable i desplegable, falta donar-li cobertura de proves i repassar bones pràctiques abans del projecte final.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
