İçeriğe geç
academia.sh

Ders 08 / 19

Modül Paketleme

Kaynak ağacın çalıştırılabilir çıktıya dönüşmesi; bağımlılık çizgesinin giriş noktalarından çıkarılması, değerlendirme sırası, kullanım imzasına göre çıktı parçası oluşturma ve parçanın içindeki çalışma zamanı.

İçindekiler

Strateji seçildi. Kuzey Yamaç Ölçüm İstasyonu sitesinin hangi sayfasının nerede üretileceği belli: tanıtım sayfası derleme zamanında, ölçüm listesi sunucuda, sorumlu paneli istemcide, yönlendirme kararı kenarda. Bu kararların ortak bir varsayımı vardı ve o varsayım henüz karşılanmadı. Her strateji, çalıştırılabilir bir çıktı ister: derleme zamanı üretim, sayfaları basacak bir programın hazır olmasını; sunucuda oluşturma, sunucuya yüklenecek bir modülü; istemcide oluşturma, tarayıcıya inecek bir betiği.

Kaynak ağaç bunların hiçbiri değildir. Kaynakta onlarca küçük dosya, aralarında içe aktarım ilişkileri ve hedef ortamın çözemeyeceği belirteçler vardır. Bu dersin sorusu şudur: kaynak dosyalar arasındaki bağımlılık ilişkisinden hangi çıktı parçaları, hangi ölçütle üretilir?

Kaynak Düzeni ile Çıktı Düzeni Aynı Şey Değildir

Modüller, Araçlar ve Ekosistem kursundaki paketleyici dersi bu ayrımı kurmuştu: kaynak düzeni okunabilirlik için, çıktı düzeni hedef ortamın kısıtları için tasarlanır. Orada tek girişli tek çıktı üretilmişti. Oluşturma stratejisi birden çok hedef seçtiği anda soru değişir. Aynı kaynak ağaçtan hem tarayıcıya inecek bir betik hem de sunucuda çalışacak bir modül üretiliyorsa, iki çıktının ortak modülleri vardır. Bu ortaklığın nasıl ele alındığı, hem çıktı boyutunu hem de önbellekleme davranışını belirler.

Modül paketleme (module bundling), giriş noktalarından erişilen modül kümesini bulup bu kümeyi hedefe uygun sayıda dosyaya bölme işidir. İki ayrı karar içerir: çizgede kimin kime bağlı olduğunun çıkarılması ve bu çizgenin parçalara ayrılması.

Uygulamanın Kaynak Ağacı

Kurs boyunca aynı kaynak ağaç kullanılacak. Aşağıdaki blok onu üretir.

mkdir -p kaynak

cat > kaynak/birim.js <<'EOF'
export const SICAKLIK_ALT = -60;
export const SICAKLIK_UST = 60;
export const sinirdaMi = (c) => c >= SICAKLIK_ALT && c <= SICAKLIK_UST;
export const fahrenhayt = (c) => c * 9 / 5 + 32;
EOF

cat > kaynak/tarih.js <<'EOF'
const iki = (s) => String(s).padStart(2, "0");
export const saatDakika = (d) => iki(new Date(d).getUTCHours()) + ":" + iki(new Date(d).getUTCMinutes());
export const gunAnahtari = (d) => new Date(d).toISOString().slice(0, 10);
EOF

cat > kaynak/bicim.js <<'EOF'
import { saatDakika } from "./tarih.js";
import { sinirdaMi, fahrenhayt } from "./birim.js";

export function olcumSatiri(o) {
  const uyari = sinirdaMi(o.sicaklik) ? "" : " (sinir disi)";
  return saatDakika(o.damga) + "  " + o.sicaklik.toFixed(1) + " C  %" + o.nem + uyari;
}
export const ikiliBirim = (o) => o.sicaklik.toFixed(1) + " C / " + fahrenhayt(o.sicaklik).toFixed(1) + " F";
EOF

cat > kaynak/sablon.js <<'EOF'
import { olcumSatiri } from "./bicim.js";

export const liste = (olcumler) =>
  "<ul>" + olcumler.map((o) => "<li>" + olcumSatiri(o) + "</li>").join("") + "</ul>";
export const baslik = (istasyon) => "<h1>" + istasyon + "</h1>";
EOF

cat > kaynak/grafik.js <<'EOF'
import { SICAKLIK_ALT, SICAKLIK_UST } from "./birim.js";

const olcekle = (v, uzunluk) => ((v - SICAKLIK_ALT) / (SICAKLIK_UST - SICAKLIK_ALT)) * uzunluk;

export function sicaklikYolu(olcumler, genislik, yukseklik) {
  const adim = olcumler.length > 1 ? genislik / (olcumler.length - 1) : 0;
  return olcumler
    .map((o, i) => (i ? "L" : "M") + (i * adim).toFixed(1) + " " +
      (yukseklik - olcekle(o.sicaklik, yukseklik)).toFixed(1))
    .join(" ");
}
EOF

cat > kaynak/arsiv.js <<'EOF'
import { gunAnahtari } from "./tarih.js";
import { liste } from "./sablon.js";

export function gunlereBol(olcumler) {
  const kova = new Map();
  for (const o of olcumler) {
    const g = gunAnahtari(o.damga);
    kova.set(g, [...(kova.get(g) ?? []), o]);
  }
  return kova;
}
export const arsivSayfasi = (gun, olcumler) => "<h2>" + gun + "</h2>" + liste(olcumler);
EOF

cat > kaynak/giris-tarayici.js <<'EOF'
import { liste, baslik } from "./sablon.js";
import { sicaklikYolu } from "./grafik.js";

export function baslat(kok, istasyon, olcumler) {
  kok.innerHTML = baslik(istasyon) + liste(olcumler);
  return sicaklikYolu(olcumler, 320, 120);
}
EOF

cat > kaynak/giris-sunucu.js <<'EOF'
import { liste, baslik } from "./sablon.js";
import { gunlereBol, arsivSayfasi } from "./arsiv.js";

export function sayfaUret(istasyon, olcumler) {
  const gunluk = [...gunlereBol(olcumler)].map(([g, l]) => arsivSayfasi(g, l));
  return { canli: baslik(istasyon) + liste(olcumler), arsiv: gunluk };
}
EOF

Sekiz modül, iki giriş noktası. Tarayıcı girişi ekranı çizer ve sıcaklık grafiğini üretir; sunucu girişi aynı listeyi ve günlük arşiv sayfalarını üretir. Biçimlendirme, şablon, birim ve tarih modülleri ikisinde de kullanılır.

Çizgenin Çıkarılması

Çizge, giriş noktalarından başlayan bir dolaşımla bulunur. Her modül bir düğüm, her içe aktarım bir yönlü kenardır. Veri Yapıları kursundaki enine arama, burada dosya sistemi üzerinde çalışır: kuyruktan bir yol alınır, kaynağı okunur, belirteçleri çözülür, bulunan komşular kuyruğa eklenir.

// cizge.mjs — kaynak agacindan bagimlilik cizgesi ve cikti parcalari.
import { readFileSync, statSync } from "node:fs";
import { dirname, join, relative } from "node:path";

const GIRISLER = ["kaynak/giris-tarayici.js", "kaynak/giris-sunucu.js"];
const ICE_AKTARIM = /^\s*(?:import|export)[^"']*from\s*["']([^"']+)["']/gm;

function cizgeCikar(girisler) {
  const dugum = new Map();                      // yol -> { boyut, komsu[] }
  const kuyruk = [...girisler];
  while (kuyruk.length > 0) {
    const yol = kuyruk.shift();
    if (dugum.has(yol)) continue;
    const kaynak = readFileSync(yol, "utf8");
    const komsu = [...kaynak.matchAll(ICE_AKTARIM)]
      .map(([, belirtec]) => relative(".", join(dirname(yol), belirtec)));
    dugum.set(yol, { boyut: statSync(yol).size, komsu });
    kuyruk.push(...komsu);
  }
  return dugum;
}

function kapanis(cizge, kok) {                  // kokten erisilen tum dugumler
  const gorulen = new Set(), yigit = [kok];
  while (yigit.length > 0) {
    const yol = yigit.pop();
    if (gorulen.has(yol)) continue;
    gorulen.add(yol);
    yigit.push(...cizge.get(yol).komsu);
  }
  return gorulen;
}

function degerlendirmeSirasi(cizge, kok) {      // sonradan-gezinme: bagimlilik once
  const sira = [], gorulen = new Set();
  (function gez(yol) {
    if (gorulen.has(yol)) return;
    gorulen.add(yol);
    for (const k of cizge.get(yol).komsu) gez(k);
    sira.push(yol);
  })(kok);
  return sira;
}

const cizge = cizgeCikar(GIRISLER);
const ad = (y) => y.replace("kaynak/", "");
const kenarSayisi = [...cizge.values()].reduce((t, d) => t + d.komsu.length, 0);

console.log("-- cizge --");
console.log("dugum:", cizge.size, " kenar:", kenarSayisi);
for (const [yol, d] of [...cizge].sort()) {
  console.log("  " + ad(yol).padEnd(20) + String(d.boyut).padStart(5) + " B  -> " +
    (d.komsu.map(ad).join(", ") || "(yaprak)"));
}

console.log("-- degerlendirme sirasi (tarayici girisi) --");
console.log("  " + degerlendirmeSirasi(cizge, GIRISLER[0]).map(ad).join(" -> "));

const kapanislar = new Map(GIRISLER.map((g) => [g, kapanis(cizge, g)]));
console.log("-- giris kapanislari --");
for (const [g, k] of kapanislar) {
  const bayt = [...k].reduce((t, y) => t + cizge.get(y).boyut, 0);
  console.log("  " + ad(g).padEnd(20) + String(k.size).padStart(2) + " modul  " +
    String(bayt).padStart(5) + " B");
}

// Imza: modulu isteyen girislerin kumesi. Ayni imza = ayni cikti parcasi.
const imza = new Map();
for (const yol of cizge.keys()) {
  const isteyen = GIRISLER.filter((g) => kapanislar.get(g).has(yol));
  const anahtar = isteyen.map(ad).join(" + ");
  if (!imza.has(anahtar)) imza.set(anahtar, []);
  imza.get(anahtar).push(yol);
}

console.log("-- cikti parcalari --");
let toplamParca = 0;
for (const [anahtar, moduller] of [...imza].sort((a, b) => b[0].length - a[0].length)) {
  const bayt = moduller.reduce((t, y) => t + cizge.get(y).boyut, 0);
  toplamParca += bayt;
  console.log("  isteyen: " + anahtar);
  console.log("    " + String(bayt).padStart(5) + " B  " + moduller.map(ad).sort().join(", "));
}
const kaynakToplam = [...cizge.values()].reduce((t, d) => t + d.boyut, 0);
console.log("-- toplam --");
console.log("  kaynak:", kaynakToplam, "B   parcalar:", toplamParca, "B");
console.log("  kopyalanan modul yok:", kaynakToplam === toplamParca);
$ node cizge.mjs
-- cizge --
dugum: 8  kenar: 10
  arsiv.js              367 B  -> tarih.js, sablon.js
  bicim.js              387 B  -> tarih.js, birim.js
  birim.js              186 B  -> (yaprak)
  giris-sunucu.js       302 B  -> sablon.js, arsiv.js
  giris-tarayici.js     239 B  -> sablon.js, grafik.js
  grafik.js             458 B  -> birim.js
  sablon.js             229 B  -> bicim.js
  tarih.js              227 B  -> (yaprak)
-- degerlendirme sirasi (tarayici girisi) --
  tarih.js -> birim.js -> bicim.js -> sablon.js -> grafik.js -> giris-tarayici.js
-- giris kapanislari --
  giris-tarayici.js    6 modul   1726 B
  giris-sunucu.js      6 modul   1698 B
-- cikti parcalari --
  isteyen: giris-tarayici.js + giris-sunucu.js
     1029 B  bicim.js, birim.js, sablon.js, tarih.js
  isteyen: giris-tarayici.js
      697 B  giris-tarayici.js, grafik.js
  isteyen: giris-sunucu.js
      669 B  arsiv.js, giris-sunucu.js
-- toplam --
  kaynak: 2395 B   parcalar: 2395 B
  kopyalanan modul yok: true

Bayt sayıları dosyaların gerçek boyutlarıdır; kaynak dosyalar birebir yukarıdaki blokla üretildiğinde bu değerler çıkar. Bir satırlık bir düzenleme bile sayıları değiştirir.

Değerlendirme Sırası

Çizgenin ilk kullanımı sıralamadır. Bir modülün gövdesi, bağımlılıklarının gövdesi çalıştıktan sonra çalışmalıdır; aksi hâlde henüz tanımlanmamış bir bağa erişilir. Sonradan gezinmeli dolaşım bu sırayı verir: tarih.js ve birim.js en başta, giriş noktası en sonda. Bu, Veri Yapıları kursundaki topolojik sıralamanın çizgeye uygulanmış hâlidir.

Sıralamanın çıktıya iki yansıması vardır. Tek dosyaya yazılan bir parçada modüllerin gövdeleri bu sırayla dizilir. Birden çok dosyaya bölünen çıktıda ise sıra, dosyaların yükleme sırasına dönüşür: bir parça, bağımlı olduğu parça değerlendirilmeden çalıştırılamaz.

Çizgede döngü varsa sonradan gezinme tek bir doğru sıra vermez. Modül sistemleri konusundaki dairesel bağımlılık tartışması burada da geçerlidir: paketleyici döngüyü kırmaz, yalnızca bir sıra seçer ve seçtiği sıra hangi bağın erken okunacağını belirler.

Giriş Noktası Kümesi

Bir derlemede kaç giriş noktası olacağı, oluşturma stratejisi kararının doğrudan sonucudur. Tarayıcıda çalışan kod için bir giriş, sunucuda çalışan kod için ayrı bir giriş, kenarda çalışan modül için üçüncü bir giriş gerekir. Her girişin kendi kapanışı vardır ve kapanışlar kesişir: örnekte iki girişin toplam altışar modülünden dördü ortaktır.

Girişler yalnız kapsam olarak değil, hedef olarak da ayrılır. Aynı kaynaktan üretilen iki çıktı farklı biçimlerde yazılır: sunucu çıktısı çalışma zamanının doğrudan yükleyebileceği modül biçiminde, tarayıcı çıktısı belgeye eklenecek betik biçiminde. Dış bırakılan bağımlılıklar da hedefe göre değişir — sunucu girişinde çalışma zamanının kendi sağladığı modüller çizgeye alınmaz, tarayıcı girişinde böyle bir kaynak yoktur ve her şeyin çıktıya girmesi gerekir.

Bu, kaynak ağaçta tek bir dosyanın iki farklı çıktıda iki farklı biçimde görünebileceği anlamına gelir. Ortak modüllerin her iki hedefte de çalışabilir olması, kaynak düzenine konan bir kısıttır: ortak katmanda yalnız ortama bağımsız kod bulunur.

Parçalama Ölçütü

Ortak modüller iki yolla ele alınabilir. Her girişin çıktısına kopyalanabilirler; o zaman aynı kod iki dosyada iki kez durur. Ya da ayrı bir parçaya konup her iki girişten yüklenebilirler; o zaman bir dosya daha üretilir.

Karar, çizgeden okunabilen tek bir bilgiye dayanır: bir modülü hangi girişler ister. Bu kümeye modülün kullanım imzası (usage signature) denir. Aynı imzaya sahip modüller birlikte gerekir, dolayısıyla birlikte konabilir. Farklı imzalar farklı parçalar üretir.

Çıktıda üç parça görünmesinin nedeni budur: iki girişin de istediği dört modül tek bir paylaşılan parçada, yalnız tarayıcının istediği iki modül bir parçada, yalnız sunucunun istediği iki modül üçüncü parçada. Son satırdaki denetim ölçütün sağlamasıdır: parçaların toplamı kaynağın toplamına eşitse hiçbir modül iki parçaya kopyalanmamıştır.

Uygulama Mimarisi kursundaki kod bölme dersi aynı ölçütü rota sınırları için kullanmıştı; orada imzayı oluşturan küme rotalardı. Ölçüt aynıdır, imzayı üreten kaynak farklıdır: bu derste girişler, orada rotalar.

Parçanın İçinde Ne Var

Bir parça, modül gövdelerinin arka arkaya yazılmasından ibaret değildir. Her modülün kendi kapsamı korunmalı, dışa aktarımları başka modüllere görünür olmalı ve bir modül birden çok kez içe aktarılsa bile bir kez değerlendirilmelidir. Bunu sağlayan üç şey parçanın içinde yer alır: modül sarmalayıcıları, tek değerlendirmeyi güvenceye alan bir kayıt, ve belirteçleri çıktı anahtarlarına çeviren bir eşleme. Modüller, Araçlar ve Ekosistem kursundaki paketleyici dersi bu yapıyı çalışır hâlde kurmuştu.

Paylaşılan parçanın varlığı bir şey daha ekler: parçalar arası dışa aktarım yüzeyi. Tarayıcı parçası sablon.js içindeki iki adı kullanıyorsa, paylaşılan parça bu adları dışarıya vermek zorundadır. Parça sınırı, bu yüzden yalnız bir dosya sınırı değil, aynı zamanda bir arayüz sınırıdır: sınırı geçen her ad çıktıda görünür kalmalı, yeniden adlandırılamaz veya elenemez.

Kapsam birleştirme bu noktada sınırlanır. Aynı parçanın içindeki modüller tek kapsamda eritilebilir; parça sınırını geçen adlar eritilemez. Parça sayısını artırmak, bu yüzden yalnız istek sayısını değil, uygulanabilecek dönüşümlerin alanını da etkiler.

Çizgenin Söylemediği Şeyler

Buradaki çizge yalnız statik içe aktarımlardan kuruldu ve bu, gerçek bir derlemenin girdisinin bir parçasıdır.

Belirteçler düzenli ifadeyle bulundu. Bir dizginin içinde geçen içe aktarım yazımı da eşleşir, yorumdaki bir satır da. Gerçek araçlar kaynağı ayrıştırıp soyut sözdizim ağacı üzerinde çalışır; uzantı tamamlama, dizin girişleri, çıplak belirteç araması ve koşullu girişlerin çözümü de o katmanın işidir.

Çizgeye giren tek kenar türü statik içe aktarım değildir. Dinamik içe aktarım bir kenardır ama zamanı farklıdır; biçem dosyaları, görseller ve yazı tipleri de kenar üretir. Bunların her biri parça oluşumunu değiştirir ve sonraki üç ders sırayla bunları ekler.

Son olarak çizge, bir modülün tamamının gerekli olduğunu söyler; içindeki hangi adın kullanıldığını söylemez. Paylaşılan parçadaki birim.js dört ad dışa aktarıyor, bunların ikisi hiç kullanılmıyor olabilir. Bu ayrımı görmek için çizgeyi modül düzeyinden ad düzeyine indirmek gerekir.

Özet

  • Modül paketleme iki karardır: giriş noktalarından erişilen modül kümesinin çıkarılması ve bu kümenin çıktı parçalarına bölünmesi.
  • Bağımlılık çizgesi giriş noktalarından dolaşımla kurulur; sonradan gezinmeli sıra, bağımlılıkları kendinden önce koyan değerlendirme sırasını verir.
  • Giriş noktası sayısı oluşturma stratejisinden gelir; her girişin ayrı bir kapanışı, ayrı bir çıktı biçimi ve ayrı bir dış bağımlılık sınırı vardır.
  • Modüller kullanım imzasına — kendilerini isteyen girişler kümesine — göre parçalara ayrılır; parçaların toplam boyutunun kaynağın toplamına eşit olması, hiçbir modülün kopyalanmadığının sağlamasıdır.
  • Parça sınırı aynı zamanda bir arayüz sınırıdır: sınırı geçen adlar çıktıda korunmak zorundadır ve kapsam birleştirme parça içiyle sınırlıdır.

Sonraki Adım

Bu dersteki parçalar giriş noktalarından doğdu ve hepsi ilk yüklemede gerekliydi. Oysa bir uygulamanın her parçası aynı anda gerekmez: istasyon sorumlusunun paneli yalnız o panel açıldığında, dışa aktarma ekranı yalnız o düğmeye basıldığında ister. Kaynakta bu ayrımı işaretleyen bir yazım vardır — import() çağrısı bir kenarı çizgeye ekler ama onu zamanında ayırır. Sonraki ders bu sınırı ele alır: dinamik içe aktarımdan doğan parçalar nasıl hesaplanır, ilk yüklemede inen ile sonradan inen nasıl ayrılır ve parça sayısını artırmanın hangi noktada zarara döndüğü nasıl ölçülü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