ES Modülleri'nden önce, JavaScript'in yerleşik bir modül sistemi yoktu. Doğru sırada script etiketleriniz, kapsüllemeyi taklit etmek için IIFE kalıpları ve ardından on yıllık Node.js'te CommonJS vardı. ES2015'te standartlaştırılan ve artık her yerde desteklenen ES Modülleri gerçek çözümdür. Bu kılavuz, adlandırılmış ve varsayılan exportlar arasındaki temellerden dinamik importlara, tree-shaking'e ve büyük kod tabanlarını bakımlı tutan kalıplara kadar her şeyi kapsar.
Adlandırılmış Exportlar ve Varsayılan Exportlar
Bir modülden export etmenin iki yolu vardır. Aralarındaki farkı — ve değiş tokuşları — anlamak, geri kalan her şeyin temelidir:
// 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 Dosyaları — Faydalı ve Tehlikeli
Barrel dosyası, birden fazla modülden yeniden export yapan ve tüketicilere daha temiz bir import yolu sunan bir index.js'dir. Büyük projelerde her yerde bulunurlar:
// 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';Tehlike: barrel dosyaları tree-shaking'i mahvedebilir. 40 modülü yeniden export eden bir barrel'dan bir fonksiyon import ederseniz, bazı bundler'lar kullanmadıklarınız dahil hepsini çeker. Çözüm, barrel'a ne koyduğunuz konusunda seçici olmak ve bundler'ınızın package.json'da sideEffects: false'u desteklediğinden emin olmaktır.
Dinamik import() — İsteğe Bağlı Code Splitting
Statik importlar (import x from '...') derleme zamanında çözülür. Dinamik import(), çalışma zamanında bir modülü tembel olarak yükleyen ve Promise döndüren bir fonksiyondur. Route tabanlı code splitting böyle uygulanır:
// 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 ve Vite'ın tamamı dinamik import()'u anlar ve yüklenen modülü otomatik olarak ayrı bir parçaya böler. Sonuç: daha küçük başlangıç paketi, daha hızlı ilk yükleme.
import.meta — Modül Meta Verisi
import.meta, geçerli modül hakkında meta veri sağlayan bir nesnedir. En kullanışlı özellik, geçerli modül dosyasının URL'si olan import.meta.url'dir:
// 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 Modülleri ve CommonJS
Eski Node.js kod tabanlarında ve bazı npm paketlerinde hâlâ CommonJS (require()) ile karşılaşırsınız. İşte insanları şaşırtan farklılıklar için hızlı bir referans:
// 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;Temel farklar: ES Modülleri statik olarak analiz edilebilirdir (importlar kod çalışmadan önce çözülür), CommonJS require'ları ise çalışma zamanında yürütülür. ESM varsayılan olarak async yükleme kullanır; CJS eşzamanlıdır. Node.js'de .mjs uzantılı dosyalar veya package.json'da "type": "module" olanlar ESM olarak işlenir.
import edebilirsiniz (Node.js bunu yönetir), ancak bir CommonJS dosyasından ESM modülünü require() edemezsiniz. Bunu yapmanız gerekiyorsa, bunun yerine dinamik import() çağrısı kullanın — her iki modül sisteminde de çalışır.Tree-Shaking ve Adlandırılmış Exportlar
Tree-shaking, import ettiğiniz ama hiç kullanmadığınız kodu kaldıran bundler sürecidir. Adlandırılmış exportlar ve statik import ifadeleri gerektirir — dinamik importlar ve CommonJS'in tree-shake edilmesi çok daha zordur. İşte iyi tree-shake edilebilen modüllerin nasıl yazılacağı:
// ✅ 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 bundlednpm'e yayımlanan kütüphaneler için tree-shaking desteği, insanların önerdiği ile kaçındığı kütüphane arasındaki farkı belirler. MDN Modüller kılavuzu, bunu mümkün kılan tam statik analiz kurallarını kapsar.
Tarayıcıda Native ES Modülleri
ES Modüllerini doğrudan tarayıcıda kullanabilirsiniz — bundler gerekmez. Script etiketinize type="module" ekleyin:
<!-- 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();Modül scriptleri her zaman ertelenir ve varsayılan olarak strict mode'da çalışır. Her modül, birden fazla başka modül tarafından import edilse bile yalnızca bir kez yürütülür — tarayıcı modül örneğini önbelleğe alır. Modüllerde paylaşılan durumunuz olduğunda bunu bilmek önemlidir.
Gerçek Bir Yeniden Düzenleme Örneği
İşte tipik bir utils dosyasının nasıl kontrolden çıktığı ve modüllere doğru şekilde nasıl bölündüğü:
// 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)Sorumluluğa göre bölmek, her dosyanın tek bir sorumluluğu olması, testlerin yazılması ve bulunmasının daha kolay olması ve bundler'ların dosya düzeyinde tree-shaking yapabilmesi anlamına gelir. Barrel dosyası, tek import yolunu tercih eden tüketiciler için importları temiz tutar.
Kullanışlı Araçlar
Modüler JavaScript oluştururken, JS Formatter import bloklarınızı temiz ve tutarlı tutar. Son paketinizde ne olduğunu anlamak için Webpack Bundle Analyzer veya Rollup'ın görselleştiricisi gibi araçlar hangi modüllerin yer kapladığını gösterir. TC39 ECMAScript Modülleri spesifikasyonu, uç durumları anlamanız gerekiyorsa tam çözüm semantiğini içerir.
Sonuç
ES Modülleri, modern JavaScript mimarisinin temelidir. Yardımcı program fonksiyonları için adlandırılmış exportları tercih edin — bunlar açık, tree-shakeable ve IDE'ye dosttur. Ağır kodu başlangıç paketinizden ayırmak için dinamik import() kullanın. Büyük kod tabanlarında barrel dosyalarına dikkat edin. Ve yeni bir Node.js projesinde hâlâ CommonJS kullanıyorsanız, geçiş zamanı gelmiştir — ekosistem tamamen ESM'e geçmiştir.