Prima degli ES Module, JavaScript non aveva un sistema di moduli integrato. Avevi tag script nell'ordine giusto, pattern IIFE per simulare l'incapsulamento, e poi un decennio di CommonJS in Node.js. Gli ES Module — standardizzati in ES2015 e ora supportati ovunque — sono la soluzione vera. Questa guida copre tutto, dalle basi degli export nominati vs default alle importazioni dinamiche, il tree-shaking e i pattern che mantengono gestibili le grandi codebase.

Export nominati vs Export predefiniti

Ci sono due modi per esportare da un modulo. Capire la differenza — e i compromessi — è la base per tutto il resto:

js
// utils/currency.js

// Named exports — explicit names, multiple per file
export function formatPrice(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}

export function parseCents(cents) {
  return cents / 100;
}

export const DEFAULT_CURRENCY = 'USD';
js
// Importing named exports — names must match
import { formatPrice, parseCents } from './utils/currency.js';

// You can rename on import
import { formatPrice as fmt } from './utils/currency.js';

// Import the entire namespace as an object
import * as Currency from './utils/currency.js';
Currency.formatPrice(49.99); // works
js
// components/ProductCard.js

// Default export — one per file, no name required
export default function ProductCard({ product }) {
  return `<div class="card">${product.name}</div>`;
}
js
// Importing a default export — you choose the name
import ProductCard from './components/ProductCard.js';
import Card        from './components/ProductCard.js'; // same thing, different name
Opinione: Gli export nominati sono quasi sempre migliori per i moduli di utilità. Sono espliciti, amichevoli con gli IDE (il completamento automatico e "vai alla definizione" funzionano perfettamente) e sicuri per il refactoring — rinominare una funzione viene rilevato dagli strumenti, non dai test. Gli export predefiniti hanno senso per i componenti che sono "l'unica cosa" esportata da un file, come i componenti React/Angular o i file degli handler di route.

File barrel — Utili e pericolosi

Un file barrel è un index.js che ri-esporta da più moduli, offrendo ai consumatori un percorso di importazione più pulito. Si trovano ovunque nei grandi progetti:

js
// utils/index.js — the barrel
export { formatPrice, parseCents, DEFAULT_CURRENCY } from './currency.js';
export { validateEmail, validatePhone }              from './validation.js';
export { debounce, throttle }                        from './timing.js';
export { fetchWithRetry, buildQueryString }          from './http.js';
js
// Consumer — clean import path, no need to know the file structure
import { formatPrice, debounce, fetchWithRetry } from './utils/index.js';
// or with bundlers that resolve index.js automatically:
import { formatPrice, debounce, fetchWithRetry } from './utils';

Il pericolo: i file barrel possono distruggere il tree-shaking. Se importi una funzione da un barrel che ri-esporta 40 moduli, alcuni bundler caricano tutti e 40 — anche quelli che non usi. La soluzione è essere selettivi su cosa mettere nel barrel e assicurarsi che il bundler supporti sideEffects: false in package.json.

import() dinamico — Code splitting su richiesta

Le importazioni statiche (import x from '...') vengono risolte al momento del bundle. L'importazione dinamica import() è una funzione che carica un modulo pigramente a runtime, restituendo una Promise. Così si implementa il code splitting basato sulle route:

js
// Load a heavy chart library only when the user navigates to the analytics page
async function loadAnalyticsDashboard() {
  const { Chart }   = await import('chart.js');
  const { buildDashboardData } = await import('./analytics/data.js');

  const data   = await buildDashboardData();
  const canvas = document.getElementById('dashboard-chart');
  new Chart(canvas, { type: 'bar', data });
}

// Attach to a route change
router.on('/analytics', loadAnalyticsDashboard);
js
// Feature flags — only load a module if the feature is enabled
async function initFeature(featureName) {
  const flags = await getFeatureFlags();

  if (flags[featureName]) {
    const module = await import(`./features/${featureName}.js`);
    module.init();
  }
}

Webpack, Rollup e Vite comprendono tutti l'import() dinamico e divideranno automaticamente il modulo caricato in un chunk separato. Il risultato: bundle iniziale più piccolo, primo caricamento più veloce.

import.meta — Metadati del modulo

import.meta è un oggetto che fornisce metadati sul modulo corrente. La proprietà più utile è import.meta.url — l'URL del file del modulo corrente:

js
// Get the directory of the current module (Node.js)
import { fileURLToPath } from 'node:url';
import { dirname, join }  from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);

// Now you can do path resolution the same way you would with CommonJS
const configPath = join(__dirname, '../config/app.json');
js
// In Vite projects, import.meta.env gives you environment variables
const apiBase = import.meta.env.VITE_API_BASE_URL;
const isProd  = import.meta.env.PROD; // boolean

// import.meta.glob — Vite-specific, import multiple files at once
const modules = import.meta.glob('./pages/*.js');
for (const path in modules) {
  const module = await modules[path]();
  console.log(path, module);
}

ES Module vs CommonJS

Incontrerai ancora CommonJS (require()) nelle codebase Node.js più vecchie e in alcuni pacchetti npm. Ecco un riferimento rapido per le differenze che creano confusione:

js
// CommonJS (CJS) — still common in Node.js
const express = require('express');
const { join } = require('path');
module.exports = { myFunction };
module.exports.default = MyClass;

// ES Modules (ESM) — the standard
import express from 'express';
import { join } from 'path';
export { myFunction };
export default MyClass;

Differenze chiave: gli ES Module sono analizzabili staticamente (le importazioni vengono risolte prima dell'esecuzione del codice), mentre le require di CommonJS vengono eseguite a runtime. ESM usa il caricamento async per impostazione predefinita; CJS è sincrono. In Node.js, i file con estensione .mjs o "type": "module" in package.json sono trattati come ESM.

Attenzione all'interoperabilità: Puoi fare import di un modulo CommonJS da un file ESM (Node.js gestisce questo), ma non puoi fare require() di un modulo ESM da un file CommonJS. Se hai bisogno, usa invece una chiamata import() dinamica — funziona in entrambi i sistemi di moduli.

Tree-Shaking ed export nominati

Il tree-shaking è il processo del bundler per rimuovere il codice che importi ma non usi mai. Richiede export nominati e istruzioni di import statiche — le importazioni dinamiche e CommonJS sono molto più difficili da tree-shakeable. Ecco come scrivere moduli che si prestano bene al tree-shaking:

js
// ✅ Tree-shakeable — bundler can see exactly what's exported
export function formatDate(date) { /* ... */ }
export function parseDate(str)   { /* ... */ }
export function addDays(date, n) { /* ... */ }

// If you only import formatDate, parseDate and addDays are excluded from the bundle
import { formatDate } from './utils/dates.js';
js
// ❌ Harder to tree-shake — the whole object is one export
const dateUtils = {
  formatDate: (date) => { /* ... */ },
  parseDate:  (str)  => { /* ... */ },
  addDays:    (date, n) => { /* ... */ }
};
export default dateUtils;

// Even if you only use dateUtils.formatDate, the whole object may be bundled

Per le librerie pubblicate su npm, il supporto al tree-shaking è la differenza tra una libreria che le persone raccomandano e una che evitano. La guida MDN ai moduli copre le regole complete di analisi statica che lo abilitano.

ES Module nativi nel browser

Puoi usare gli ES Module direttamente nel browser — nessun bundler richiesto. Aggiungi type="module" al tag script:

js
<!-- index.html -->
<!-- type="module" enables ES Module semantics -->
<!-- deferred by default — runs after HTML is parsed -->
<script type="module" src="./app.js"></script>
js
// app.js — can use import/export directly
import { initRouter }    from './router.js';
import { setupAuth }     from './auth.js';
import { loadDashboard } from './dashboard.js';

async function main() {
  await setupAuth();
  initRouter();
  await loadDashboard();
}

main();

Gli script modulo sono sempre differiti e vengono eseguiti in strict mode per impostazione predefinita. Ogni modulo viene eseguito una sola volta, anche se importato da più altri moduli — il browser memorizza nella cache l'istanza del modulo. Vale la pena saperlo quando hai uno stato condiviso nei moduli.

Un esempio reale di refactoring

Ecco come un tipico file utils cresce fuori controllo e viene suddiviso correttamente in moduli:

js
// Before: one giant utils.js with everything
export function formatPrice(amount) { /* ... */ }
export function validateEmail(email) { /* ... */ }
export function debounce(fn, ms) { /* ... */ }
export function fetchWithRetry(url, opts) { /* ... */ }
export function parseCSV(text) { /* ... */ }
export function buildQueryString(params) { /* ... */ }

// After: split by concern
// utils/
//   currency.js     → formatPrice, parseCents
//   validation.js   → validateEmail, validatePhone
//   timing.js       → debounce, throttle
//   http.js         → fetchWithRetry, buildQueryString
//   csv.js          → parseCSV, formatCSV
//   index.js        → re-exports all of the above (optional barrel)

Dividere per preoccupazione significa che ogni file ha una singola responsabilità, i test sono più facili da scrivere e localizzare, e i bundler possono fare tree-shaking a livello di file. Il file barrel mantiene le importazioni pulite per i consumatori che preferiscono il singolo percorso di importazione.

Strumenti utili

Quando costruisci JavaScript modulare, JS Formatter mantiene i tuoi blocchi di import puliti e coerenti. Per capire cosa c'è nel tuo bundle finale, strumenti come Webpack Bundle Analyzer o il visualizzatore di Rollup mostrano quali moduli occupano spazio. La specifica TC39 ECMAScript Modules fornisce la semantica esatta di risoluzione se hai bisogno di capire i casi limite.

In conclusione

Gli ES Module sono la base dell'architettura JavaScript moderna. Preferisci gli export nominati per le funzioni di utilità — sono espliciti, tree-shakeable e amichevoli con gli IDE. Usa l'import() dinamico per dividere il codice pesante dal tuo bundle iniziale. Fai attenzione con i file barrel nelle grandi codebase. E se sei ancora su CommonJS in un nuovo progetto Node.js, è il momento di passare — l'ecosistema si è completamente spostato su ESM.