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.