TaskFlow, en acabar la lliçó anterior, ja és una aplicació completa i funcional: cinc components muntats, un context compartit, un controlador reactiu, un mixin i un servei simulat, tots coordinats exactament com es va planejar a la primera lliçó d'aquest mòdul. Falta, abans de poder dir que el projecte està realment acabat, comprovar amb proves automatitzades les peces que s'han completat en aquest mòdul i que el mòdul 9 no va poder cobrir perquè encara no existien, empaquetar l'aplicació per al seu desplegament, i decidir si alguna de les seves peces mereix publicar-se per separat com a llibreria reutilitzable. Aquesta és l'última lliçó del curs: no introdueix cap concepte nou de Lit, i tanca, a la seva darrera secció, el recorregut complet iniciat a la lliçó "Què són els Web Components i per què Lit?".
Contingut
- Què queda per tancar abans d'acabar
- Checklist final de proves: el que 09-01 i 09-02 no van arribar a cobrir
- Nous tests:
<task-filter>i el flux complet d'un esdeveniment - Build final amb Vite: comandes i estructura de
dist/ - Publicant
<task-card>com a llibreria npm reutilitzable - Checklist de desplegament: lloc estàtic davant de llibreria
- TaskFlow, de principi a fi: un repàs mòdul a mòdul
- Propers passos, fora d'aquest curs
- Tancament del curs
- Què queda per tancar abans d'acabar
El mòdul 9 va construir la bateria de proves de TaskFlow (09-01), la seva accessibilitat (09-02), el seu rendiment (09-03) i el seu catàleg de patrons i antipatrons (09-04) sobre l'aplicació tal com existia aleshores: sense <task-filter> ben establert, amb <task-board> encara en una versió anterior de la seva càrrega inicial. Aquest mòdul ha completat aquestes peces, i aquesta lliçó tanca el cercle: estén la bateria de proves al que faltava, deixa l'aplicació preparada per publicar-se, i amb això dona per acabat el curs complet.
- Checklist final de proves: el que 09-01 i 09-02 no van arribar a cobrir
Abans d'escriure cap test nou, convé fer un inventari honest de quina part de TaskFlow ja té cobertura i quina no, utilitzant exactament les mateixes eines de la lliçó "Proves Unitàries amb Web Test Runner" (09-01):
| Component o flux | Té test des del mòdul 9? | Què falta cobrir en aquesta lliçó |
|---|---|---|
<task-card>: títol, insígnia d'estat, clic que expandeix |
Sí (09-01) | Res |
<task-card>: role, aria-expanded |
Sí (09-02) | Res |
<task-filter>: aria-label, aria-pressed, canvi d'estat |
No | Un test que comprovi que prémer un botó actualitza el context de filtre |
<task-list>: filtratge segons el context |
No | Un test d'integració que comprovi que el filtre redueix les targetes visibles |
tarea-cambiada: l'esdeveniment complet, de <task-card> fins a <task-board> |
No | Un test d'integració que comprovi el cicle esdeveniment amunt / propietat avall en dos salts |
Les dues primeres files ja estaven resoltes; les tres últimes són exactament les peces que aquest mòdul ha afegit o completat, i són les que es cobreixen a l'apartat següent.
- Nous tests:
<task-filter> i el flux complet d'un esdeveniment
<task-filter> i el flux complet d'un esdevenimentEl primer test nou comprova <task-filter> de forma aïllada, però per exercitar realment el seu comportament cal embolcallar-la en un proveïdor de context de prova, ja que <task-filter> per si sola, sense cap ancestre que publiqui filtroContext, només veuria el valor de reserva definit en el seu propi valorActual:
// test/task-filter.test.js
import { fixture, html, expect } from '@open-wc/testing';
import { ContextProvider } from '@lit/context';
import { LitElement } from 'lit';
import { filtroContext } from '../src/context/filtro-context.js';
import '../src/components/task-filter.js';
// Un petit component de prova que només publica el context,
// exactament el mateix paper que <task-board> compleix en l'aplicació real.
class ProveedorDePrueba extends LitElement {
constructor() {
super();
this.valor = { texto: '', estado: 'todas', actualizar: (cambios) => this._actualizar(cambios) };
this._provider = new ContextProvider(this, { context: filtroContext, initialValue: this.valor });
}
_actualizar(cambios) {
this._provider.value = { ...this._provider.value, ...cambios };
this.valor = this._provider.value;
}
render() {
return html`<slot></slot>`;
}
}
customElements.define('proveedor-de-prueba', ProveedorDePrueba);
describe('task-filter', () => {
it('expone aria-pressed="true" solo en el botón del estado activo', async () => {
const contenedor = await fixture(
html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
);
const filtro = contenedor.querySelector('task-filter');
const botones = filtro.shadowRoot.querySelectorAll('button');
expect(botones[0].getAttribute('aria-pressed')).to.equal('true'); // "todas", actiu per defecte
expect(botones[1].getAttribute('aria-pressed')).to.equal('false');
});
it('actualiza el contexto compartido al pulsar un botón de estado', async () => {
const contenedor = await fixture(
html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
);
const proveedor = contenedor;
const filtro = contenedor.querySelector('task-filter');
const botonPendientes = filtro.shadowRoot.querySelectorAll('button')[1];
botonPendientes.click();
await filtro.updateComplete;
expect(proveedor.valor.estado).to.equal('pendiente');
});
});El segon test comprova el flux complet de l'apartat 2, tercera fila: que un canvi d'estat disparat dins del shadow root d'una <task-card>, diversos nivells per sota, acaba modificant de veritat l'array tareas de <task-board>, exactament el recorregut descrit al mapa de la lliçó 10-01.
// test/task-board.test.js
import { fixture, html, expect, aTimeout } from '@open-wc/testing';
import '../src/components/task-board.js';
describe('task-board: flujo completo de tarea-cambiada', () => {
it('propaga un cambio de estado desde una task-card hasta el array tareas', async () => {
const tablero = await fixture(html`<task-board></task-board>`);
// La càrrega inicial de tareas-service.js tarda 1200 ms, simulats amb
// setTimeout; aquí s'espera el mateix que s'esperaria al navegador.
await aTimeout(1300);
await tablero.updateComplete;
const lista = tablero.shadowRoot.querySelector('task-list');
const primeraTarjeta = lista.shadowRoot.querySelector('task-card');
const selector = primeraTarjeta.shadowRoot.querySelector('select');
selector.value = 'hecha';
selector.dispatchEvent(new Event('change'));
await primeraTarjeta.updateComplete;
await lista.updateComplete;
await tablero.updateComplete;
expect(tablero.tareas[0].estado).to.equal('hecha');
});
});aTimeout, importada també de @open-wc/testing, és una petita utilitat que retorna una promesa que es resol després del nombre de mil·lisegons indicat; s'utilitza aquí perquè la càrrega simulada de tareas-service.js (lliçó 07-03, consolidada a 10-03) tarda un temps real, no artificial, i el test necessita esperar aquest mateix temps abans que existeixi cap <task-card> sobre la qual actuar. La resta del test encadena tres updateComplete diferents —de la targeta, de la llista, del tauler— perquè, com es va explicar a la lliçó "Comunicació de Pare a Fill amb Propietats" (05-03) i a "Patrons de Comunicació entre Components Germans" (05-04), l'esdeveniment travessa dos reenviaments abans d'arribar a <task-board>, i cada nivell necessita completar la seva pròpia actualització abans que el següent la reflecteixi.
- Build final amb Vite: comandes i estructura de
dist/
dist/Amb les proves ja completes, el build de producció de TaskFlow com a aplicació segueix exactament el criteri fixat a la lliçó "Empaquetatge, Publicació i TypeScript" (08-04, apartats 1 i 4): Vite, no Rollup de forma directa, perquè TaskFlow és una aplicació autosuficient, no una llibreria pensada perquè un altre bundler la torni a processar.
El resultat, a dist/, queda amb aquesta forma:
dist/
├── index.html
└── assets/
├── index-a1b2c3d4.js
├── index-e5f6g7h8.css
└── task-board-i9j0k1l2.jsCada nom de fitxer inclou un fragment hash derivat del seu contingut (a1b2c3d4, en l'exemple), exactament el mecanisme de control de memòria cau explicat a 08-04: si el contingut d'un fitxer no canvia entre dos desplegaments successius, el seu nom tampoc no canvia, i el navegador de qui ja va visitar TaskFlow pot continuar utilitzant la còpia que ja té a la memòria cau sense tornar-la a descarregar. index.html, regenerat per Vite en cada build, ja apunta als noms de fitxer correctes d'aquesta build concreta, sense que calgui editar manualment cap ruta.
- Publicant
<task-card> com a llibreria npm reutilitzable
<task-card> com a llibreria npm reutilitzableTaskFlow, com a aplicació completa, es desplega amb el build de l'apartat anterior. Però hi cap una pregunta diferent, ja plantejada en abstracte a la lliçó 08-04: val la pena publicar <task-card> per separat, perquè altres projectes —no només TaskFlow— la puguin instal·lar com a dependència d'npm? Si la resposta fos afirmativa (una cosa raonable, atès que <task-card> no depèn de cap dada específica de TaskFlow excepte les que rep com a propietats), el package.json d'aquest paquet independent hauria de declarar lit com a dependència externa, no inclosa en el propi bundle, exactament pel motiu explicat a 08-04 (apartat 3): evitar que dos components de tercers, ambdós dependents de Lit, carreguin dues còpies completes de la biblioteca en la mateixa aplicació.
{
"name": "@taskflow/task-card",
"version": "1.0.0",
"type": "module",
"main": "dist/task-card.js",
"module": "dist/task-card.js",
"files": ["dist"],
"peerDependencies": {
"lit": "^3.0.0"
},
"devDependencies": {
"lit": "^3.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"rollup": "^4.0.0"
}
}peerDependencies és la peça que formalitza aquesta decisió: declara que el paquet espera que qui l'instal·li aporti la seva pròpia còpia de lit, en lloc de portar la seva pròpia inclosa. devDependencies manté una còpia de lit disponible només durant el desenvolupament del propi paquet (per poder executar les seves proves o el seu build), sense que aquesta còpia viatgi dins del paquet final publicat. La configuració de Rollup, idèntica a la de la lliçó 08-04, produeix el dist/task-card.js que aquest package.json referencia:
// rollup.config.js (del paquet @taskflow/task-card, no de l'aplicació TaskFlow)
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/task-card.js',
external: ['lit', /^lit\//],
output: {
file: 'dist/task-card.js',
format: 'esm',
},
plugins: [resolve()],
};external: ['lit', /^lit\//] és el detall que faltava a l'exemple de la lliçó 08-04: indica a Rollup que no inclogui, al bundle final, ni el propi paquet lit ni cap dels seus submòduls (lit/directives/..., lit/decorators.js), deixant aquests import intactes a la sortida perquè el bundler de qui instal·li @taskflow/task-card els resolgui amb la seva pròpia còpia de Lit, coherent amb la declaració de peerDependencies del package.json.
- Checklist de desplegament: lloc estàtic davant de llibreria
| Pas | Com a aplicació completa (dist/ de l'apartat 4) |
Com a llibreria (@taskflow/task-card de l'apartat 5) |
|---|---|---|
| Eina de build | Vite (vite build) |
Rollup, amb lit com a dependència externa |
| Inclou una còpia de Lit? | Sí, el bundle és autosuficient | No, es declara com a peerDependency |
| On es publica | Un servidor de fitxers estàtics o una CDN | El registre d'npm |
| Qui ho consumeix | Persones que visiten TaskFlow directament al seu navegador | Altres projectes, com una dependència més del seu propi package.json |
| Quines proves han de passar abans | Tota la bateria de test/*.test.js (mòdul 9 i aquesta lliçó) |
Els mateixos tests, executats contra el propi paquet abans de publicar cada versió |
Els dos camins són legítims i no s'exclouen mútuament: res impedeix que TaskFlow es despleguí com a aplicació completa (apartat 4) mentre, en paral·lel, <task-card> es publiqui també com a paquet independent (apartat 5) perquè altres equips la reutilitzin fora de TaskFlow, exactament el mateix component en tots dos casos, només empaquetat de dues formes diferents segons a qui va dirigit cada resultat.
- TaskFlow, de principi a fi: un repàs mòdul a mòdul
Amb TaskFlow ja provat, empaquetat i llest per desplegar-se, val la pena mirar enrere i recórrer, d'un cop d'ull, el camí complet d'aquest curs:
| Mòdul | Què va aportar a TaskFlow |
|---|---|
| 1. Introducció a Lit i Web Components | El primer component Lit, sense cap dada ni interacció encara. |
| 2. Plantilles Reactives i Renderitzat | El motor de plantilles, condicionals, llistes i el cicle de renderitzat. |
| 3. Propietats i Estat Reactiu | titulo, estado, prioridad, expandida, i el conversor de fechaLimite. |
| 4. Estils en Components Lit | Shadow DOM, estils compartits, variables CSS de theming, i <user-avatar> amb slots. |
| 5. Esdeveniments i Comunicació entre Components | tarea-cambiada, el patró d'elevar l'estat, i el naixement de <task-board>. |
| 6. Cicle de Vida i Comportament Avançat | ContadorTiempoRestanteController i el mixin ConEstadoCarga. |
| 7. Directives i Funcionalitats Avançades de Plantilles | classMap, resaltarSiUrgente, until, i <task-filter> amb @lit/context. |
| 8. Integració, Interoperabilitat i Desplegament | Ús en HTML pla i en altres frameworks, SSR, empaquetatge amb Rollup i Vite. |
| 9. Proves i Bones Pràctiques | La primera bateria de tests, accessibilitat, rendiment, i el catàleg d'antipatrons. |
| 10. Projecte: Construint TaskFlow | Arquitectura consolidada, components unificats, i el tancament final d'aquesta lliçó. |
Cap fila d'aquesta taula és una sorpresa a aquesta alçada del curs; s'inclou aquí, a l'última lliçó, precisament perquè el recorregut complet es vegi d'un sol cop, una cosa que cap lliçó anterior, centrada en el seu propi mòdul, podia oferir.
- Propers passos, fora d'aquest curs
Aquest curs ha cobert Lit de forma exhaustiva per al tipus d'aplicació que representa TaskFlow, però l'ecosistema al voltant de Lit continua creixent, i convé deixar assenyalats alguns camins concrets per a qui vulgui continuar més enllà d'aquesta última lliçó:
@lit-labs/virtualizer: la lliçó "Rendiment i Optimització" (09-03, apartat 9) va esmentar la virtualització com a tècnica per a llistes de desenes de milers d'elements, fora de l'abast pràctic d'aquest curs, i va assenyalar que TaskFlow no la necessitava. És, literalment, el següent pas natural si<task-list>hagués de créixer molt més enllà dels volums de dades gestionats al mòdul 9.@lit-labs/motion: un paquet experimental del propi ecosistema de Lit per a animacions declaratives entre estats, un terreny que aquesta lliçó només ha tocat de forma manual amb l'animació CSS deresaltarSiUrgente(lliçó 10-02).@lit-labs/observers: controladors reactius ja escrits per l'equip de Lit per embolcallarResizeObserver,IntersectionObserveri altres API d'observació del navegador, seguint exactament el mateix patró deReactiveControllerestudiat a la lliçó 06-03 ambContadorTiempoRestanteController.- Explorar Lit amb TypeScript i decoradors més a fons: la lliçó 08-04 va presentar l'equivalència sintàctica entre
static propertiesi decoradors; migrar progressivament TaskFlow, component a component, és un exercici pràctic natural per consolidar aquesta lliçó amb un projecte real. - La documentació oficial a lit.dev: en particular la seva secció de receptes (Playground) i les seves notes de cada versió, per seguir de prop qualsevol canvi futur de l'API a mesura que Lit evolucioni més enllà de la versió estudiada en aquest curs.
- Tancament del curs
Aquest curs va començar, a la lliçó "Què són els Web Components i per què Lit?", amb una pregunta sobre estàndards de la plataforma web i una promesa: aprendre a construir interfícies reactives amb components propis del navegador, sense dependre d'un framework d'aplicació complet. Deu mòduls i trenta-nou lliçons després, aquesta promesa s'ha complert amb una aplicació real i completa, TaskFlow, construïda peça a peça, mòdul a mòdul, sense deixar cap cap solt: cada concepte presentat es va aplicar de seguida sobre el propi projecte, i cada peça esmentada sense resoldre del tot —des de <task-filter> al mòdul 5 fins a l'animació de resaltarSiUrgente en aquesta última lliçó— va trobar el seu lloc abans que el curs acabés.
Qui hagi seguit el curs complet, escrivint i provant cada exemple, ja té el criteri necessari per abordar un projecte real amb Lit: quan cal elevar l'estat a un ancestre comú i quan recórrer a un context compartit; quan un controlador reactiu i quan un mixin; quan until i quan una propietat de càrrega explícita; i, sobretot, l'hàbit de preguntar-se, davant de cada decisió de disseny, quina de les dues alternatives evita millor els antipatrons catalogats al mòdul 9. Aquest criteri, més que la memorització d'una API concreta, és el que sobreviu un cop tancat un curs.
Enhorabona per haver arribat fins aquí. TaskFlow queda acabat, provat i llest per desplegar-se; la resta del camí, amb Lit o amb qualsevol altra eina, queda ja a les teves mans.
Errors Comuns i Consells
- Publicar una llibreria sense declarar
litcom a dependència externa: com es va explicar a l'apartat 5, i ja s'havia advertit a la lliçó 08-04, oblidarexternala la configuració de Rollup inclouria una còpia completa de Lit dins del paquet publicat, amb el risc de duplicació ja conegut. - Executar
vite buildesperant el mateix resultat que per a una llibreria: com recorda la taula de l'apartat 6, cada camí de desplegament té una eina i una configuració diferents; mesclar tots dos objectius en la mateixa configuració de build sol produir un resultat que no serveix bé per a cap dels dos casos. - Donar per bona una bateria de tests incompleta només perquè el mòdul 9 ja la va començar: com s'ha vist a l'apartat 2,
<task-filter>i el flux complet detarea-cambiadano tenien cap test fins a aquesta lliçó; convé revisar, abans de donar cap projecte per provat, quines peces es van afegir després de l'última vegada que es van escriure tests. - Tractar aquesta lliçó com el final de l'aprenentatge, no del curs: com assenyala l'apartat 8, l'ecosistema de Lit continua viu més enllà d'aquestes quaranta lliçons; el criteri après aquí és la base per continuar explorant, no un punt final.
Exercicis
- Afegeix un test a
test/task-board.test.jsque comprovi que, després de canviar el text de cerca a l'<input>de<task-filter>(accessible des detablero.shadowRoot.querySelector('task-filter').shadowRoot.querySelector('input')), el nombre de<task-card>visibles dins de<task-list>es redueix en conseqüència. Recorda esperarupdateCompletea cada nivell implicat, com al test de l'apartat 3. - Completa el
package.jsonde l'apartat 5 afegint un script"build": "rollup -c"i explica, basant-te en la lliçó 08-04 (apartat 2), per què aquest paquet utilitza Rollup mentre que l'aplicació TaskFlow completa, a l'apartat 4 d'aquesta lliçó, utilitza Vite per al mateix verb "fer el build". - Repassa la taula de l'apartat 7 i, per al mòdul que consideris que va aportar el canvi més important en l'arquitectura general de TaskFlow (no en una tècnica concreta, sinó en com s'organitzen els components entre si), justifica la teva elecció amb un exemple concret del propi projecte.
Solucions
it('reduce las tarjetas visibles al escribir en el filtro de texto', async () => {
const tablero = await fixture(html`<task-board></task-board>`);
await aTimeout(1300);
await tablero.updateComplete;
const filtro = tablero.shadowRoot.querySelector('task-filter');
const lista = tablero.shadowRoot.querySelector('task-list');
const input = filtro.shadowRoot.querySelector('input');
const tarjetasAntes = lista.shadowRoot.querySelectorAll('task-card').length;
input.value = 'demo';
input.dispatchEvent(new Event('input'));
await filtro.updateComplete;
await lista.updateComplete;
const tarjetasDespues = lista.shadowRoot.querySelectorAll('task-card').length;
expect(tarjetasDespues).to.be.lessThan(tarjetasAntes);
});- Aquest paquet utilitza Rollup perquè el seu objectiu, tal com es va explicar a la lliçó 08-04 (apartat 1) i es va recordar a l'apartat 6 d'aquesta lliçó, és produir una llibreria reutilitzable per altres bundlers, no una aplicació autosuficient llesta per servir-se directament; Rollup genera una sortida neta i propera al codi d'entrada, pensada precisament perquè un bundler extern (el de qui instal·li
@taskflow/task-card) la torni a processar segons les seves pròpies necessitats. L'aplicació TaskFlow completa, en canvi, no té cap consumidor extern que la torni a empaquetar: el seu objectiu és el resultat final ja optimitzat per al navegador, amb divisió en fragments i gestió de memòria cau per hash, que Vite ofereix de fàbrica (utilitzant Rollup per sota, però amb una configuració orientada a aplicacions, no a llibreries). - Una resposta raonable assenyala el mòdul 5, amb l'aparició de
<task-board>: abans d'aquesta lliçó,<task-list>mantenia el seu propi array de tasques d'exemple i no existia cap component capaç de coordinar més de dos nivells de la jerarquia; a partir de "Patrons de Comunicació entre Components Germans" (05-04), tota l'arquitectura de TaskFlow va passar a organitzar-se al voltant d'un ancestre comú que reparteix propietats cap avall i recull esdeveniments cap amunt, el mateix patró que, ampliat amb el context de filtre al mòdul 7, continua vigent sense canvis estructurals fins a la versió final consolidada en aquest mòdul 10. Altres respostes igualment vàlides podrien assenyalar el mòdul 7, per la introducció de@lit/contextcom a alternativa al reenviament manual de propietats, o el mòdul 4, per l'aparició de<user-avatar>com a primer exemple de descomposició en un component fill reutilitzable.
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
