Før ES-moduler hadde JavaScript ingen innebygd modulstruktur. Du hadde script-tagger i riktig rekkefølge, IIFE-mønstre for å simulere innkapsling, og deretter et tiår med CommonJS i Node.js. ES-moduler — standardisert i ES2015 og nå støttet overalt — er den ekte løsningen. Denne guiden dekker alt fra det grunnleggende om navngitte vs. standard-eksporter til dynamiske importer, tree-shaking og mønstrene som holder store kodebaser vedlikeholdbare.
Navngitte eksporter vs. standard-eksporter
Det finnes to måter å eksportere fra en modul. Å forstå forskjellen — og avveiningene — er grunnlaget for alt annet:
// 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';// 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// components/ProductCard.js
// Default export — one per file, no name required
export default function ProductCard({ product }) {
return `<div class="card">${product.name}</div>`;
}// Importing a default export — you choose the name
import ProductCard from './components/ProductCard.js';
import Card from './components/ProductCard.js'; // same thing, different nameBarrel-filer — nyttige og farlige
En barrel-fil er en index.js som re-eksporterer fra flere moduler og gir forbrukerne en renere importsti. De er overalt i store prosjekter:
// 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';// 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';Faren: barrel-filer kan ødelegge tree-shaking. Hvis du importerer én funksjon fra en barrel som re-eksporterer 40 moduler, trekker noen bundlere inn alle 40 — selv de du ikke bruker. Løsningen er å være selektiv om hva du legger i barrel, og sørge for at bundleren støtter sideEffects: false i package.json.
Dynamisk import() — kodeoppdelingpå forespørsel
Statiske importer (import x from '...') løses ved bundle-tid. Dynamisk import() er en funksjon som laster en modul dovent ved kjøring og returnerer et Promise. Det er slik du implementerer rutebasert kodeoppdeling:
// 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);// 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 og Vite forstår alle dynamisk import() og vil automatisk dele den lastede modulen inn i et separat chunk. Resultatet: mindre startbundle, raskere første innlasting.
import.meta — modulmetadata
import.meta er et objekt som gir deg metadata om den gjeldende modulen. Den mest nyttige egenskapen er import.meta.url — URL-en til den gjeldende modulfilen:
// 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');// 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-moduler vs. CommonJS
Du vil fortsatt støte på CommonJS (require()) i eldre Node.js-kodebaser og noen npm-pakker. Her er en rask referanse for forskjellene som skaper problemer:
// 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;Viktige forskjeller: ES-moduler er statisk analyserbare (importer løses før koden kjører), mens CommonJS require kjøres ved kjøretid. ESM bruker async-innlasting som standard; CJS er synkron. I Node.js behandles filer med filendelsen .mjs eller med "type": "module" i package.json som ESM.
import:ere en CommonJS-modul fra en ESM-fil (Node.js håndterer dette), men du kan ikke require():ere en ESM-modul fra en CommonJS-fil. Hvis du trenger det, bruk et dynamisk import()-kall i stedet — det fungerer i begge modulsystemene.Tree-shaking og navngitte eksporter
Tree-shaking er bundler-prosessen med å fjerne kode du importerer men aldri bruker. Det krever navngitte eksporter og statiske import-setninger — dynamiske importer og CommonJS er mye vanskeligere å tree-shake. Slik skriver du moduler som tree-shaker godt:
// ✅ 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';// ❌ 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 bundledFor biblioteker publisert til npm er tree-shaking-støtte forskjellen mellom et bibliotek folk anbefaler og ett de unngår. MDN Modules-guiden dekker de fullstendige statiske analysereglene som muliggjør det.
Native ES-moduler i nettleseren
Du kan bruke ES-moduler direkte i nettleseren — ingen bundler nødvendig. Legg til type="module" i script-taggen din:
<!-- index.html -->
<!-- type="module" enables ES Module semantics -->
<!-- deferred by default — runs after HTML is parsed -->
<script type="module" src="./app.js"></script>// 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();Modulskript er alltid utsatt og kjører i strict-modus som standard. Hver modul kjøres bare én gang, selv om den importeres av flere andre moduler — nettleseren cacher modulinstansen. Det er verdt å vite når du har delt tilstand i moduler.
Et ekte refaktoreringseksempel
Her er hvordan en typisk utils-fil vokser ut av kontroll og deles riktig inn i moduler:
// 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)Å dele opp etter ansvar betyr at hver fil har ett enkelt ansvar, tester er enklere å skrive og finne, og bundlere kan tree-shake på filnivå. Barrel-filen holder importer rene for forbrukere som foretrekker den enkle importstien.
Nyttige verktøy
Når du bygger modulær JavaScript, holder JS Formatter importblokkene dine rene og konsistente. For å forstå hva som er i den endelige bundlen, viser verktøy som Webpack Bundle Analyzer eller Rollups visualizer hvilke moduler som tar opp plass. TC39 ECMAScript Modules-spesifikasjonen har den nøyaktige løsningssemantikken hvis du trenger å forstå kanttilfeller.
Oppsummering
ES-moduler er grunnlaget for moderne JavaScript-arkitektur. Foretrekk navngitte eksporter for hjelpefunksjoner — de er eksplisitte, tree-shakeable og IDE-vennlige. Bruk dynamisk import() for å dele tung kode ut av startbundlen din. Vær forsiktig med barrel-filer i store kodebaser. Og hvis du fortsatt bruker CommonJS i et nytt Node.js-prosjekt, er det på tide å bytte — økosystemet har fullt ut skiftet til ESM.