⌨️ Sviluppo web

Moduli ES in JavaScript: import, export e uso nel browser

Come funzionano i moduli ES: export con nome e predefiniti, import dinamico, script type module, import map e differenze con CommonJS, con esempi pratici.

Per molto tempo JavaScript non ha avuto un sistema di moduli nativo. Si includevano gli script in ordine con più tag <script>, sperando che le variabili globali non si scontrassero, oppure si usavano soluzioni esterne come CommonJS in Node.js. Oggi il linguaggio ha i suoi moduli ES (ECMAScript modules), supportati da tutti i browser moderni e da Node.js. Capirli bene aiuta a scrivere codice ordinato e a sfruttare meglio gli strumenti di build.

Che cos'è un modulo

Un modulo è un file con un proprio ambito: le variabili dichiarate al suo interno non diventano globali. Il modulo decide esplicitamente cosa esporre con export e gli altri file decidono cosa usare con import.

// matematica.js
export const PI_GRECO = 3.14159;

export function areaCerchio(raggio) {
  return PI_GRECO * raggio * raggio;
}

function aiutoInterno() {
  // non esportata: invisibile all'esterno
}
// main.js
import { areaCerchio } from './matematica.js';

console.log(areaCerchio(2));

Export con nome e export predefinito

Esistono due forme di esportazione.

Export con nome: un modulo può averne quanti vuole e chi importa usa gli stessi nomi, tra parentesi graffe. Si possono rinominare con as.

import { areaCerchio as area, PI_GRECO } from './matematica.js';

Export predefinito: al massimo uno per modulo, importato senza graffe e con il nome che preferisci.

// formattaData.js
export default function formattaData(data) {
  return data.toLocaleDateString('it-IT');
}

// altrove
import formatta from './formattaData.js';

Molti team preferiscono usare solo export con nome: i nomi restano coerenti in tutto il progetto, gli editor completano meglio gli import e la ricerca nel codice è più affidabile. È una convenzione, non una regola del linguaggio.

Puoi anche importare tutto come oggetto, con import * as matematica from './matematica.js', o riesportare da un file indice: export { areaCerchio } from './matematica.js'.

Caratteristiche importanti dei moduli

  • Modalità rigorosa automatica: ogni modulo è in strict mode, senza bisogno di dichiararlo.
  • Valutati una sola volta: se più file importano lo stesso modulo, il suo codice viene eseguito una volta e tutti condividono la stessa istanza. È utile per configurazioni e stati condivisi, ma va tenuto presente.
  • Collegamenti vivi: un import non è una copia del valore, ma un riferimento all'esportazione. Se il modulo di origine aggiorna una variabile esportata, chi la importa vede il nuovo valore. Non è invece possibile riassegnare un import dall'esterno.
  • Struttura statica: le istruzioni import ed export stanno al livello principale del file e non possono essere dentro condizioni. Proprio questa staticità permette agli strumenti di build di analizzare le dipendenze ed eliminare il codice non usato (tree shaking).

I moduli nel browser

Per caricare un modulo direttamente nel browser basta l'attributo type="module":

<script type="module" src="/js/main.js"></script>

Alcune differenze rispetto agli script classici:

  • i moduli sono differiti per impostazione predefinita: vengono eseguiti dopo l'analisi dell'HTML, come con defer;
  • i percorsi degli import devono essere URL completi o relativi, con estensione: ./utils.js, non utils;
  • i moduli vengono caricati rispettando le regole CORS, quindi aprire il file HTML direttamente dal disco (file://) di solito non funziona: serve un piccolo server locale;
  • this al livello principale è undefined, non l'oggetto globale.

Per i browser molto vecchi esisteva l'attributo nomodule, che carica uno script solo dove i moduli non sono supportati; oggi è raramente necessario.

Import map

Nel browser un import come import { format } from 'date-fns' non funziona da solo, perché il browser non sa dove si trovi date-fns. Le import map permettono di associare nomi brevi a URL:

<script type="importmap">
{
  "imports": {
    "utils/": "/js/utils/",
    "grafici": "/vendor/grafici.esm.js"
  }
}
</script>
<script type="module">
  import { disegna } from 'grafici';
  import { formatta } from 'utils/formatta.js';
</script>

Per progetti piccoli, import map e moduli nativi possono sostituire del tutto uno strumento di build.

Import dinamico

La funzione import() carica un modulo su richiesta e restituisce una promessa. È lo strumento per suddividere il codice e scaricare funzioni pesanti solo quando servono:

pulsanteEsporta.addEventListener('click', async () => {
  const { esportaPdf } = await import('./esportazione-pdf.js');
  esportaPdf(documento);
});

Chi non clicca mai il pulsante non scarica mai quel codice. Gli strumenti di build riconoscono import() e creano automaticamente un file separato.

Nei moduli è disponibile anche await al livello principale, utile per esempio per caricare una configurazione prima di proseguire. Va usato con moderazione, perché blocca l'esecuzione dei moduli che dipendono da quello corrente.

Moduli ES e CommonJS

In Node.js convivono due sistemi: CommonJS (require e module.exports) e i moduli ES. Le differenze principali:

Aspetto CommonJS Moduli ES
Sintassi require(), module.exports import, export
Caricamento sincrono asincrono
Analisi statica limitata completa
Nel browser no, senza strumenti sì, nativo

In Node.js un file viene trattato come modulo ES se ha estensione .mjs oppure se il package.json contiene "type": "module". Per nuovi progetti, i moduli ES sono la scelta da preferire: sono lo standard del linguaggio e funzionano in entrambi gli ambienti.

In sintesi

  • Un modulo ha un ambito proprio ed espone solo ciò che dichiara con export.
  • Gli export con nome rendono i nomi coerenti; quello predefinito è al massimo uno per file.
  • Nel browser si usano con type="module", sono differiti e richiedono percorsi con estensione.
  • Le import map associano nomi brevi a URL; import() carica codice solo quando serve.
  • Per nuovi progetti preferisci i moduli ES a CommonJS anche in Node.js.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche