← Kursa Dön
📄 Text · 30 min

Modül Sistemi

Neden Bu Konu Önemli?

2010'ların başında bir JavaScript projesi hayal et: 15 farklı .js dosyası, hepsi <script> tag'leri ile sırayla yükleniyor. Her dosya global scope'a değişkenler ekliyor. Bir dosyadaki user değişkeni, başka dosyadaki user değişkeniyle çakışıyor. Hangi dosya hangi dosyaya bağımlı? Kimse bilmiyor. Dosya sırasını değiştirsen her şey çöküyor.

Bu kaos, modül sisteminin doğuş sebebidir.

Modüller, kodu bağımsız, kapsüllenmiş, yeniden kullanılabilir parçalara böler. Her modül kendi scope'una sahiptir — global scope'u kirletmez. Neyi dışarıya açacağını (export) ve neyi dışarıdan alacağını (import) açıkça belirtir. Tıpkı bir apartman dairesi gibi: herkes kendi evinde yaşar, kapıları kapalıdır, ama ihtiyaç duyulduğunda komşudan şeker isteyebilirsin.

Bu derste JavaScript'in iki modül sistemini (CommonJS ve ESM), import/export mekanizmalarını ve dynamic import'u öğreneceğiz.


Modül Olmadan Önceki Dünya

Modüllerin değerini anlamak için, modülsüz dünyanın sorunlarını görelim:

<!-- ❌ Modülsüz dünya — global scope kabusu -->
<script src="utils.js"></script>      <!-- formatDate, log fonksiyonları -->
<script src="validator.js"></script>   <!-- validate fonksiyonu (+ kendi log'u!) -->
<script src="user.js"></script>        <!-- User sınıfı, log kullanıyor -->
<script src="app.js"></script>         <!-- Ana uygulama -->

<!-- Sorunlar:
  1. utils.js'teki log, validator.js'teki log ile çakışıyor
  2. Sıra önemli — user.js, utils.js'ten önce yüklenirse hata
  3. Her şey window objesinde — isim çakışması kaçınılmaz
  4. Hangi dosya neye bağımlı? Belli değil
-->
// utils.js — global scope'a ekleniyor
var log = function (msg) { console.log("[UTILS]", msg); };
var formatDate = function (date) { /* ... */ };

// validator.js — utils'in log'unu eziyor!
var log = function (msg) { console.log("[VALIDATOR]", msg); };
// ↑ utils.js'teki log artık YOK — üzerine yazıldı!

// user.js — hangi log'u kullanacak?
log("User oluşturuldu"); // [VALIDATOR] User oluşturuldu
// utils.js'teki log'u kullanmak istiyorduk!

Bu sorunları çözmek için topluluk çeşitli pattern'ler geliştirdi (IIFE, revealing module pattern) ve sonunda iki standart modül sistemi ortaya çıktı.


CommonJS (CJS) — Node.js'in Modül Sistemi

CommonJS, Node.js ile birlikte gelen ilk yaygın modül sistemidir. require() ile import, module.exports ile export yapılır.

// --- math.js (CommonJS modülü) ---
function topla(a, b) {
  return a + b;
}

function carp(a, b) {
  return a * b;
}

// Dışarıya açma — iki yol:
// Yol 1: module.exports ile nesne
module.exports = { topla, carp };

// Yol 2: exports kısayolu (module.exports'un referansı)
// exports.topla = topla;
// exports.carp = carp;
// --- app.js (CommonJS import) ---
const { topla, carp } = require("./math");
// veya: const math = require("./math");

console.log(topla(2, 3));  // 5
console.log(carp(4, 5));   // 20

CommonJS Özellikleri

// 1. Senkron yükleme — require() çağrıldığı anda dosya okunur
const fs = require("fs"); // Dosya anında okunur ve parse edilir

// 2. Koşullu import — çalışma zamanında karar verilebilir
if (process.env.NODE_ENV === "production") {
  const logger = require("./prodLogger");
} else {
  const logger = require("./devLogger");
}

// 3. Cache — bir modül sadece İLK require'da çalışır
const a = require("./counter"); // Modül kodu çalışır
const b = require("./counter"); // Cache'ten gelir — tekrar çalışmaz
console.log(a === b); // true — aynı referans!

// 4. Döngüsel bağımlılık — kısmi destek
// a.js: require("./b") → b'nin o anki exports'u (belki eksik)
// b.js: require("./a") → a'nın o anki exports'u (belki eksik)

⚠️ Dikkat: exports ve module.exports aynı şey değildir. exports başlangıçta module.exports'a referanstır ama exports = { ... } yazarsan referansı koparırsın:

// ❌ Bu ÇALIŞMAZ — exports referansı koptu
exports = { topla, carp };
// module.exports hâlâ {} (boş nesne)

// ✅ Bu çalışır
module.exports = { topla, carp };
// veya
exports.topla = topla;
exports.carp = carp;

ES Modules (ESM) — Modern Standart

ES2015 ile JavaScript diline eklenen resmi modül sistemi. import ve export keyword'leri ile çalışır. Bugün hem tarayıcılarda hem Node.js'te desteklenir.

Named Export (İsimli Dışa Aktarma)

// --- utils.js ---

// Tek tek export
export function formatDate(date) {
  return date.toLocaleDateString("tr-TR");
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat("tr-TR", {
    style: "currency",
    currency: "TRY",
  }).format(amount);
}

export const VERSION = "1.0.0";

// veya toplu export
function helper1() { /* ... */ }
function helper2() { /* ... */ }
const CONFIG = { debug: false };

export { helper1, helper2, CONFIG };
// --- app.js ---

// Named import — süslü parantez gerekli
import { formatDate, formatCurrency, VERSION } from "./utils.js";

console.log(formatDate(new Date()));   // "01.03.2026"
console.log(formatCurrency(149.90));   // "₺149,90"
console.log(VERSION);                   // "1.0.0"

Default Export (Varsayılan Dışa Aktarma)

Her modülün en fazla bir default export'u olabilir. Default export, "bu modülün ana çıktısı budur" demektir.

// --- UserService.js ---

// Default export — modülün "ana" export'u
export default class UserService {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  
  async getUser(id) {
    const response = await fetch(`${this.baseUrl}/users/${id}`);
    return response.json();
  }
  
  async createUser(data) {
    const response = await fetch(`${this.baseUrl}/users`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return response.json();
  }
}

// Named export'lar da aynı dosyada olabilir
export const API_VERSION = "v2";
// --- app.js ---

// Default import — süslü parantez YOK, istediğin ismi ver
import KullaniciServisi from "./UserService.js";
import { API_VERSION } from "./UserService.js";

// veya tek satırda
import KullaniciServisi, { API_VERSION } from "./UserService.js";

const servis = new KullaniciServisi("https://api.example.com");

Named vs Default: Hangisini Seçmeli?

// Named export AVANTAJLARI:
// ✅ IDE otomatik tamamlama (auto-import) daha iyi çalışır
// ✅ İsim tutarlılığı — her yerde aynı isimle kullanılır
// ✅ Tree shaking daha verimli — sadece kullanılanlar dahil edilir
// ✅ Re-export kolaylığı

// Default export AVANTAJLARI:
// ✅ Import eden istediği ismi verebilir
// ✅ "Bu modülün tek bir amacı var" mesajı verir
// ✅ React component'leri için convention

// 🏆 Genel tercih: Named export
// Default export sadece modülün tek bir ana çıktısı varsa
// (class, component, factory function)

// İyi pratik:
export function topla(a, b) { return a + b; }      // ✅ Named
export function cikar(a, b) { return a - b; }       // ✅ Named
export default class Calculator { /* ... */ }        // ✅ Default (ana sınıf)

Yeniden İsimlendirme (Aliasing)

// İsim çakışması varsa yeniden isimlendir
import { formatDate as tarihFormatla } from "./utils.js";
import { formatDate as formatDateV2 } from "./utils-v2.js";

tarihFormatla(new Date());
formatDateV2(new Date());

// Export tarafında da yeniden isimlendirilebilir
const internalFunction = () => { /* ... */ };
export { internalFunction as publicFunction };

Tümünü Import Etme (Namespace Import)

// Tüm named export'ları bir namespace altında al
import * as MathUtils from "./math.js";

MathUtils.topla(2, 3);
MathUtils.carp(4, 5);
MathUtils.PI; // 3.14159

// ⚠️ Tree shaking çalışmaz — tüm modül dahil edilir
// Mümkünse sadece ihtiyacını import et

Re-Export (Yeniden Dışa Aktarma)

Büyük projelerde modülleri bir index dosyasında toplayıp, tek bir noktadan dışarıya açmak yaygın bir pattern'dir. Buna "barrel export" denir.

// --- services/userService.js ---
export class UserService { /* ... */ }
export function validateUser(user) { /* ... */ }

// --- services/orderService.js ---
export class OrderService { /* ... */ }
export function calculateTotal(items) { /* ... */ }

// --- services/authService.js ---
export class AuthService { /* ... */ }
export function hashPassword(pwd) { /* ... */ }
// --- services/index.js (barrel file) ---
// Tüm servisleri tek dosyadan re-export et
export { UserService, validateUser } from "./userService.js";
export { OrderService, calculateTotal } from "./orderService.js";
export { AuthService, hashPassword } from "./authService.js";

// Default export'u re-export etme
export { default as Logger } from "./logger.js";
// --- app.js ---
// Tek import satırı ile tüm servisler
import {
  UserService,
  OrderService,
  AuthService,
  validateUser,
} from "./services/index.js";

// veya kısaca (index.js otomatik bulunur)
import { UserService, OrderService } from "./services";

💡 İpucu: Barrel export'lar kodu organize eder ama tree shaking'i olumsuz etkileyebilir. Büyük kütüphanelerde, import edilen tek bir fonksiyon için tüm barrel dosyasının yüklenmesi performans sorununa yol açabilir. Modern bundler'lar bunu optimize etse de, dikkatli olun.


Dynamic Import — Tembel Yükleme

Statik import ifadeleri dosyanın en üstünde olmalıdır ve derleme zamanında çözülür. Ama bazen bir modülü koşullu veya sonradan yüklemek istersin. import() fonksiyonu bunu sağlar.

// Statik import — her zaman yüklenir
import { agirKutuphane } from "./agirKutuphane.js";

// Dynamic import — sadece gerektiğinde yüklenir
async function grafigeCiz() {
  // Kullanıcı "Grafik Göster" butonuna tıkladığında yükle
  const { Chart } = await import("./chartLibrary.js");
  
  const chart = new Chart("canvas", {
    type: "bar",
    data: veri,
  });
}

document.getElementById("grafikBtn").addEventListener("click", grafigeCiz);

Koşullu Import

// Platforma göre farklı modül yükle
async function veritabaniBaslat() {
  let db;
  
  if (typeof window !== "undefined") {
    // Tarayıcı — IndexedDB kullan
    const { IndexedDBAdapter } = await import("./adapters/indexeddb.js");
    db = new IndexedDBAdapter();
  } else {
    // Node.js — SQLite kullan
    const { SQLiteAdapter } = await import("./adapters/sqlite.js");
    db = new SQLiteAdapter();
  }
  
  await db.connect();
  return db;
}

Route-Based Code Splitting

// SPA'da sayfa bazlı kod bölme
const routes = {
  "/": () => import("./pages/Home.js"),
  "/about": () => import("./pages/About.js"),
  "/dashboard": () => import("./pages/Dashboard.js"),
  "/settings": () => import("./pages/Settings.js"),
};

async function sayfaYukle(yol) {
  const yukleyici = routes[yol];
  
  if (!yukleyici) {
    const { NotFound } = await import("./pages/NotFound.js");
    return new NotFound();
  }
  
  const modul = await yukleyici();
  const Sayfa = modul.default;
  return new Sayfa();
}

// Kullanıcı /dashboard'a gittiğinde, sadece Dashboard.js yüklenir
// Diğer sayfalar yüklenmez — ilk yükleme hızlı!

import() Detayları

// import() bir Promise döner
import("./modul.js")
  .then((modul) => {
    modul.default; // Default export
    modul.namedExport; // Named export
  })
  .catch((err) => {
    console.error("Modül yüklenemedi:", err);
  });

// async/await ile
async function yukle() {
  try {
    const { default: Sinif, yardimci } = await import("./modul.js");
    const nesne = new Sinif();
    yardimci();
  } catch (err) {
    console.error("Modül yüklenemedi:", err);
  }
}

CommonJS vs ESM Karşılaştırması

// ┌──────────────────┬────────────────────┬────────────────────┐
// │                  │     CommonJS       │     ES Modules     │
// ├──────────────────┼────────────────────┼────────────────────┤
// │ Import           │ require()          │ import ... from    │
// │ Export           │ module.exports     │ export / export    │
// │                  │ exports.x          │ default            │
// │ Yükleme          │ Senkron            │ Asenkron           │
// │ Değerlendirme    │ Çalışma zamanı     │ Derleme zamanı     │
// │ Koşullu import   │ ✅ if içinde       │ ❌ (dynamic için   │
// │                  │ require()          │ import() kullan)   │
// │ Top-level await  │ ❌                 │ ✅                 │
// │ this             │ module.exports     │ undefined          │
// │ Dosya uzantısı   │ .js, .cjs          │ .js, .mjs          │
// │ __dirname        │ ✅                 │ ❌ (import.meta    │
// │                  │                    │ .url kullan)       │
// │ Tree shaking     │ Zor                │ Kolay              │
// │ Tarayıcı desteği │ ❌ (bundler lazım) │ ✅ (native)        │
// └──────────────────┴────────────────────┴────────────────────┘

Node.js'te ESM Kullanma

// Yöntem 1: package.json'da type: "module"
// {
//   "name": "projem",
//   "type": "module"    ← Bu satır tüm .js dosyalarını ESM yapar
// }

// Yöntem 2: .mjs uzantısı kullan
// utils.mjs → ESM olarak yorumlanır
// utils.cjs → CommonJS olarak yorumlanır

// ESM'de __dirname yok — import.meta.url kullan
import { fileURLToPath } from "url";
import { dirname } from "path";

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

CJS'ten ESM'e Geçiş

// CommonJS (eski)
const express = require("express");
const { readFile } = require("fs/promises");
const path = require("path");
const myModule = require("./myModule");

module.exports = { app, start };

// ↓ ESM'e çevrilmiş hali ↓

// ES Modules (yeni)
import express from "express";
import { readFile } from "fs/promises";
import path from "path";
import myModule from "./myModule.js"; // .js uzantısı ZORUNLU (Node.js ESM'de)

export { app, start };

Tarayıcıda ESM Kullanımı

<!-- type="module" ile ESM desteği -->
<script type="module">
  import { formatDate } from "./utils.js";
  console.log(formatDate(new Date()));
</script>

<!-- Modül dosyası olarak -->
<script type="module" src="./app.js"></script>

<!-- ESM desteklemeyen tarayıcılar için fallback -->
<script nomodule src="./app-legacy.js"></script>
// Tarayıcı ESM özellikleri:
// 1. Strict mode otomatik — "use strict" gerekmez
// 2. Her modülün kendi scope'u var — global scope kirlenmiyor
// 3. defer davranışı varsayılan — DOM hazır olduktan sonra çalışır
// 4. CORS gerekli — farklı origin'den modül yüklemek için
// 5. Her modül sadece BİR KEZ çalışır (cache)

Import Maps

<!-- Import maps — bare specifier desteği (tarayıcıda) -->
<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
    "react": "https://esm.sh/react@18",
    "@utils/": "./src/utils/"
  }
}
</script>

<script type="module">
  // Artık bare specifier kullanabilirsin (dosya yolu yerine paket adı)
  import _ from "lodash";
  import React from "react";
  import { formatDate } from "@utils/date.js";
</script>

Yaygın Hatalar ve Tuzaklar

Hata 1: Uzantı Unutmak

// ❌ Node.js ESM'de uzantı ZORUNLU
import { topla } from "./math"; // ERR_MODULE_NOT_FOUND

// ✅ Uzantıyı yaz
import { topla } from "./math.js";

// Bundler'lar (Vite, Webpack) genelde uzantısız da çözümler
// Ama native ESM'de (Node.js, tarayıcı) uzantı zorunludur

Hata 2: require ve import Karıştırmak

// ❌ ESM dosyasında require kullanılamaz
import { topla } from "./math.js";
const fs = require("fs"); // ReferenceError: require is not defined

// ✅ Her ikisi de ESM olmalı
import { topla } from "./math.js";
import fs from "fs";

// CJS modülünü ESM'den import etmek:
import cjsModule from "./legacy.cjs"; // Default import olarak gelir

Hata 3: Döngüsel Bağımlılık

// ❌ Döngüsel bağımlılık — kaçınılması gereken durum
// a.js
import { b } from "./b.js";
export const a = "A + " + b;

// b.js
import { a } from "./a.js";
export const b = "B + " + a;

// Sonuç: a veya b undefined olabilir!
// ESM bunu CJS'ten daha iyi yönetir ama yine de sorunlu

// ✅ Çözüm: Ortak bağımlılığı ayrı modüle çıkar
// shared.js
export const shared = "paylaşılan";

// a.js
import { shared } from "./shared.js";
export const a = "A + " + shared;

// b.js
import { shared } from "./shared.js";
export const b = "B + " + shared;

Gerçek Dünya Örneği: Modüler E-Ticaret Yapısı

// --- models/Product.js ---
export class Product {
  constructor(id, name, price) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
  
  formattedPrice() {
    return `${this.price.toFixed(2)} TL`;
  }
}

// --- services/ProductService.js ---
import { Product } from "../models/Product.js";

export class ProductService {
  #baseUrl;
  
  constructor(baseUrl) {
    this.#baseUrl = baseUrl;
  }
  
  async getAll() {
    const response = await fetch(`${this.#baseUrl}/products`);
    const data = await response.json();
    return data.map((p) => new Product(p.id, p.name, p.price));
  }
  
  async getById(id) {
    const response = await fetch(`${this.#baseUrl}/products/${id}`);
    const data = await response.json();
    return new Product(data.id, data.name, data.price);
  }
}

// --- utils/format.js ---
export function formatCurrency(amount) {
  return new Intl.NumberFormat("tr-TR", {
    style: "currency",
    currency: "TRY",
  }).format(amount);
}

export function formatDate(date) {
  return new Intl.DateTimeFormat("tr-TR").format(date);
}

// --- services/index.js (barrel) ---
export { ProductService } from "./ProductService.js";
export { CartService } from "./CartService.js";
export { AuthService } from "./AuthService.js";

// --- app.js ---
import { ProductService } from "./services/index.js";
import { formatCurrency } from "./utils/format.js";

async function main() {
  const productService = new ProductService("https://api.example.com");
  
  const urunler = await productService.getAll();
  
  urunler.forEach((urun) => {
    console.log(`${urun.name}: ${formatCurrency(urun.price)}`);
  });
  
  // Ağır modülü sadece gerektiğinde yükle
  document.getElementById("raporBtn").addEventListener("click", async () => {
    const { ReportGenerator } = await import("./services/ReportGenerator.js");
    const rapor = new ReportGenerator(urunler);
    rapor.olustur();
  });
}

main();

Özet

Bu derste JavaScript'in modül sistemini derinlemesine öğrendik:

  • Modüller kodu kapsüllenmiş, bağımsız, yeniden kullanılabilir parçalara böler ve global scope kirliliğini önler.

  • CommonJS (require/module.exports) Node.js'in orijinal modül sistemidir — senkron, çalışma zamanında çözülür.

  • ES Modules (import/export) dilin resmi standartıdır — statik analiz, tree shaking ve tarayıcı desteği sağlar.

  • Named export IDE desteği ve tree shaking için daha iyi; default export modülün tek bir ana çıktısı için kullanılır.

  • Dynamic import (import()) modülleri çalışma zamanında, koşullu ve tembel (lazy) olarak yüklemeyi sağlar — kod bölme (code splitting) için kritiktir.

  • Barrel export (index.js) modülleri tek noktadan dışarıya açar ama tree shaking üzerindeki etkisine dikkat edilmelidir.

Bir sonraki derste Iterator ve Generator kavramlarını keşfedeceğiz.