Ders 03 / 14
CommonJS
Fonksiyon çağrısıyla yükleme modeli, modül sarmalayıcısı ve önbellek, değer kopyası ile canlı bağın farkı, dairesel bağımlılıkta iki sistemin davranışı ve birlikte çalışabilirlik.
İçindekiler
Önceki ders, içe aktarmanın bir deyim olduğu ve bağlamanın değerlendirmeden önce tamamlandığı bir modeli kurdu. Dilin bundan önce yaygınlaşmış ikinci bir modül sistemi vardır ve bu sistem hâlâ çalışan kodun büyük bölümünü taşır.
İkisinin bir arada bulunması bir geçiş kazası değil, kalıcı bir durumdur: bir projede iki sistemin dosyaları yan yana durur ve birbirini çağırır. Bu dersin sorusu, ikinci sistemin neyi farklı yaptığı ve iki modelin sınırının nerede olduğudur.
Yükleme Bir Fonksiyon Çağrısıdır
Bu sistemde bir modülü almak, require adlı bir fonksiyonu çağırmaktır; dışa aktarmak ise
module.exports adlı bir nesneye yazmaktır. İkisi de sözdizim değil, sıradan çalışma
zamanı işlemleridir.
Bu, kaynak metnin doğrudan çalıştırılmadığı anlamına gelir. Çalışma zamanı, dosyanın içeriğini bir fonksiyon gövdesine sarar ve o fonksiyonu beş argümanla çağırır:
// dosya: sarmalayici.cjs console.log(typeof require, typeof module, typeof exports); console.log(__filename.endsWith('sarmalayici.cjs'), typeof __dirname); console.log(module.exports === exports);
$ node sarmalayici.cjs function object object true string true
Bu beş ad dilin bir parçası değildir; modül sarmalayıcısı (module wrapper) tarafından sağlanır. Modül kapsamı, önceki derste ayrıştırma düzeyinde elde edilen yalıtımın burada sıradan bir fonksiyon kapsamıyla elde edilmiş hâlidir. Sonuç aynıdır: en üst düzey bildirimler genel kapsama sızmaz.
Son satır ayrıca bir tuzağı gösterir: exports başlangıçta module.exports ile aynı
nesnedir. exports.ad = ... yazmak ikisini de etkiler, ancak exports = ... yazmak
yalnızca yerel adı değiştirir ve dışa aktarım yüzeyine dokunmaz. Yüzeyi tümüyle
değiştirmek için module.exports atanmalıdır.
Çağrı tabanlı olmanın doğrudan sonucu, yüklemenin koşullu ve hesaplanmış olabilmesidir:
// dosya: bicim-duz.cjs module.exports = (olcum) => `sözcük: ${olcum.sozcukSayisi}`;
// dosya: secim.cjs — bicim-duz.cjs yukarıdaki dosyadır const kip = process.argv[2] ?? 'duz'; const bicimlendir = require(`./bicim-${kip}.cjs`); console.log(bicimlendir({ sozcukSayisi: 8, cumleSayisi: 2 }));
$ node secim.cjs sözcük: 8
Belirteç çalışma anında hesaplandığı için hangi dosyanın yükleneceği kaynağa bakılarak bilinemez. Esnekliğin bedeli budur: statik çözümleme yapan araçlar bu bağımlılığı göremez, ağaç sarsma uygulanamaz.
Önbellek ve Tek Değerlendirme
Aynı dosya birden çok kez istendiğinde gövdesi bir kez çalışır. Çalışma zamanı, çözülmüş dosya yolunu anahtar alan bir önbellek tutar:
// dosya: sayac.cjs console.log('sayac.cjs gövdesi çalıştı'); let sayi = 0; module.exports = { artir() { sayi += 1; return sayi; }, get deger() { return sayi; }, };
// dosya: onbellek.cjs — sayac.cjs yukarıdaki dosyadır const a = require('./sayac.cjs'); const b = require('./sayac.cjs'); console.log('aynı nesne mi:', a === b); a.artir(); console.log('b üzerinden okunan:', b.deger); console.log('önbellekte kayıtlı mı:', require.resolve('./sayac.cjs') in require.cache);
$ node onbellek.cjs sayac.cjs gövdesi çalıştı aynı nesne mi: true b üzerinden okunan: 1 önbellekte kayıtlı mı: true
Gövde bir kez çalıştı ve iki çağrı aynı nesneyi verdi. Önceki derste sayılan dört güvenceden dördüncüsü — tek değerlendirme — burada gözlenebilir bir tabloyla gerçekleştirilmiştir. Önbelleğin anahtarı çözülmüş mutlak yoldur; aynı dosyaya farklı yollardan ulaşılırsa, kayıt da farklı olur.
Kopya ve Canlı Bağ
module.exports sıradan bir nesnedir; içe aktarma da sıradan bir okuma. Bu, önceki dersin
canlı bağ davranışının burada bulunmadığı anlamına gelir:
// dosya: kayit.cjs let cagriSayisi = 0; function say() { cagriSayisi += 1; } module.exports = { cagriSayisi, say };
// dosya: kopya.cjs — kayit.cjs yukarıdaki dosyadır const { cagriSayisi, say } = require('./kayit.cjs'); const tumu = require('./kayit.cjs'); say(); say(); console.log('yıkımla alınan:', cagriSayisi); console.log('nesne üzerinden:', tumu.cagriSayisi);
$ node kopya.cjs yıkımla alınan: 0 nesne üzerinden: 0
İki okuma da sıfır verdi. module.exports nesnesi kurulurken cagriSayisi değişkeninin o
anki değeri nesneye kopyalandı; sonraki artırmalar yerel değişkeni değiştirdi ama nesneyi
değiştirmedi. Aynı senaryo standart modül sözdiziminde 2 vermişti.
Bu fark, iki sistemin arasında taşınan kodun sessiz hata kaynağıdır. Değişen bir sayaç ya
da bayrak dışa aktarılıyorsa, çağrı tabanlı sistemde okuma anında hesaplanan bir erişimci
kullanılmalıdır — sayac.cjs örneğindeki get deger tanımı bunun için vardı.
Dairesel Bağımlılık
İki modül birbirini içe aktardığında çizge döngü içerir. Hiçbir sistem bu döngüyü çözemez; yapabilecekleri, döngüyü belirli bir noktada kırmaktır. Kırma noktası iki sistemde farklıdır.
Çağrı tabanlı sistemde require, önbellekte kısmen dolmuş bir dışa aktarım nesnesi
bulur ve onu döndürür:
// dosya: a.cjs console.log('a: gövde başladı'); exports.ad = 'A'; const b = require('./b.cjs'); console.log('a: b.ad =', b.ad, '| b.tanim =', typeof b.tanim); exports.tanim = function () { return 'A tanımı'; }; console.log('a: gövde bitti');
// dosya: b.cjs console.log('b: gövde başladı'); const a = require('./a.cjs'); console.log('b: a.ad =', a.ad, '| a.tanim =', typeof a.tanim); exports.ad = 'B'; exports.tanim = function () { return 'B tanımı'; }; console.log('b: gövde bitti');
$ node a.cjs a: gövde başladı b: gövde başladı b: a.ad = A | a.tanim = undefined b: gövde bitti a: b.ad = B | b.tanim = function a: gövde bitti (node:4526) Warning: Accessing non-existent property 'tanim' of module exports inside circular dependency (Use `node --trace-warnings ...` to show where the warning was created)
b.cjs, a.cjs dosyasının yarısı çalışmış hâlini gördü: ad tanımlıydı, tanim
tanımsızdı. Program çöktürmedi, eksik bir değer verdi. Uyarı satırındaki süreç numarası her
çalıştırmada değişir.
Standart modül sözdiziminde bağlama değerlendirmeden önce tamamlandığı için adlar zaten vardır; sorun, bir adın değerinin henüz atanmamış olmasıdır. Fonksiyon bildirimleri gövde çalışmadan hazır olduğundan sorunsuz kullanılır:
// dosya: a.mjs import { bAdi, bTanim } from './b.mjs'; console.log('a: gövde başladı'); export const aAdi = 'A'; export function aTanim() { return 'A tanımı'; } console.log('a: b.bAdi =', bAdi, '| bTanim() =', bTanim());
// dosya: b.mjs import { aAdi, aTanim } from './a.mjs'; console.log('b: gövde başladı'); console.log('b: aTanim() =', aTanim()); export const bAdi = 'B'; export function bTanim() { return 'B tanımı'; }
$ node a.mjs b: gövde başladı b: aTanim() = A tanımı a: gövde başladı a: b.bAdi = B | bTanim() = B tanımı
b.mjs, henüz gövdesi çalışmamış olan a.mjs dosyasının fonksiyonunu çağırabildi. Aynı
düzenekte const ile tanımlı bir değere erken erişilirse sonuç sessiz bir tanımsız değil,
hatadır:
// dosya: a2.mjs import { bAdi } from './b2.mjs'; export const aAdi = 'A'; console.log('a2: bAdi =', bAdi);
// dosya: b2.mjs import { aAdi } from './a2.mjs'; console.log('b2: aAdi =', aAdi); export const bAdi = 'B';
$ node a2.mjs 2>&1 | grep '^ReferenceError' ReferenceError: Cannot access 'aAdi' before initialization
Ayrım özetlenebilir: biri eksik veriyi sessizce geçirir, diğeri erişimi durdurur. İkisinde de asıl çözüm aynıdır — dairesel bağımlılık bir tasarım kokusudur ve ortak parçanın üçüncü bir modüle çıkarılmasıyla giderilir.
Birlikte Çalışabilirlik
İki sistem aynı projede bulunduğunda hangi dosyanın hangi kurala göre okunacağı bir işaretle
belirlenir: .mjs uzantısı standart modülü, .cjs uzantısı çağrı tabanlı modülü zorlar;
uzantısı .js olan dosyalar için en yakın bildirim dosyasındaki tip alanına bakılır.
Standart modülden çağrı tabanlı modüle erişim doğrudandır:
// dosya: eski-bicim.cjs function bicimle(sayi) { return sayi.toFixed(2); } module.exports = { bicimle, surum: '1.0.0' };
// dosya: tuketici.mjs — eski-bicim.cjs yukarıdaki dosyadır import eski from './eski-bicim.cjs'; import { bicimle } from './eski-bicim.cjs'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const elle = require('./eski-bicim.cjs'); console.log('varsayılan dışa aktarım:', eski); console.log('adlandırılmış içe aktarım:', bicimle(3.14159)); console.log('createRequire ile:', elle === eski);
$ node tuketici.mjs
varsayılan dışa aktarım: { bicimle: [Function: bicimle], surum: '1.0.0' }
adlandırılmış içe aktarım: 3.14
createRequire ile: true
module.exports nesnesi varsayılan dışa aktarım olarak gelir. Adlandırılmış içe aktarımın
çalışması ise bir çıkarıma dayanır: çalışma zamanı kaynağı tarayarak hangi adların atandığını
tahmin eder. Atamalar bir döngü içinde veya hesaplanmış anahtarlarla yapılıyorsa bu tahmin
tutmaz ve yalnızca varsayılan dışa aktarım kullanılabilir.
Ters yön daha kısıtlıdır. require eşzamanlı bir çağrıdır: değeri hemen döndürmek
zorundadır. Standart modül çizgesi ise üst düzey await (top-level await) içerebilir,
yani değerlendirmesi eşzamansız olabilir. Böyle bir çizge eşzamanlı çağrıyla yüklenemez:
// dosya: bekleyen.mjs const gecikme = await new Promise((coz) => setTimeout(() => coz('hazır'), 10)); export const durum = gecikme;
// dosya: bekleyen-cagri.cjs — bekleyen.mjs yukarıdaki dosyadır const m = require('./bekleyen.mjs'); console.log(m.durum);
$ node bekleyen-cagri.cjs 2>&1 | grep '^Error' Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.
Sınır teknik bir zorunluluktur, bir eksiklik değil: eşzamanlı bir çağrı, tamamlanması zamana yayılan bir işi bekleyemez. Hata iletisinin işaret ettiği çözüm, sonraki dersin konusudur.
Özet
- Çağrı tabanlı sistemde içe aktarma bir fonksiyon çağrısı, dışa aktarma bir nesneye yazmadır; belirteç çalışma anında hesaplanabilir, bu yüzden statik çözümleme yapılamaz.
- Modül sarmalayıcısı, kapsam yalıtımını sıradan bir fonksiyon kapsamıyla sağlar ve
require,module,exportsadlarını dışarıdan verir. - Yüklenen modüller çözülmüş yola göre önbelleğe alınır; gövde bir kez çalışır ve tüm çağrılar aynı nesneyi alır.
- Dışa aktarılan ilkel değerler kopyadır; canlı bağ davranışı için erişimci tanımlanmalıdır.
- Dairesel bağımlılıkta çağrı tabanlı sistem kısmen dolmuş bir nesne verir, standart sistem erken erişimi hata ile durdurur.
- Uzantı ve tip alanı hangi kuralın uygulanacağını belirler; eşzamanlı çağrı, üst düzey
awaitiçeren bir çizgeyi yükleyemez.
Sonraki Adım
Eşzamanlı çağrının yapamadığını yapan bir biçim vardır: söz döndüren, çalışma anında çağrılabilen ve belirteci hesaplanabilen bir içe aktarma. Sonraki ders bu biçimi, kod bölme ve gecikmeli yükleme kalıplarını ve modül kaydının bu kalıplardaki rolünü ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.