İçeriğe geç
academia.sh

Ders 10 / 14

Paketleyici Kavramı

Modül çizgesinden tek çıktı üretme gerekçesi, çalışan bir paketleyicinin üç adımı, çıktının yapısı ve ağaç sarsmanın yan etkilerle sınırlanması.

İçindekiler

Modül sistemleri ve paket yönetimi konuları, kodun kaynak dizininde nasıl düzenlendiğini ve bağımlılıkların nasıl bulunduğunu kurdu. Kaynak düzeni, hedef ortamın istediği düzenle aynı olmak zorunda değildir.

Paketleyici (bundler), modül çizgesini girdi alıp hedef ortamda çalışan bir veya birkaç dosya üreten araç sınıfıdır. Bu ders, aracın ne yaptığını anlatmak yerine küçük bir gerçekleştirimini kurar; ondan sonra gerçek araçların bu iskelete eklediği şeyler anlaşılabilir hâle gelir.

Neden Tek Çıktı

Kaynak düzeni yüzlerce küçük dosyadan oluşur. Bu düzenin hedef ortamda doğrudan kullanılması üç sorun çıkarır.

Dosya başına maliyet. Ağ üzerinden yüklenen her dosya ayrı bir istektir. İsteklerin gecikmesi, taşınan baytların süresini aşabilir. İnternet Nasıl Çalışır kursunda tanıtılan istek başına gecikme, çok sayıda küçük dosyada baskın maliyet hâline gelir.

Çözümleme farkı. Çıplak belirteçler dosya sistemindeki bir arama kuralıyla çözülür. Bu kural her ortamda bulunmaz; tarayıcı, bicim-yardimcisi adını kendiliğinden bir dosyaya çeviremez. Paketleyici bu adları çözülmüş hâlleriyle değiştirir.

Dil ve biçim farkı. Kaynak, hedef ortamın anlamadığı bir sözdizim veya iki farklı modül sistemi karışımı içerebilir. Çıktı tek bir biçimde üretilir.

Bunların hiçbiri her projede geçerli değildir. Yalnızca çalışma zamanında çalışan, ağ üzerinden dağıtılmayan bir kütüphane paketlenmeden kullanılabilir. Paketleme bir zorunluluk değil, belirli sorunların çözümüdür.

Üç Adım

Bir paketleyicinin çekirdeği üç adımdır: giriş noktasından başlayarak çizgeyi çıkarmak, her modülü bir fonksiyona sarmak, ve modülleri birbirine bağlayan küçük bir çalışma zamanıyla birlikte yazmak.

Ölçüm kütüphanesinin çağrı tabanlı modüllerle yazılmış hâli girdi olsun:

// dosya: kaynak/bolumleme.js
const SOZCUK_AYIRACI = /[^\p{L}\p{N}]+/u;

exports.sozcuklereBol = function (metin) {
  return metin.split(SOZCUK_AYIRACI).filter((s) => s.length > 0);
};
// dosya: kaynak/istatistik.js
exports.ortalamaUzunluk = function (sozcukler) {
  if (sozcukler.length === 0) return 0;
  return sozcukler.reduce((t, s) => t + s.length, 0) / sozcukler.length;
};

exports.enUzunSozcuk = function (sozcukler) {
  return sozcukler.reduce((en, s) => (s.length > en.length ? s : en), '');
};
// dosya: kaynak/index.js
const { sozcuklereBol } = require('./bolumleme.js');
const { enUzunSozcuk, ortalamaUzunluk } = require('./istatistik.js');

exports.olc = function (metin) {
  const sozcukler = sozcuklereBol(metin);
  return {
    sozcukSayisi: sozcukler.length,
    ortalamaUzunluk: ortalamaUzunluk(sozcukler),
    enUzun: enUzunSozcuk(sozcukler),
  };
};
// dosya: kaynak/giris.js
const { olc } = require('./index.js');

console.log(olc('Modul kendi kapsamini tasir. Betik genel kapsamda calisir.'));

Paketleyici, bu dört dosyayı tek bir çıktıya çevirir:

// dosya: paketle.mjs
import { readFile, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';

const ISTEK = /require\(['"]([^'"]+)['"]\)/g;

async function grafCikar(giris) {
  const moduller = new Map();
  const kuyruk = [giris];
  while (kuyruk.length > 0) {
    const yol = kuyruk.shift();
    if (moduller.has(yol)) continue;
    const kaynak = await readFile(yol, 'utf8');
    const harita = {};
    for (const [, istek] of kaynak.matchAll(ISTEK)) {
      const cozulen = join(dirname(yol), istek);
      harita[istek] = cozulen;
      kuyruk.push(cozulen);
    }
    moduller.set(yol, { kaynak, harita });
  }
  return moduller;
}

function uret(moduller, giris) {
  const kayit = [...moduller]
    .map(([yol, m]) => `${JSON.stringify(yol)}: [function (require, module, exports) {\n${m.kaynak}}, ${JSON.stringify(m.harita)}]`)
    .join(',\n');
  return `(function (kayit, giris) {
  const onbellek = {};
  function calistir(yol) {
    if (onbellek[yol]) return onbellek[yol].exports;
    const [fabrika, harita] = kayit[yol];
    const modul = { exports: {} };
    onbellek[yol] = modul;
    fabrika((istek) => calistir(harita[istek]), modul, modul.exports);
    return modul.exports;
  }
  calistir(giris);
})({\n${kayit}\n}, ${JSON.stringify(giris)});
`;
}

const giris = 'kaynak/giris.js';
const moduller = await grafCikar(giris);
const cikti = uret(moduller, giris);
await writeFile('paket.js', cikti);
console.log('modül sayısı:', moduller.size);
console.log('çizge:', [...moduller.keys()].join(', '));
console.log('çıktı boyutu:', cikti.length, 'bayt');
$ node paketle.mjs
modül sayısı: 4
çizge: kaynak/giris.js, kaynak/index.js, kaynak/bolumleme.js, kaynak/istatistik.js
çıktı boyutu: 1781 bayt
$ node paket.js
{ sozcukSayisi: 8, ortalamaUzunluk: 6.125, enUzun: 'kapsamini' }

Dört dosya tek dosyaya indi ve çıktı, kaynağın verdiği sonucun aynısını üretti.

Çıktının Yapısı

Üretilen dosyanın başı, çalışma zamanının tamamını gösterir:

(function (kayit, giris) {
  const onbellek = {};
  function calistir(yol) {
    if (onbellek[yol]) return onbellek[yol].exports;
    const [fabrika, harita] = kayit[yol];
    const modul = { exports: {} };
    onbellek[yol] = modul;
    fabrika((istek) => calistir(harita[istek]), modul, modul.exports);
    return modul.exports;
  }
  calistir(giris);
})({
"kaynak/giris.js": [function (require, module, exports) {
// dosya: kaynak/giris.js

Yapı, modül sistemleri konusunda anlatılanların birebir karşılığıdır. Her modül bir fonksiyona sarılmıştır — modül sarmalayıcısının çıktıya yazılmış hâli. onbellek nesnesi tek değerlendirme güvencesini sağlar. harita nesnesi, kaynaktaki belirteçleri çıktıdaki anahtarlara çevirir; çözümleme derleme anında yapılmış, çalışma anına bırakılmamıştır.

Örneğin sınırları da açıktır ve gerçek araçlarla arasındaki farkı verir. Belirteçleri düzenli ifadeyle bulmak yeterli değildir: dizgi içinde geçen bir require yazımı da eşleşir. Gerçek araçlar kaynağı ayrıştırıp soyut sözdizim ağacı üzerinden çalışır — Bilgisayarlar Nasıl Çalışır kursundaki derleme aşamaları dersinde tanıtılan yapı budur. Ayrıca burada uzantı tamamlama, dizin girişleri ve çıplak ad araması yoktur; hepsi gerçek bir çözümleyicinin işidir.

Kapsam Birleştirme

Her modülün ayrı bir fonksiyona sarılması bir maliyet taşır: her modül için bir fonksiyon nesnesi, bir kapanış ve bir çağrı. Modül sayısı arttıkça bu maliyet ölçülebilir hâle gelir.

Kapsam birleştirme (scope hoisting), birbirine bağlı modülleri ayrı fonksiyonlara sarmak yerine tek bir kapsamda birleştiren dönüşümdür. Adlar çakışmaması için yeniden adlandırılır; sonuç, ara katman olmadan çalışan tek bir gövdedir.

Dönüşüm ancak modüller arası ilişkiler derleme anında biliniyorsa uygulanabilir. Çağrı tabanlı sistemde require çağrısı çalışma anında hesaplanabildiği için bu bilgi eksiktir; standart modül sözdiziminde ise tamdır. Statik yapının ikinci somut kazancı budur.

Ağaç Sarsmanın Sınırı

Kullanılmayan kodun çıktıdan düşürülmesi, örnek paketleyicide hiç yapılmıyor. Etkisi ölçülebilir: istatistik modülüne, hiçbir yerden çağrılmayan bir işlev eklendiğinde

// kaynak/istatistik.js dosyasının sonuna eklenir
// Hiçbir yerden çağrılmayan, ama modülde duran bir dışa aktarım.
exports.harfDagilimi = function (sozcukler) {
  const sayac = new Map();
  for (const sozcuk of sozcukler) {
    for (const harf of sozcuk.toLowerCase()) {
      sayac.set(harf, (sayac.get(harf) ?? 0) + 1);
    }
  }
  return [...sayac.entries()].sort((a, b) => b[1] - a[1]);
};

çıktı büyür, davranış değişmez:

$ node paketle.mjs
modül sayısı: 4
çizge: kaynak/giris.js, kaynak/index.js, kaynak/bolumleme.js, kaynak/istatistik.js
çıktı boyutu: 2127 bayt
$ node paket.js
{ sozcukSayisi: 8, ortalamaUzunluk: 6.125, enUzun: 'kapsamini' }

Üç yüz kırk altı bayt, hiç çalışmayan kod için ödendi. Gerçek araçlar bu işlevi düşürebilir — ama yalnızca üç koşul sağlandığında:

Kullanım görülebilmeli. Dışa aktarımın hangi adla ve nereden kullanıldığı statik olarak izlenebilmelidir. Ad hesaplanarak erişiliyorsa izlenemez.

Yan etki olmamalı. Modülün en üst düzeyinde, dışa aktarımlardan bağımsız bir iş yapılıyorsa — bir kayda yazma, bir yerleşik nesneyi değiştirme — modülün tümü düşürülemez. Araç, yan etkisi olmadığı bildirilmemiş modüllerde temkinli davranır; bu yüzden paketler bildiriminde yan etki bilgisini işaretler.

Erişim doğrudan olmalı. Ad alanı nesnesinin tamamı bir değere bağlanıp dolaştırılıyorsa hangi adların kullanıldığı bilinemez.

Bu koşullar, kütüphane yazarına somut bir sorumluluk yükler: en üst düzeyde iş yapmamak, adları doğrudan dışa aktarmak ve yan etki durumunu bildirimde doğru işaretlemek.

Dış Bırakılan Bağımlılıklar

Her bağımlılığın çıktıya katılması gerekmez. Çalışma zamanının kendi sağladığı modüller ya da hedef ortamda zaten bulunan kütüphaneler dış bağımlılık (external) olarak işaretlenir: paketleyici onları çizgeye almaz, çağrıyı olduğu gibi bırakır.

Ayrım kütüphane ve uygulama arasında da geçerlidir. Bir uygulamanın çıktısı, çalışması için gereken her şeyi taşımalıdır. Bir kütüphanenin çıktısı ise bağımlılıklarını içine gömmemelidir; gömerse tüketici aynı paketin iki kopyasını taşır ve bağımlılık ağacı dersinde anlatılan yinelenen sürüm sorunları ortaya çıkar.

Özet

  • Paketleyici, modül çizgesini girdi alıp hedef ortamda çalışan çıktı üretir; gerekçesi dosya başına ağ maliyeti, çözümleme farkı ve biçim farkıdır.
  • Çekirdek üç adımdır: çizgeyi çıkarmak, her modülü bir fonksiyona sarmak, modülleri bağlayan küçük bir çalışma zamanı yazmak.
  • Çıktıdaki önbellek tek değerlendirme güvencesini, belirteç haritası ise derleme anında yapılmış çözümlemeyi taşır.
  • Kapsam birleştirme, modül başına düşen sarmalayıcı maliyetini kaldırır ve yalnızca ilişkiler derleme anında bilindiğinde uygulanabilir.
  • Ağaç sarsma üç koşula bağlıdır: kullanımın izlenebilmesi, yan etkisizlik ve doğrudan erişim. Yan etki bilgisi bildirimde işaretlenir.

Sonraki Adım

Paketleyici modülleri birleştirdi, ama kaynağın yazıldığı sözdizimin hedef ortamda tanınacağını varsaydı. Bu varsayım her zaman doğru değildir: hedef, kaynağın kullandığı yazımı ayrıştıramayabilir veya çağırdığı yeteneği sağlamıyor olabilir. Sonraki ders, kaynağın hedefe uyarlanmasını ve bu uyarlamanın nerede durduğunu ele alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat