La lliçó anterior ha resolt, amb controladors reactius, la reutilització de lògica amb estat propi i cicle de vida, com el temporitzador de proximitat de data de <task-card>. Però no tot comportament reutilitzable encaixa bé en aquest motlle: de vegades allò que es vol compartir entre diversos components no és un objecte independent amb el seu propi estat intern, sinó propietats i mètodes que haurien de passar a formar part de la pròpia classe i de la pròpia API pública del component, com si s'haguessin escrit directament en ella. Per a aquest segon cas, JavaScript ofereix una tècnica més general i anterior a Lit: els mixins. Aquesta lliçó explica el patró, l'aplica a un petit exemple de TaskFlow, i tanca amb el criteri per decidir, en cada situació futura, entre un mixin i un controlador reactiu.
Contingut
- Què és un mixin en JavaScript pur
- El patró típic d'un mixin a Lit
- Aplicant un mixin:
ConEstadoCarga - Usant el mixin en un component de TaskFlow
- Mixin en front de controlador reactiu: el criteri de decisió
- Limitacions dels mixins: ordre de composició
- Limitacions dels mixins: col·lisió de noms
- Tancament del mòdul 6
- Què és un mixin en JavaScript pur
Un mixin, en JavaScript, no és una paraula clau del llenguatge ni una característica especial: és, senzillament, una funció que rep una classe com a argument i retorna una nova classe que estén la rebuda, afegint-li propietats o mètodes addicionals. No hi ha res específic de Lit en aquesta idea; és una tècnica general de composició de classes que existeix en JavaScript des que les classes mateixes (class) formen part del llenguatge, aprofitant que extends pot rebre qualsevol expressió que avaluï a una classe, no només un nom de classe escrit literalment.
const MiMixin = (ClaseBase) => class extends ClaseBase {
metodoNuevo() {
console.log('Este método viene del mixin');
}
};
class ClaseOriginal {
metodoOriginal() {
console.log('Este método viene de la clase original');
}
}
class ClaseFinal extends MiMixin(ClaseOriginal) {}
const instancia = new ClaseFinal();
instancia.metodoOriginal(); // "Este método viene de la clase original"
instancia.metodoNuevo(); // "Este método viene del mixin"ClaseFinal estén el resultat de cridar MiMixin(ClaseOriginal), que és, en si mateix, una classe nova (anònima, definida amb class extends ClaseBase { ... } dins del cos de la funció) que hereta de ClaseOriginal i li afegeix metodoNuevo. El resultat final té accés tant a allò que ja existia a ClaseOriginal com a allò que aporta el mixin, exactament com si s'hagués escrit una única classe amb tot junt, però mantenint MiMixin com una peça separada i reutilitzable amb qualsevol altra classe base.
- El patró típic d'un mixin a Lit
Aplicat a components de Lit, el patró és idèntic, amb la particularitat que la "classe base" rebuda pel mixin sol ser, en darrer terme, LitElement (o el resultat d'aplicar ja un altre mixin sobre LitElement):
const MiMixin = (Base) => class extends Base {
static properties = {
...Base.properties,
propiedadNueva: { type: String },
};
constructor(...args) {
super(...args);
this.propiedadNueva = 'valor por defecto';
}
};
class MiComponente extends MiMixin(LitElement) {
render() {
return html`<p>${this.propiedadNueva}</p>`;
}
}Dos detalls d'aquest patró mereixen atenció abans d'aplicar-lo a un exemple real. Primer, static properties = { ...Base.properties, propiedadNueva: {...} }: com static properties és un objecte normal de JavaScript, cal combinar explícitament, amb l'operador de propagació, les propietats ja declarades per la classe base amb les noves que aporta el mixin; oblidar el ...Base.properties faria que qualsevol propietat reactiva declarada més avall en la cadena d'herència (per exemple, directament a MiComponente, si estengués static properties al seu torn) deixés de funcionar correctament, perquè MiMixin l'hauria sobreescrit amb un objecte que no la inclou. Segon, el constructor(...args) { super(...args); ... }: un mixin ha de reenviar fidelment qualsevol argument rebut a super(...args), perquè no pot saber d'antuvi quins arguments espera la classe base sobre la qual s'aplicarà (en el cas de LitElement, el propi constructor no sol rebre arguments, però el mixin no hauria d'assumir-ho si aspira a ser reutilitzable amb qualsevol classe base).
- Aplicant un mixin:
ConEstadoCarga
ConEstadoCargaTaskFlow no té, per ara, cap operació que impliqui una espera visible (com una petició de xarxa; això arribarà al mòdul 8), però serveix com a exemple clar i autocontingut d'un comportament que diversos components de l'aplicació podrien necessitar compartir en un futur pròxim: una propietat cargando i una forma reutilitzable d'embolcallar una plantilla amb un indicador visual mentre aquesta propietat estigui activa.
// src/mixins/con-estado-carga.js
import { html } from 'lit';
export const ConEstadoCarga = (Base) => class extends Base {
static properties = {
...Base.properties,
cargando: { state: true },
};
constructor(...args) {
super(...args);
this.cargando = false;
}
conIndicadorDeCarga(plantilla) {
if (this.cargando) {
return html`<p class="cargando">Cargando…</p>`;
}
return plantilla;
}
};ConEstadoCarga afegeix dues coses a qualsevol classe sobre la qual s'apliqui: la propietat d'estat cargando (inicialitzada a false), i el mètode conIndicadorDeCarga(plantilla), que rep la plantilla "normal" del component i retorna, en el seu lloc, un avís de càrrega si cargando és true. Val la pena notar que, a diferència del controlador reactiu de la lliçó anterior, aquí no hi ha cap objecte separat: cargando passa a ser una propietat reactiva més de la pròpia classe final, tan accessible com titulo o estado a <task-card>, i conIndicadorDeCarga passa a ser un mètode més d'aquesta mateixa classe, invocable com this.conIndicadorDeCarga(...) des de dins de render().
- Usant el mixin en un component de TaskFlow
Aplicar el mixin a un component és tan directe com embolcallar LitElement en la crida a la funció:
// src/components/task-board.js
import { LitElement, html, css } from 'lit';
import { ConEstadoCarga } from '../mixins/con-estado-carga.js';
import { estilosCompartidos } from '../styles/shared-styles.js';
import './task-list.js';
class TaskBoard extends ConEstadoCarga(LitElement) {
static properties = {
...ConEstadoCarga(LitElement).properties,
tareas: { type: Array },
};
// ...constructor, gestionarTareaCambiada() y estilos sin cambios...
render() {
return this.conIndicadorDeCarga(html`
<div class="tablero">
<h1>TaskFlow</h1>
<task-list .tareas="${this.tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>
</div>
`);
}
}
customElements.define('task-board', TaskBoard);TaskBoard estén ConEstadoCarga(LitElement) en lloc de LitElement directament, i a partir d'aquest moment té accés, com si fossin seus des de sempre, tant a la propietat cargando com al mètode conIndicadorDeCarga. El propi render() de TaskBoard embolcalla la seva plantilla habitual en una crida a this.conIndicadorDeCarga(...): mentre this.cargando sigui false (el seu valor per defecte), el comportament és idèntic al d'abans d'aplicar el mixin; en quant algun codi activi this.cargando = true (per exemple, en iniciar una operació que triga un temps a completar-se), render() mostrarà l'avís de càrrega en el seu lloc, sense que TaskBoard hagi hagut d'escriure aquesta lògica condicional per si mateixa.
Val la pena notar la construcció, una mica repetitiva, de static properties = { ...ConEstadoCarga(LitElement).properties, tareas: {...} }: com s'ha explicat a l'apartat 2, cada nivell de la cadena que afegeixi les seves pròpies propietats reactives s'ha de recordar de propagar també les heretades del nivell anterior, i això inclou la pròpia classe final que usa el mixin, no només el mixin en si. És un detall de manteniment a tenir present cada vegada que es combina un mixin amb propietats pròpies del component final.
- Mixin en front de controlador reactiu: el criteri de decisió
Amb les dues tècniques ja vistes en aquest mòdul, convé fixar un criteri clar per triar entre elles, en lloc d'aplicar-les de forma intercanviable sense raonar-ne el perquè:
| Criteri | Mixin | Controlador reactiu |
|---|---|---|
| On viu l'estat afegit? | Directament a la instància del component (this.cargando) |
En un objecte propi, separat del host (this._contadorTiempo.cercaDeVencer) |
| S'integra a l'API pública del component? | Sí: les seves propietats i mètodes passen a ser indistingibles dels propis del component | No necessàriament: el host decideix què exposar, si és que exposa alguna cosa |
| Com s'activa? | Embolcallant la classe amb extends MiMixin(Base) |
Instanciant-lo dins del constructor amb new Controlador(this) |
| Millor cas d'ús | Comportament que hauria de sentir-se part de la pròpia classe (un mètode d'utilitat, una propietat que la resta del component usa amb naturalitat) | Lògica amb estat propi i necessitats de cicle de vida, pensada per romandre desacoblada del host |
| Exemple d'aquest curs | ConEstadoCarga: cargando i conIndicadorDeCarga se senten part natural de TaskBoard |
ContadorTiempoRestanteController: el temporitzador no necessita fondre's amb l'API pública de TaskCard |
El criteri de fons, resumit en una frase, és aquest: un mixin és adequat quan el comportament hauria d'integrar-se en la pròpia classe i la seva API pública, com si s'hagués escrit allà directament; un controlador reactiu és preferible quan la lògica té el seu propi estat intern i convé mantenir-la desacoblada, com una peça que el host usa però de la qual no necessita heretar ni exposar directament els seus detalls. La documentació oficial de Lit es decanta, de fet, per recomanar controladors reactius com a primera opció en front dels mixins en la majoria dels casos de lògica amb estat, precisament pels problemes que s'expliquen en els dos apartats següents.
- Limitacions dels mixins: ordre de composició
Quan s'aplica un únic mixin, com a l'exemple de ConEstadoCarga, l'ordre no genera cap ambigüitat. El problema apareix en quant es combinen diversos mixins sobre la mateixa classe base:
Aquí, MixinB s'aplica primer sobre LitElement, i MixinA s'aplica després sobre el resultat de MixinB(LitElement). Si tots dos mixins sobreescriuen, per exemple, el mateix mètode del cicle de vida (connectedCallback, o qualsevol altre), l'ordre d'anidament determina quina de les dues versions "veu" primer la crida i quina depèn de que l'altra invoqui super correctament per no perdre el seu propi comportament. Amb dos mixins, aquest raonament ja exigeix certa cura; amb tres o més, aplicats en ordres diferents en diferents components d'una mateixa aplicació, el resultat pot tornar-se difícil de predir sense llegir amb atenció el codi de cada mixin implicat, cosa que rares vegades passa amb un únic controlador reactiu (o amb diversos, registrats de manera independent mitjançant addController, sense cap relació d'ordre d'herència entre ells).
- Limitacions dels mixins: col·lisió de noms
El segon problema freqüent dels mixins és la col·lisió de noms: si dos mixins diferents, aplicats sobre la mateixa classe, declaren una propietat o un mètode amb el mateix nom (o si un mixin usa, sense adonar-se'n, un nom que la pròpia classe final ja usava pel seu compte), un dels dos sobreescriu silenciosament l'altre, sense que JavaScript emeti cap avís ni error. Per exemple, si un segon mixin de TaskFlow, pensat per gestionar errors, també declarés una propietat anomenada cargando (potser amb un significat lleugerament diferent), i es combinés amb ConEstadoCarga sobre el mateix component, un dels dos valors de cargando prevaldria sobre l'altre segons l'ordre d'aplicació, i el resultat seria difícil de depurar sense conèixer el codi intern de tots dos mixins.
Aquest risc és, precisament, un dels motius principals pels quals un controlador reactiu sol ser més segur: com que el seu estat viu en un objecte propi (this._contadorTiempo, no directament this), dos controladors diferents mai poden col·lisionar entre si ni amb les propietats del propi host, encara que usin internament noms de camp idèntics, perquè cada un viu en el seu propi espai de noms, aïllat de la resta.
- Tancament del mòdul 6
Amb aquesta lliçó es completa el mòdul 6. El recorregut ha anat de menys a més control sobre el cicle de vida d'un component: primer els callbacks heretats de Custom Elements (connectedCallback, disconnectedCallback), després els hooks propis del cicle d'actualització de Lit (willUpdate, firstUpdated, updated, updateComplete), i finalment dues tècniques de composició per reutilitzar tota aquesta lògica entre diversos components sense duplicar-la: els controladors reactius, recomanats quan el comportament té estat propi i convé mantenir-lo desacoblat, i els mixins, adequats quan el comportament ha d'integrar-se de forma natural en la pròpia classe i la seva API pública, a costa d'assumir el risc de col·lisions de noms i d'un ordre de composició que pot tornar-se difícil de raonar amb diversos mixins combinats.
Amb el cicle de vida ja dominat en totes les seves formes, el curs torna la mirada cap a un terreny diferent: el de les plantilles. El mòdul 7, "Directives i Funcionalitats Avançades de Plantilles", presentarà un conjunt d'eines —directives com classMap, styleMap o until, entre altres— que, en més d'un cas, permeten simplificar patrons que aquest mateix curs ja ha resolt a mà fins ara amb codi JavaScript explícit, exactament el tipus de simplificació que convé apreciar millor una vegada s'entén, com ja passa a partir d'aquest mòdul, què passa realment per sota quan una plantilla es torna a renderitzar.
Errors Comuns i Consells
- Oblidar propagar
Base.propertiesen declararstatic propertiesdins d'un mixin (o en la classe final que l'usa): com s'ha vist als apartats 2 i 4, sense...Base.propertiesqualsevol propietat reactiva declarada en un nivell diferent de la cadena de mixins deixa de registrar-se com a reactiva, produint errors silenciosos on una propietat "no reacciona" sense cap missatge d'error visible. - Encadenar massa mixins sobre un mateix component: com s'explica a l'apartat 6, cada mixin addicional incrementa la dificultat de raonar sobre l'ordre d'execució i sobre possibles col·lisions; si un component comença a necessitar tres o quatre mixins combinats, sol ser un senyal que almenys part d'aquesta lògica encaixaria millor com a controladors reactius independents.
- Usar un mixin per a lògica amb estat que no necessita integrar-se en l'API pública del component: com s'ha explicat a l'apartat 5, si el comportament (com el temporitzador de la lliçó anterior) pot viure perfectament desacoblat, sense que la resta del component necessiti tractar-lo com una propietat o mètode propis, un controlador reactiu evita per complet els riscos de col·lisió de noms i d'ordre de composició.
- No documentar què espera un mixin de la seva classe base: si
ConEstadoCargaassumís, per exemple, l'existència d'un mètoderender()amb una forma concreta (més enllà de rebre el seu resultat com a argument deconIndicadorDeCarga), qualsevol component que l'usi necessitaria conèixer aquest contracte implícit; com més clar i mínim sigui allò que un mixin exigeix de la seva base, més fàcil serà reutilitzar-lo amb seguretat en components futurs.
Exercicis
- Escriu un segon mixin,
ConContadorDeErrores, que afegeixi una propietat d'estatultimoError(inicialitzada anull) i un mètoderegistrarError(mensaje)que l'actualitzi. Aplica'l junt ambConEstadoCargasobreTaskBoard(class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement))), i comprova que ambdues propietats (cargandoiultimoError) convivencen sense problemes, atès que no comparteixen cap nom. - Explica, basant-te en l'apartat 7, què passaria si
ConContadorDeErroresde l'exercici anterior declarés també una propietat anomenadacargando(per exemple, per indicar si s'està reintentant una operació després d'un error), i en quin ordre d'aplicació dels dos mixins prevaldria cada valor. - Reprèn el
ContadorTiempoRestanteControllerde la lliçó anterior i explica, amb les teves pròpies paraules, per què no tindria sentit reescriure'l com un mixin (ConContadorDeTiempoRestante = (Base) => class extends Base {...}) aplicat directament sobreTaskCard, recolzant-te en el criteri de l'apartat 5.
Solucions
// src/mixins/con-contador-de-errores.js
export const ConContadorDeErrores = (Base) => class extends Base {
static properties = {
...Base.properties,
ultimoError: { state: true },
};
constructor(...args) {
super(...args);
this.ultimoError = null;
}
registrarError(mensaje) {
this.ultimoError = mensaje;
}
};class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)) {
static properties = {
...ConContadorDeErrores(ConEstadoCarga(LitElement)).properties,
tareas: { type: Array },
};
}Com que cargando i ultimoError són noms diferents, tots dos mixins afegeixen les seves propietats sense cap conflicte, i TaskBoard acaba amb accés a this.cargando, this.conIndicadorDeCarga(...), this.ultimoError i this.registrarError(...), tots disponibles simultàniament.
-
Si tots dos mixins declaressin una propietat
cargando, el mixin aplicat en últim lloc (el més extern en l'anidament, és a dir, el primer que apareix llegint l'expressió d'esquerra a dreta) seria qui defineixi la versió final destatic properties.cargandoi, en el seuconstructor, la darrera assignació dethis.cargando = ...en executar-se (atès que cada mixin cridasuper(...args)abans d'assignar el seu propi valor per defecte, el mixin més extern s'executa després de la cadena desuper, i la seva assignació és la que queda vigent en acabar la construcció). Aclass TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)), seriaConContadorDeErroresqui prevaldria, i el significat original decargandoaportat perConEstadoCargaquedaria silenciosament sobreescrit, sense cap avís d'error. -
ContadorTiempoRestanteControllermanté un estat intern (l'identificador de l'interval, el valor actual decercaDeVencer) que no necessita, en cap moment, sentir-se part de l'API pública deTaskCard; la resta del component només necessita llegirthis._contadorTiempo.cercaDeVencerdes derender(), sense quecercaDeVencerhagi de ser una propietat reactiva més deTaskCarden peu d'igualtat ambtitulooestado. Convertir-lo en un mixin obligaria a fusionar aquest estat directament aTaskCard(com passa ambcargandoaConEstadoCarga), incrementant el risc de col·lisió de noms si, en el futur,TaskCardo un altre mixin aplicat sobre ella necessités també una propietat anomenadacercaDeVencero similar; a més, el mixin heretaria també el problema d'ordre de composició de l'apartat 6 en quant es combinés amb qualsevol altre mixin futur, cosa que un controlador reactiu, aïllat en el seu propi objecte, evita per complet.
Conclusió
Aquest mòdul ha explicat amb detall el cicle de vida complet d'un component Lit: els callbacks heretats de Custom Elements, els hooks propis del cicle d'actualització de Lit, i dues tècniques de composició —controladors reactius i mixins— per reutilitzar comportament entre components sense duplicar codi. ConEstadoCarga, el mixin d'aquesta darrera lliçó, ha mostrat el cas en el qual fusionar comportament directament en la classe d'un component té sentit, i la comparació amb ContadorTiempoRestanteController ha deixat un criteri clar per decidir, en qualsevol situació futura de TaskFlow, entre una tècnica i l'altra.
Amb el cicle de vida ja dominat, toca veure funcionalitats avançades de plantilles (directives) que simplifiquen patrons ja vistos.
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
