En tancar la lliçó anterior el model de Nómada Tasques estava complet i protegit: Tasca amb el seu estat privat, Tauler amb la seva API pública mínima, ErrorDeValidacio amb la seva jerarquia. El problema és que tot això viu en un únic js/app.js que ja passa de les tres-centes línies, barrejat amb les constants, el backlog d'exemple i el codi que l'executa. Partir-lo en diversos fitxers i carregar-los amb <script> no soluciona res: tots comparteixen el mateix espai global, l'ordre de càrrega es converteix en una dependència invisible que ningú no ha escrit, i n'hi ha prou que dos fitxers declarin const PESOS perquè l'aplicació caigui. En aquesta lliçó aprendràs el sistema de mòduls de JavaScript: fronteres explícites entre fitxers, amb export per declarar què s'ofereix i import per declarar què es necessita. Al final hauràs reorganitzat el projecte sencer en sis fitxers reals, amb un graf de dependències que es pot dibuixar.
Contingut
- El problema: àmbit global, ordre i dependències invisibles
export: exportacions amb nomimport: portar el que necessites- Reanomenar amb
asi importar-ho tot amb* as export default- Reexportar: el fitxer de barril
- Àmbit propi i execució única
<script type="module">i què canviaimport()dinàmic iawaita nivell de mòdul- CommonJS:
requireimodule.exports - Node i
"type": "module" - Dependències circulars
- Nómada Tasques, reorganitzat
- Errors Habituals i Consells
- Exercicis
- Conclusió
- El problema: àmbit global, ordre i dependències invisibles
Suposem que parteixes el fitxer gran en tres i els carregues com vas aprendre al Mòdul 1:
<script src="js/constants.js"></script>
<script src="js/tasca.js"></script>
<script src="js/app.js"></script>Això té tres problemes seriosos, i cap no dona un missatge d'error clar.
Problema 1: tot comparteix el mateix àmbit global. Cada <script> clàssic executa el seu codi a l'àmbit global, així que totes les declaracions de nivell superior conviuen al mateix espai de noms.
// js/constants.js
const PESOS = { alta: 3, mitjana: 2, baixa: 1 };
// js/format.js
const PESOS = { negreta: 700, normal: 400 };
// ✗ SyntaxError: Identifier 'PESOS' has already been declaredDos fitxers escrits per dues persones diferents, cadascun perfectament raonable, i l'aplicació no arrenca. Amb var seria encara pitjor: no hi hauria error, el segon trepitjaria el primer en silenci i el càlcul de l'esforç donaria NaN en algun lloc inesperat.
Problema 2: l'ordre importa i no està escrit enlloc. Si algú mou el <script> de tasca.js per sobre del de constants.js, la classe Tasca intentarà fer servir PESOS abans que existeixi. L'HTML es converteix en un fitxer de configuració crític que ningú no documenta.
Problema 3: les dependències són invisibles. Obre tasca.js i digues de què depèn. No ho pots saber sense llegir-lo sencer i buscar els identificadors que no declara. No hi ha cap línia que digui «aquest fitxer necessita PESOS i ErrorDeValidacio».
L'apedaçament clàssic, abans que existissin els mòduls, era el patró mòdul de 03-04: tancar-ho tot dins d'una funció que s'invoca a si mateixa i penjar del global una sola variable.
var NomadaTasques = (function () {
const PESOS = { alta: 3, mitjana: 2, baixa: 1 }; // privat gràcies al closure
class Tasca { /* … */ }
return { Tasca }; // única cosa pública
})();Funciona —va ser la millor pràctica durant anys— però continua sense resoldre l'ordre ni fer explícites les dependències, i n'hi ha prou que dues llibreries triïn el mateix nom global per tornar al problema 1. Des d'ES2015, el llenguatge porta la solució de sèrie.
export: exportacions amb nom
export: exportacions amb nomUn mòdul és simplement un fitxer .js que fa servir export o import. Tot el que declara és privat a aquell fitxer tret del que exporta explícitament.
// util/dates.js
export const AVUI = '2026-09-20';
export function diesEntre(des, fins) {
const ms = new Date(fins) - new Date(des);
return Math.round(ms / (1000 * 60 * 60 * 24));
}
export function estaVencuda(dataLimit, estat, avui = AVUI) {
return dataLimit < avui && estat !== 'feta'; // R10
}
// Aquesta NO s'exporta: és un detall intern, invisible des de fora
function normalitzarData(text) {
return new Date(text).toISOString().slice(0, 10);
}Hi ha dues maneres d'escriure el mateix. La primera és posar export davant de cada declaració, com a dalt. La segona és agrupar al final del fitxer, cosa que dona una «taula de continguts» molt còmoda de llegir:
// util/dates.js — variant amb llista d'exportació
const AVUI = '2026-09-20';
function diesEntre(des, fins) { /* … */ }
function estaVencuda(dataLimit, estat, avui = AVUI) { /* … */ }
function normalitzarData(text) { /* … */ }
export { AVUI, diesEntre, estaVencuda }; // normalitzarData es queda a dinsEs pot exportar qualsevol cosa amb nom: const, let, function, class. I es pot reanomenar en la mateixa exportació:
Les declaracions
exportiimporthan d'estar al nivell superior del mòdul, mai dins d'unif, un bucle o una funció. El motiu és que s'analitzen abans d'executar res: això permet al navegador descobrir el graf de fitxers i descarregar-los en paral·lel, i és també el que fa possible detectar errors d'importació sense arribar a executar el programa. Per importar condicionalment existeiximport()dinàmic (apartat 9).
import: portar el que necessites
import: portar el que necessitesA l'altre extrem, import declara què es necessita i d'on ve.
// model/tasca.js
import { AVUI, estaVencuda } from '../util/dates.js';
export class Tasca {
// …
estaVencuda(avui = AVUI) {
return estaVencuda(this.dataLimit, this.estat, avui);
}
}Les claus d'import { … } no són desestructuració, encara que s'hi assemblin molt a la de 04-07. És una sintaxi pròpia del sistema de mòduls: els noms han de coincidir exactament amb els exportats, no admet valors per defecte ni patrons imbricats, i es resol abans d'executar.
Sobre la ruta del mòdul (l'especificador), tres regles que causen molts entrebancs al navegador:
| Especificador | Significat | Val al navegador? |
|---|---|---|
'./dates.js' |
Relatiu a aquest fitxer, mateixa carpeta | Sí |
'../util/dates.js' |
Relatiu, pujant un nivell | Sí |
'/js/util/dates.js' |
Absolut des de l'arrel del lloc | Sí |
'dates.js' |
Bare specifier (sense ./) |
No: el navegador ho interpreta com el nom d'un paquet |
I la més important: al navegador, l'extensió .js és obligatòria. import { AVUI } from './util/dates' falla amb un 404. Node en mode ESM també l'exigeix. És l'error número u de qui ve d'altres entorns on es pot ometre.
- Reanomenar amb
as i importar-ho tot amb * as
as i importar-ho tot amb * asSi dos mòduls exporten alguna cosa amb el mateix nom, o si el nom original xoca amb una variable local, es reanomena en importar:
import { estaVencuda as dataVencuda } from '../util/dates.js';
import { estaVencuda as tascaVencuda } from './regles.js';
console.log(dataVencuda('2026-09-05', 'pendent')); // trueI si vols tot el mòdul agrupat sota un nom:
import * as dates from '../util/dates.js';
console.log(dates.AVUI); // '2026-09-20'
console.log(dates.diesEntre('2026-09-20', '2026-09-30')); // 10
console.log(dates.normalitzarData); // undefined ← no estava exportadaAquest dates és l'objecte d'espai de noms del mòdul: conté una propietat per cada exportació. Té dues particularitats: està congelat (no li pots afegir ni canviar res) i les seves propietats són enllaços vius, no còpies, cosa que veuràs a l'apartat 7.
| Forma | Quan fer-la servir |
|---|---|
import { a, b } from '…' |
El normal: es veu d'un cop d'ull què es fa servir |
import { a as x } from '…' |
Hi ha col·lisió de noms o l'original és poc clar aquí |
import * as ns from '…' |
El mòdul exporta moltes coses relacionades i el prefix aporta claredat (dates.diesEntre) |
import '…' |
Només interessa l'efecte d'executar-lo (registrar alguna cosa, carregar estils); no es porta cap nom |
export default
export defaultCada mòdul pot tenir una exportació per defecte, pensada per quan el fitxer representa una sola cosa.
// app.js
import Tauler from './model/tauler.js'; // sense claus, i el nom el tries tu
import ElTaulerDelTaller from './model/tauler.js'; // també vàlid: és el mateixLa diferència amb les exportacions amb nom:
| Amb nom | Per defecte | |
|---|---|---|
| Quantes per mòdul | Les que vulguis | Una |
| Sintaxi d'importació | import { X } from … |
import X from … |
| El nom ha de coincidir? | Sí | No, el posa qui importa |
| Errors de teclat | Es detecten en carregar | Passen desapercebuts |
| Autocompletat de l'editor | Funciona bé | Pitjor |
Es poden combinar al mateix fitxer:
// model/tasca.js
export default class Tasca { /* … */ }
export class TascaRecurrent extends Tasca { /* … */ }
export const ESTATS = ['pendent', 'en-curs', 'feta'];En aquest projecte farem servir exportacions amb nom a tots els fitxers. No és l'única opció vàlida —molts equips fan servir default per a la peça principal de cada mòdul—, però les nominals tenen dos avantatges pràctics: el nom és el mateix a tot el codi base, cosa que fa que buscar Tauler trobi tots els usos, i un error de teclat salta a l'instant en lloc de donar undefined a mitja execució.
- Reexportar: el fitxer de barril
Un mòdul pot reexportar el que importa d'altres, sense arribar a fer-lo servir. Serveix per oferir un punt d'entrada únic a un grup de fitxers.
// model/index.js — un "barril"
export { Tasca, TascaRecurrent } from './tasca.js';
export { Tauler } from './tauler.js';
export { ErrorDeValidacio, ErrorDeDades } from './errors.js';
// També es pot reexportar tot el d'un mòdul:
export * from './regles.js';
// O reexportar un default amb nom:
export { default as Formatador } from './format.js';Qui el consumeix veu un únic mòdul:
Els barrils són còmodes, però convé un advertiment honest: en importar del barril arrossegues la càrrega de tots els fitxers que reexporta, encara que només en facis servir un. Amb sis fitxers és irrellevant; en projectes grans és una de les causes típiques que l'arrencada es torni lenta, i la seva relació amb la divisió de codi es tracta a 09-05. Per a un projecte de la mida de Nómada Tasques, importar directament de cada fitxer és més clar.
- Àmbit propi i execució única
Dues propietats dels mòduls que resolen exactament els problemes de l'apartat 1.
Cada mòdul té el seu propi àmbit. Res del que declares és global, així que dos mòduls poden declarar const PESOS sense assabentar-se l'un de l'altre. S'ha acabat el problema 1.
// util/format.js
const PESOS = { negreta: 700, normal: 400 }; // ✓ sense conflicte amb el de regles.jsUn mòdul s'executa una sola vegada, per moltes vegades que s'importi. La primera importació el carrega i l'executa; les altres reben el resultat ja calculat. Això converteix cada mòdul en un singleton de facto:
// dades/comptador.js
console.log("⚙ comptador.js s'està executant");
export const registre = [];
export function anotar(text) { registre.push(text); }// model/tauler.js
import { anotar } from '../dades/comptador.js';
anotar('tauler carregat');
// model/tasca.js
import { anotar, registre } from '../dades/comptador.js';
anotar('tasca carregada');
console.log(registre); // [ 'tauler carregat', 'tasca carregada' ] ← el MATEIX arrayA la consola, ⚙ comptador.js s'està executant apareix una sola vegada. Els dos mòduls comparteixen el mateix registre perquè comparteixen la mateixa instància del mòdul. Aquesta propietat és utilíssima —un mòdul de configuració, una memòria cau, un magatzem compartit—, però convé tenir-la present: l'estat que posis a nivell de mòdul és global per a tota l'aplicació, amb tots els inconvenients que això té per a les proves del Mòdul 8.
I un detall fi: les importacions són enllaços vius, no còpies. Si el mòdul d'origen reassigna una variable exportada, qui la va importar veu el valor nou.
// app.js
import { crides, incrementar } from './comptador.js';
console.log(crides); // 0
incrementar();
console.log(crides); // 1 ← s'ha actualitzat sol
// crides = 5; // ✗ TypeError: Assignment to constant variableLes variables importades són de només lectura des del mòdul que importa: només el mòdul propietari les pot canviar. És una forma d'encapsulació que encaixa perfectament amb el que vas aprendre a 05-03.
<script type="module"> i què canvia
<script type="module"> i què canviaPerquè el navegador tracti un fitxer com a mòdul cal dir-l'hi:
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title>Nómada Tasques</title>
<link rel="stylesheet" href="css/estils.css">
</head>
<body>
<h1>Nómada Tasques · Taller Nómada</h1>
<!-- UN sol script: el punt d'entrada. Els imports porten la resta. -->
<script type="module" src="js/app.js"></script>
</body>
</html>Un sol <script> per a tota l'aplicació: els import s'encarreguen de la resta, i el navegador descobreix i descarrega el graf complet. Aquestes són les diferències respecte a un script clàssic:
| Aspecte | <script> clàssic |
<script type="module"> |
|---|---|---|
| Àmbit de les declaracions | Global compartit | Propi del mòdul |
| Mode estricte | Només amb 'use strict' |
Sempre actiu |
| Moment d'execució | Bloqueja l'HTML en descarregar i executar | defer implícit: espera que l'HTML estigui llest |
| Execucions si s'inclou dues vegades | Dues | Una |
import/export |
SyntaxError |
Permesos |
this al nivell superior |
window |
undefined |
| Origen del fitxer | Qualsevol, inclòs file:// |
Requereix HTTP i respecta CORS |
El defer implícit és una bona notícia pràctica: quan arribis al Mòdul 6 i comencis a buscar elements de la pàgina, l'HTML ja estarà completament construït, sense necessitat d'esperar cap esdeveniment. I si necessites el contrari —que s'executi així que es descarregui, sense esperar—, existeix <script type="module" async src="…">.
L'última fila és la que més maldecaps dona. Obrir l'index.html amb doble clic no funciona amb mòduls:
Access to script at 'file:///…/js/app.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, https, …
El motiu és de seguretat: els mòduls es descarreguen amb les mateixes regles que qualsevol petició de xarxa, i el protocol file:// no té un origen vàlid. La solució és la que ja vas conèixer a 01-02: aixecar un servidor local.
# Amb l'extensió Live Server de VS Code: clic dret → "Open with Live Server"
# Amb Python, si el tens instal·lat
python3 -m http.server 8000
# Amb Node
npx serveI després obrir http://localhost:8000, no la ruta del fitxer. A partir d'ara, tot el desenvolupament del projecte es fa amb un servidor local en marxa.
import() dinàmic i await a nivell de mòdul
import() dinàmic i await a nivell de mòdulLes importacions estàtiques van al principi del fitxer i es resolen abans d'executar. Quan necessites carregar alguna cosa condicionalment o en un moment concret, existeix la forma de funció:
// import() s'assembla a una crida a funció i retorna una promesa
const modul = await import('./informes/exportadorPDF.js');
modul.exportar(tauler);Ens centrem aquí només en la sintaxi, perquè el seu ús principal —dividir l'aplicació en trossos que es descarreguen sota demanda perquè arrenqui abans— és matèria de 09-05, i perquè el que retorna és una promesa, un objecte que estudiaràs a fons a 05-06. Retén tres diferències:
import estàtic |
import() dinàmic |
|
|---|---|---|
| On es pot escriure | Només al nivell superior | A qualsevol lloc: dins d'un if, d'una funció… |
| Quan es resol | Abans d'executar el mòdul | En el moment en què s'executa la línia |
| La ruta pot ser variable | No, ha de ser un literal | Sí: import(\./idiomes/${codi}.js`)` |
| Què retorna | Res (declara enllaços) | Una promesa amb l'objecte d'espai de noms |
Aquell await de l'exemple tampoc no és habitual fora d'una funció: als mòduls ES està permès fer servir await directament al nivell superior, cosa que s'anomena top-level await.
// dades/configuracio.js
const resposta = await carregarConfiguracioSimulada(); // ✓ només vàlid en un mòdul ES
export const config = resposta;Quan un mòdul fa servir await al nivell superior, tots els mòduls que l'importen esperen que acabi abans d'executar-se. És una eina còmoda, però té un cost real en l'arrencada de l'aplicació, així que convé fer-la servir amb cap. Hi tornaràs a 05-06, quan await deixi de ser una paraula que apareix de passada.
- CommonJS:
require i module.exports
require i module.exportsAbans que els mòduls ES existissin, Node.js va inventar el seu propi sistema, anomenat CommonJS, i continua sent omnipresent. El trobaràs constantment, així que cal reconèixer-lo.
// util/dates.js — versió CommonJS
const AVUI = '2026-09-20';
function diesEntre(des, fins) {
return Math.round((new Date(fins) - new Date(des)) / 86400000);
}
module.exports = { AVUI, diesEntre };
// o bé: exports.AVUI = AVUI; exports.diesEntre = diesEntre;// app.js — versió CommonJS
const { AVUI, diesEntre } = require('./util/dates.js');
const dates = require('./util/dates.js'); // l'objecte sencer
console.log(diesEntre(AVUI, '2026-09-30')); // 10Aquí les claus sí que són desestructuració de debò (04-07), perquè require retorna un objecte normal i corrent. Aquesta és la diferència de fons entre els dos sistemes:
ESM (import/export) |
CommonJS (require/module.exports) |
|
|---|---|---|
| Estàndard | Del llenguatge, des d'ES2015 | De Node.js |
| Quan es resol | Estàticament, abans d'executar | En temps d'execució, en arribar a la línia |
| Càrrega | Asíncrona | Síncrona (bloqueja) |
| On es pot escriure | Només al nivell superior | A qualsevol lloc, inclòs dins d'un if |
| Ruta variable | No | Sí |
| Què s'importa | Enllaços vius | Una còpia de l'objecte en aquell instant |
| Funciona al navegador | Sí | No sense eines |
await al nivell superior |
Sí | No |
Extensió .js obligatòria |
Sí | No |
La diferència entre «enllaç viu» i «còpia» té conseqüències visibles:
// comptador.cjs
let crides = 0;
function incrementar() { crides += 1; }
module.exports = { crides, incrementar };const { crides, incrementar } = require('./comptador.cjs');
incrementar();
console.log(crides); // 0 ← còpia del valor en el moment d'importar!Amb ESM, aquell mateix codi imprimiria 1. És una font clàssica de confusió en migrar entre sistemes.
Quin fer servir avui: ESM en tot el que sigui nou. És l'estàndard del llenguatge, funciona al navegador i a Node, i permet l'anàlisi estàtica de la qual depenen les eines de 09-05. CommonJS el veuràs en projectes amb anys, en molts fitxers de configuració i en dependències antigues.
- Node i
"type": "module"
"type": "module"Al navegador, type="module" a l'etiqueta decideix el sistema. A Node ho decideix el package.json de la carpeta:
{
"name": "nomada-tasques",
"version": "1.0.0",
"type": "module",
"scripts": {
"inici": "node js/app.js"
}
}Amb "type": "module", Node interpreta tots els .js d'aquell projecte com a mòduls ES. Sense aquella línia (o amb "type": "commonjs"), els interpreta com a CommonJS. I si necessites barrejar, les extensions manen sobre el package.json:
| Extensió | S'interpreta com |
|---|---|
.js |
El que digui "type" al package.json més proper |
.mjs |
Sempre mòdul ES |
.cjs |
Sempre CommonJS |
Amb això, ja pots executar el projecte des del terminal, cosa molt còmoda per provar la lògica sense obrir el navegador:
Un avís freqüent: en un mòdul ES no existeixen require, __dirname ni __filename. Si els necessites, hi ha equivalents moderns, però el normal és no trobar-los a faltar.
- Dependències circulars
Dos mòduls que s'importen mútuament formen un cicle. No és un error del llenguatge, però sol acabar en undefined en el pitjor moment.
// model/tasca.js
import { Tauler } from './tauler.js';
export class Tasca {
moure(desti) { return new Tauler(desti); }
}
// model/tauler.js
import { Tasca } from './tasca.js';
export class Tauler {
afegir(dades) { return new Tasca(dades); }
}El que passa per dins és que un mòdul comença a executar-se, troba l'import, va a l'altre, i aquell torna al primer —que encara no ha acabat d'executar-se—. Amb les class, que tenen zona morta temporal (05-02), el resultat típic és un error del tipus Cannot access 'Tasca' before initialization.
flowchart LR
A["tasca.js"] -->|"import { Tauler }"| B["tauler.js"]
B -->|"import { Tasca }"| A
A -.->|"⚠ cicle"| A
Gairebé sempre un cicle és un senyal de disseny: dos mòduls que es necessiten mútuament en realitat són un, o els en falta un tercer. Les tres sortides habituals:
- Extreure el que és comú a un tercer mòdul. Si tots dos necessiten les constants i els errors, treu-los a
model/errors.jsimodel/regles.js, i que tots dos en depenguin. - Invertir la dependència. Que
TaulerconeguiTasca(hi afegeix tasques), però queTascano coneguiTauler. Si una tasca necessita alguna cosa del tauler, que li ho passin per paràmetre. - Fusionar els dos mòduls si de debò són inseparables.
A Nómada Tasques apliquem la número 2, que és la que produeix un graf net: les capes de baix no coneixen les de dalt.
- Nómada Tasques, reorganitzat
Aquí tens el projecte sencer repartit en els seus mòduls definitius. Aquesta és l'estructura sobre la qual treballaran tots els mòduls següents del curs.
nomada-tasques/
├── index.html
├── package.json ← { "type": "module" }
├── css/
│ └── estils.css
└── js/
├── app.js ← punt d'entrada
├── model/
│ ├── tasca.js ← class Tasca, TascaRecurrent
│ ├── tauler.js ← class Tauler
│ └── errors.js ← ErrorDeValidacio, ErrorDeDades
├── dades/
│ └── backlog.js ← el backlog canònic
└── util/
├── dates.js ← AVUI, diesEntre, estaVencuda
└── format.js ← marques, plurals, taules de textI el seu graf de dependències:
flowchart TD
APP["js/app.js<br/>punt d'entrada"]
TAU["model/tauler.js"]
TAS["model/tasca.js"]
BAC["dades/backlog.js"]
ERR["model/errors.js"]
DAT["util/dates.js"]
FOR["util/format.js"]
APP --> TAU
APP --> BAC
APP --> FOR
APP --> ERR
TAU --> TAS
TAU --> ERR
TAS --> ERR
TAS --> DAT
TAS --> FOR
BAC --> TAS
Fixa't que totes les fletxes van cap avall: util/ i model/errors.js no depenen de ningú, model/tasca.js en depèn, model/tauler.js depèn de la tasca, i app.js és a dalt de tot. No hi ha cicles, i cada fitxer declara a les seves primeres línies exactament de què depèn.
Els fitxers, ara. Primer les fulles del graf:
// js/util/dates.js
export const AVUI = '2026-09-20';
/** Dies de diferència entre dues dates ISO (negatiu si 'fins' ja ha passat). */
export function diesEntre(des, fins) {
return Math.round((new Date(fins) - new Date(des)) / 86400000);
}
/** R10: vençuda = data límit passada i la tasca sense acabar. */
export function estaVencuda(dataLimit, estat, avui = AVUI) {
return dataLimit < avui && estat !== 'feta';
}
/** '2026-09-05' → '5 de setembre de 2026' */
export function dataLlegible(iso) {
const MESOS = ['gener', 'febrer', 'març', 'abril', 'maig', 'juny',
'juliol', 'agost', 'setembre', 'octubre', 'novembre', 'desembre'];
const [any, mes, dia] = iso.split('-');
return `${Number(dia)} de ${MESOS[Number(mes) - 1]} de ${any}`;
}// js/util/format.js
export const MARQUES = Object.freeze({ pendent: '○', 'en-curs': '▸', feta: '✓' });
export const PESOS = Object.freeze({ alta: 3, mitjana: 2, baixa: 1 });
export function marcaEstat(estat) {
return MARQUES[estat] ?? '?';
}
export function plural(quantitat, singular, pluralForma) {
return `${quantitat} ${quantitat === 1 ? singular : pluralForma}`;
}
/** Alinea un text a la dreta amb espais, per a taules de consola. */
export function alinear(text, ample) {
return String(text).padStart(ample, ' ');
}// js/model/errors.js
export class ErrorDeValidacio extends Error {
constructor(missatge, camp, valorRebut) {
super(missatge);
this.name = 'ErrorDeValidacio';
this.camp = camp;
this.valorRebut = valorRebut;
}
descriure() {
return `[${this.name}] ${this.camp}: ${this.message}`;
}
}
export class ErrorDeDades extends Error {
constructor(missatge, causa) {
super(missatge);
this.name = 'ErrorDeDades';
this.causa = causa;
}
}Ara el model, que ja només conté el que li correspon:
// js/model/tasca.js
import { AVUI, estaVencuda as dataVencuda } from '../util/dates.js';
import { PESOS, marcaEstat } from '../util/format.js';
import { ErrorDeValidacio } from './errors.js';
const TRANSICIONS = Object.freeze({ // R6
pendent: ['en-curs'],
'en-curs': ['pendent', 'feta'],
feta: []
});
export class Tasca {
#estat = 'pendent'; // R5
#hores;
constructor(dades) {
if (typeof dades.titol !== 'string' || dades.titol.trim() === '') {
throw new ErrorDeValidacio('El títol no pot estar buit.', 'titol', dades.titol); // R2
}
this.id = dades.id;
this.titol = dades.titol.trim();
this.responsable = dades.responsable || null; // R8
this.prioritat = dades.prioritat ?? 'mitjana';
this.etiquetes = [...new Set((dades.etiquetes ?? []).map((e) => e.trim().toLowerCase()))]; // R9
this.dataLimit = dades.dataLimit;
this.revisor = dades.revisor ?? null;
this.horesEstimades = dades.horesEstimades; // passa pel setter (R3)
if (dades.estat !== undefined) {
if (!Object.hasOwn(TRANSICIONS, dades.estat)) {
throw new ErrorDeValidacio(`Estat desconegut: "${dades.estat}".`, 'estat', dades.estat);
}
this.#estat = dades.estat;
}
}
get estat() { return this.#estat; }
get horesEstimades() { return this.#hores; }
get oberta() { return this.#estat !== 'feta'; }
get esforc() { return (PESOS[this.prioritat] ?? 0) * this.#hores; }
set horesEstimades(valor) {
if (typeof valor !== 'number' || !(valor > 0 && valor <= 40)) {
throw new ErrorDeValidacio('Les hores han de ser entre 1 i 40.', 'horesEstimades', valor); // R3
}
this.#hores = valor;
}
estaVencuda(avui = AVUI) {
return dataVencuda(this.dataLimit, this.#estat, avui);
}
canviarEstat(nou) {
if (!(TRANSICIONS[this.#estat] ?? []).includes(nou)) {
throw new ErrorDeValidacio(
`Transició no permesa: "${this.#estat}" → "${nou}".`, 'estat', nou); // R6
}
this.#estat = nou;
return this;
}
descripcio(avui = AVUI) {
const avis = this.estaVencuda(avui) ? ' ⚠ VENÇUDA' : '';
return `${marcaEstat(this.#estat)} [${this.id}] ${this.titol} · ${this.responsable ?? 'sense assignar'} · ${this.#hores} h${avis}`;
}
toJSON() {
return { id: this.id, titol: this.titol, responsable: this.responsable,
prioritat: this.prioritat, estat: this.#estat, etiquetes: [...this.etiquetes],
horesEstimades: this.#hores, dataLimit: this.dataLimit, revisor: this.revisor };
}
static desDeJSON(dades) {
return new Tasca(typeof dades === 'string' ? JSON.parse(dades) : dades);
}
}
export class TascaRecurrent extends Tasca {
constructor(dades) {
super(dades);
this.periodicitat = dades.periodicitat ?? 'setmanal';
}
descripcio(avui) {
return `${super.descripcio(avui)} · es repeteix ${this.periodicitat}`;
}
}Observa l'import { estaVencuda as dataVencuda }: la funció d'utilitat i el mètode de la classe es diuen igual, i as resol el xoc sense reanomenar cap dels dos. És exactament per a això que existeix.
// js/model/tauler.js
import { Tasca } from './tasca.js';
import { ErrorDeValidacio } from './errors.js';
export class Tauler {
#tasques = [];
constructor(nom, tasques = []) {
this.nom = nom;
for (const t of tasques) this.afegir(t);
}
get total() { return this.#tasques.length; }
get tasques() { return [...this.#tasques]; }
get obertes() { return this.#tasques.filter((t) => t.oberta); }
get horesTotals() { return this.#tasques.reduce((s, t) => s + t.horesEstimades, 0); }
get horesObertes() { return this.obertes.reduce((s, t) => s + t.horesEstimades, 0); }
get esforc() { return this.#tasques.reduce((s, t) => s + t.esforc, 0); }
cercarPerId(id) { return this.#tasques.find((t) => t.id === id) ?? null; }
filtrar(predicat) { return this.#tasques.filter(predicat); }
vencudes(avui) { return this.obertes.filter((t) => t.estaVencuda(avui)); }
horesPerResponsable() {
return this.obertes.reduce((acc, t) => {
const clau = t.responsable ?? 'sense assignar';
acc[clau] = (acc[clau] ?? 0) + t.horesEstimades;
return acc;
}, {});
}
afegir(tasca) {
if (!(tasca instanceof Tasca)) {
throw new ErrorDeValidacio("Només s'admeten instàncies de Tasca.", 'tasca', tasca);
}
if (this.cercarPerId(tasca.id)) {
throw new ErrorDeValidacio(`Id duplicat: ${tasca.id}.`, 'id', tasca.id); // R1
}
this.#tasques.push(tasca);
return this;
}
canviarEstat(id, nou) {
const tasca = this.cercarPerId(id);
if (tasca === null) throw new ErrorDeValidacio(`No existeix la tasca ${id}.`, 'id', id);
tasca.canviarEstat(nou);
return this;
}
resum(avui) {
return { total: this.total, obertes: this.obertes.length,
horesTotals: this.horesTotals, horesObertes: this.horesObertes,
vencudes: this.vencudes(avui).length, esforc: this.esforc };
}
}// js/dades/backlog.js
import { Tasca } from '../model/tasca.js';
/** Dades planes del backlog canònic de Taller Nómada. */
export const dadesBacklog = [
{ id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván', prioritat: 'alta', estat: 'en-curs', etiquetes: ['espai', 'disseny'], horesEstimades: 12, dataLimit: '2026-09-30', revisor: 'Marta' },
{ id: 2, titol: 'Cartelleria del taller de serigrafia', responsable: 'Marta', prioritat: 'mitjana', estat: 'pendent', etiquetes: ['serigrafia', 'comunicació'], horesEstimades: 6, dataLimit: '2026-10-15', revisor: null },
{ id: 3, titol: 'Actualitzar el web de reserves', responsable: 'Lucía', prioritat: 'alta', estat: 'pendent', etiquetes: ['web', 'reserves'], horesEstimades: 14, dataLimit: '2026-10-02', revisor: 'Iván' },
{ id: 4, titol: 'Inventari de tintes de serigrafia', responsable: 'Marta', prioritat: 'baixa', estat: 'feta', etiquetes: ['serigrafia', 'magatzem'], horesEstimades: 3, dataLimit: '2026-09-12', revisor: null },
{ id: 5, titol: 'Guia sobre enquadernació per a residents', responsable: 'Iván', prioritat: 'mitjana', estat: 'en-curs', etiquetes: ['enquadernació', 'documentació'], horesEstimades: 8, dataLimit: '2026-11-05', revisor: 'Lucía' },
{ id: 6, titol: 'Pressupost de la fusteria', responsable: 'Iván', prioritat: 'alta', estat: 'pendent', etiquetes: ['fusteria', 'compres'], horesEstimades: 5, dataLimit: '2026-09-05', revisor: 'Marta' }
];
/** Retorna instàncies noves cada vegada: ningú no comparteix estat per accident. */
export function crearBacklog() {
return dadesBacklog.map((dades) => new Tasca(dades));
}Aquell crearBacklog() com a funció, en lloc d'un array exportat ja construït, és una decisió deliberada. Com que els mòduls s'executen una sola vegada (apartat 7), un array exportat directament seria el mateix per a tota l'aplicació, i una prova que canviés l'estat d'una tasca contaminaria la següent. Retornant instàncies noves a cada crida, cada consumidor té les seves.
I per fi el punt d'entrada, que ja no conté lògica: només orquestra.
// js/app.js
import { Tauler } from './model/tauler.js';
import { crearBacklog } from './dades/backlog.js';
import { AVUI, dataLlegible } from './util/dates.js';
import { plural, alinear } from './util/format.js';
import { ErrorDeValidacio } from './model/errors.js';
const tauler = new Tauler('Taller Nómada', crearBacklog());
console.log(`— ${tauler.nom} · ${dataLlegible(AVUI)} —`);
for (const tasca of tauler.tasques) {
console.log(tasca.descripcio(AVUI));
}
const r = tauler.resum(AVUI);
console.log(`\n${plural(r.total, 'tasca', 'tasques')}, ${r.obertes} obertes`);
console.log(`Hores: ${r.horesObertes} obertes de ${r.horesTotals} totals`);
console.log(`Vençudes: ${r.vencudes} · Esforç ponderat: ${r.esforc}`);
console.log('\nCàrrega per responsable:');
for (const [persona, hores] of Object.entries(tauler.horesPerResponsable())) {
console.log(` ${persona.padEnd(8)} ${alinear(hores, 3)} h`);
}
try {
tauler.canviarEstat(6, 'feta'); // pendent → feta: prohibit (R6)
} catch (error) {
if (error instanceof ErrorDeValidacio) console.error(`\n⚠ ${error.descriure()}`);
else throw error;
}Executant-lo amb node js/app.js (o obrint index.html des del servidor local i mirant la consola):
— Taller Nómada · 20 de setembre de 2026 — ▸ [1] Redissenyar la sala polivalent · Iván · 12 h ○ [2] Cartelleria del taller de serigrafia · Marta · 6 h ○ [3] Actualitzar el web de reserves · Lucía · 14 h ✓ [4] Inventari de tintes de serigrafia · Marta · 3 h ▸ [5] Guia sobre enquadernació per a residents · Iván · 8 h ○ [6] Pressupost de la fusteria · Iván · 5 h ⚠ VENÇUDA 6 tasques, 5 obertes Hores: 45 obertes de 48 totals Vençudes: 1 · Esforç ponderat: 124 Càrrega per responsable: Iván 25 h Marta 6 h Lucía 14 h ⚠ [ErrorDeValidacio] estat: Transició no permesa: "pendent" → "feta".
Els números canònics de sempre, ara produïts per sis fitxers amb fronteres clares. I el més important: si demà cal canviar com es calcula l'esforç, saps exactament quin fitxer obrir.
Errors Habituals i Consells
- Oblidar l'extensió
.jsa l'especificador.from './util/dates'dona un 404 al navegador i unERR_MODULE_NOT_FOUNDa Node. L'extensió és obligatòria. - Obrir l'HTML amb doble clic. Els mòduls no funcionen sobre
file://per CORS. Servidor local sempre. - Confondre
import { X }amb desestructuració. No admet valors per defecte, ni reanomenat amb:, ni patrons imbricats; per reanomenar es fa serviras. - Nom mal escrit en una importació amb nom. Dona
SyntaxError: The requested module does not provide an export named 'Taule'. És un bon error: apareix en carregar, no a mitja execució. - Barrejar
export defaulti amb nom sense criteri. Tria una convenció per projecte i respecta-la. A Nómada Tasques: sempre amb nom. - Posar un
importdins d'unifo d'una funció. ÉsSyntaxError; per a això hi haimport()dinàmic. - Estat mutable a nivell de mòdul. Com que el mòdul s'executa una vegada, un
export const cache = new Map()és global per a tota l'aplicació. De vegades és justament el que vols; altres, un mal de cap a les proves del Mòdul 8. Exporta funcions fàbrica quan cada consumidor hagi de tenir la seva. - Dependències circulars. Si veus
Cannot access 'X' before initialization, busca el cicle. La solució gairebé mai no és un truc: és redissenyar perquè les dependències apuntin en una sola direcció. - Barrils en projectes grans. Un
index.jsque reexporta cinquanta fitxers els carrega tots encara que en facis servir un. Còmode, però amb cost (09-05). - Consell: ordena els
importde cada fitxer per capes —primer llibreries externes, després mòduls del projecte, després els més propers—. És la convenció que apliquen les eines de 08-02 i fa que el fitxer es llegeixi com una declaració d'intencions.
Exercicis
Exercici 1 — Detectar el cicle. Aquest projecte no arrenca. Dibuixa el graf, identifica el cicle i proposa una reorganització amb un mòdul nou.
// a/informe.js
import { Tauler } from '../b/tauler.js';
export function generarInforme(tasques) { return new Tauler('temp', tasques).resum(); }
// b/tauler.js
import { formatarInforme } from '../a/informe.js';
export class Tauler {
imprimir() { return formatarInforme(this.resum()); }
}Exercici 2 — Un mòdul util/estadistiques.js. Crea un mòdul que exporti mitjana(nombres), maxim(objectes, camp) i agruparPer(objectes, camp), més una constant VERSIO. Cap de les tres no pot importar res del model (han de servir per a qualsevol array d'objectes). Després escriu un app.js que l'importi i calculi, sobre el backlog canònic: la mitjana d'hores de les tasques obertes, la tasca de més hores i l'agrupament per prioritat.
Exercici 3 — Comptador compartit. Crea dades/metriques.js que exporti registrar(esdeveniment) i informe(), guardant el recompte en estat de mòdul. Importa'l des de model/tasca.js i des de model/tauler.js, registra un esdeveniment a cada canvi d'estat i demostra amb una traça de consola que els dos mòduls comparteixen el mateix comptador. Després explica en dues línies quan això seria un problema.
Solucions
Exercici 1
El cicle és directe: informe.js → tauler.js → informe.js.
flowchart LR
I["a/informe.js"] -->|"import Tauler"| T["b/tauler.js"]
T -->|"import formatarInforme"| I
La causa de fons és una inversió de capes: Tauler és una peça del model i no hauria de saber res de com es formaten els informes. La solució no és un truc d'importació, sinó ordenar les responsabilitats:
// util/formatInforme.js — capa baixa: no importa res
export function formatarInforme(resum) {
return `${resum.total} tasques · ${resum.horesObertes} h obertes · esforç ${resum.esforc}`;
}
// model/tauler.js — capa mitjana: només depèn d'utilitats
export class Tauler {
resum() { /* … */ }
// imprimir() desapareix: el tauler calcula, no presenta
}
// informes/informe.js — capa alta: coneix les dues de baix
import { Tauler } from '../model/tauler.js';
import { formatarInforme } from '../util/formatInforme.js';
export function generarInforme(tasques) {
return formatarInforme(new Tauler('temp', tasques).resum());
}Graf resultant, sense cicles i amb totes les fletxes en la mateixa direcció:
flowchart TD
INF["informes/informe.js"] --> TAU["model/tauler.js"]
INF --> FMT["util/formatInforme.js"]
TAU --> FMT
La lliçó general: quan apareix un cicle, gairebé sempre hi ha una capa que està mirant cap amunt. Aquí era Tauler.imprimir(), un mètode de presentació en una classe de model.
Exercici 2
// js/util/estadistiques.js
export const VERSIO = '1.0.0';
/** Mitjana aritmètica d'un array de nombres. Retorna 0 si és buit. */
export function mitjana(nombres) {
if (nombres.length === 0) return 0;
return nombres.reduce((s, n) => s + n, 0) / nombres.length;
}
/** Objecte amb el valor màxim al camp indicat, o null si l'array és buit. */
export function maxim(objectes, camp) {
return objectes.reduce((millor, actual) =>
(millor === null || actual[camp] > millor[camp]) ? actual : millor, null);
}
/** Agrupa pel valor d'un camp: { alta: [...], mitjana: [...] } */
export function agruparPer(objectes, camp) {
return objectes.reduce((acc, obj) => {
const clau = obj[camp] ?? 'sense valor';
(acc[clau] ??= []).push(obj);
return acc;
}, {});
}// js/app.js
import { crearBacklog } from './dades/backlog.js';
import { mitjana, maxim, agruparPer, VERSIO } from './util/estadistiques.js';
const backlog = crearBacklog();
const obertes = backlog.filter((t) => t.oberta);
console.log(`estadistiques v${VERSIO}`);
console.log(mitjana(obertes.map((t) => t.horesEstimades)).toFixed(1)); // '9.0' (45 / 5)
console.log(maxim(backlog, 'horesEstimades').titol); // 'Actualitzar el web de reserves'
const perPrioritat = agruparPer(backlog, 'prioritat');
for (const [prioritat, tasques] of Object.entries(perPrioritat)) {
console.log(`${prioritat}: ${tasques.length}`);
}
// alta: 3 · mitjana: 2 · baixa: 1La restricció de l'enunciat —que no importi res del model— és el punt de l'exercici. mitjana, maxim i agruparPer funcionen igual amb tasques, amb residents o amb reserves de sala, i per això viuen a util/ i no depenen de ningú: són una fulla del graf. Un mòdul d'utilitats que importa del model deixa de ser reutilitzable i és la llavor del pròxim cicle. Fixa't a més que maxim retorna el primer màxim en cas d'empat, perquè la comparació és estricta (>).
Exercici 3
// js/dades/metriques.js
const compte = new Map(); // estat de mòdul: un de sol per a tota l'aplicació
export function registrar(esdeveniment) {
compte.set(esdeveniment, (compte.get(esdeveniment) ?? 0) + 1);
}
export function informe() {
return Object.fromEntries([...compte.entries()].sort((a, b) => b[1] - a[1]));
}
export function reiniciar() { // imprescindible per a les proves del Mòdul 8
compte.clear();
}// js/model/tasca.js (afegit)
import { registrar } from '../dades/metriques.js';
canviarEstat(nou) {
// …validació R6…
this.#estat = nou;
registrar(`tasca:${nou}`);
return this;
}// js/model/tauler.js (afegit)
import { registrar } from '../dades/metriques.js';
canviarEstat(id, nou) {
// …
tasca.canviarEstat(nou);
registrar('tauler:canviEstat');
return this;
}// js/app.js
import { informe } from './dades/metriques.js';
tauler.canviarEstat(2, 'en-curs');
tauler.canviarEstat(3, 'en-curs');
tauler.canviarEstat(1, 'feta');
console.log(informe());
// { 'tauler:canviEstat': 3, 'tasca:en-curs': 2, 'tasca:feta': 1 }Que l'informe sumi les crides fetes des de dos mòduls diferents demostra que tots dos van rebre la mateixa instància de metriques.js: el mòdul es va carregar i executar una sola vegada, i aquell Map és únic.
Quan és un problema? Quan l'estat compartit hauria de ser independent. A les proves del Mòdul 8, cada test heretaria els recomptes de l'anterior i fallaria de manera intermitent —d'aquí que hàgim exportat reiniciar()—. I si l'aplicació mostrés dos taulers alhora, les mètriques de tots dos es barrejarien sense poder separar-les. L'alternativa és la mateixa que apliquem a crearBacklog(): exportar una fàbrica (crearMetriques()) perquè cada consumidor tingui la seva instància, i reservar l'estat de mòdul per al que de debò és global, com la configuració de l'aplicació.
Conclusió
El projecte ha deixat de ser un fitxer per convertir-se en una arquitectura. Els tres problemes del principi estan resolts per construcció: cada mòdul té el seu propi àmbit, així que dos fitxers poden declarar PESOS sense assabentar-se'n; l'ordre de càrrega el dedueix el motor a partir dels import, no l'HTML; i les dependències són explícites, escrites a les primeres línies de cada fitxer, fins al punt que es pot dibuixar el graf i comprovar d'un cop d'ull que totes les fletxes apunten cap avall.
Domines la sintaxi completa. export davant d'una declaració o agrupat en una llista al final; import { a, b } amb noms que han de coincidir —no és desestructuració—; as per reanomenar en qualsevol dels dos extrems, com vas fer amb estaVencuda as dataVencuda per resoldre un xoc real; import * as ns per portar l'espai de noms complet; export default per al cas de «aquest fitxer és una sola cosa», amb els seus pros i contres davant de les nominals; i la reexportació en barrils, còmoda però amb un cost de càrrega que convé conèixer. Saps que un mòdul s'executa una sola vegada i funciona com un singleton, que les importacions són enllaços vius de només lectura, i que per això crearBacklog() és una funció i no un array exportat.
Pel que fa a l'entorn tens el quadre complet: <script type="module"> amb el seu àmbit propi, el seu mode estricte permanent, el seu defer implícit —que t'anirà molt bé al Mòdul 6— i la seva exigència de servidor local per CORS; import() dinàmic i await a nivell de mòdul com a sintaxi que reprendràs a 05-06 i 09-05; CommonJS amb require/module.exports per reconèixer-lo quan aparegui, amb la seva taula de diferències davant d'ESM —resolució estàtica davant de dinàmica, enllaços vius davant de còpies, navegador sí o no—; i el "type": "module" del package.json que permet executar el projecte amb node js/app.js. I saps diagnosticar una dependència circular: no com un problema d'importació que s'arregla amb un truc, sinó com el símptoma d'una capa que està mirant cap amunt.
Nómada Tasques viu ara a model/tasca.js, model/tauler.js, model/errors.js, dades/backlog.js, util/dates.js, util/format.js i js/app.js, i continua donant els mateixos números de sempre: 48 h totals, 45 obertes, 1 vençuda, esforç 124, l'Iván amb 25 h, la Lucía amb 14 i la Marta amb 6. Amb una diferència important: aquell dades/backlog.js és avui un array escrit a mà, i tothom sap que a l'aplicació real les dades vindran d'algun lloc —un fitxer, un servidor— i trigaran a arribar. Un llenguatge d'un sol fil no es pot quedar esperant amb els braços plegats mentre això passa, perquè mentre espera la interfície sencera es congela. Com es programa una espera sense bloquejar res és l'altre gran salt del curs, i comença a JavaScript Asíncron: Callbacks.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
