İçeriğe geç
academia.sh

Ders 14 / 16

Sunucuda Şablon Oluşturma

Görünümün sunucuda veriden üretilmesi: şablon değerlendiricisinin çözümleme ve üretim adımları, kaçışlamanın öntanımlı olması, ham çıktının açık işaretle istenmesi, öznitelik bağlamının farkı ve düzen ile sayfa şablonunun ayrılması.

İçindekiler

Önceki ders diskteki dosyaları sundu. Katalog sayfası da o dosyalardan biriydi ama içeriğini kendisi taşımıyordu: belge boş bir kap olarak gidiyor, kitap listesi tarayıcıda betikle kuruluyordu. Sunucu kitap kayıtlarını zaten okuyabildiğine göre listeyi belgenin içine kendisi yazabilir.

Bu birleştirmeyi yapan mekanizmaya şablon (template) denir: içine değer konacak yerleri işaretlenmiş bir metin. Şablonu veriyle birleştiren programa şablon değerlendiricisi (template engine) denir. Bu dersin merkezinde tek bir soru vardır: veriden gelen bir metin işaretlemenin içine konurken ne olur?

Şablonun İki Adımı

Değerlendirici işini iki adımda yapar. Önce şablon metni çözümlenir: düz metin parçaları ile yer tutucular birbirinden ayrılır ve blok etiketleri iç içe bir yapı kurar. Sonra bu yapı veriyle gezilerek üretilir.

İki adımın ayrı olması bir gerekliliktir. Çözümleme şablona bakar ve veriye bakmaz; üretim veriye bakar ve şablon metnini bir daha okumaz. Böylece aynı şablon bir kez çözümlenip yüz bin kez üretilebilir, ve daha önemlisi, verinin içeriği hiçbir koşulda şablon yapısını değiştiremez. Veriden gelen bir metin bir blok etiketine benzese bile üretim adımında etiket olarak yorumlanmaz, çünkü çözümleme çoktan bitmiştir.

Aşağıdaki değerlendirici dört etiket tanır: {{ ad }} değeri kaçışlayarak yazar, {{! ad }} ham yazar, {{# ad }} ile {{/ ad }} arası bir dizi üzerinde yinelenir, {{^ ad }} ile {{/ ad }} arası ise değer boşken çalışır.

// sablon.mjs — kacislamayi ontanimli yapan kucuk bir sablon degerlendiricisi
const KACIS = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
export const kacisla = (deger) => String(deger).replace(/[&<>"']/g, (k) => KACIS[k]);

// {{ ad }} kacislar, {{! ad }} kacislamaz, {{# ad }}..{{/ ad }} yineler,
// {{^ ad }}..{{/ ad }} bos oldugunda calisir.
const ETIKET = /\{\{\s*([#^/!]?)\s*([\w.]+)\s*\}\}/g;

const coz = (yigit, ad) => {
  if (ad === ".") return yigit[yigit.length - 1];
  const parcalar = ad.split(".");
  for (let i = yigit.length - 1; i >= 0; i--) {
    let deger = yigit[i];
    if (deger === null || typeof deger !== "object") continue;
    if (!(parcalar[0] in deger)) continue;
    for (const p of parcalar) deger = deger?.[p];
    return deger;
  }
  return undefined;
};

// Sablonu tek geciste agaca cevirir: dizgi parcalari ve blok dugumleri.
const ayristir = (metin) => {
  const kok = { cocuk: [] };
  const yigit = [kok];
  let son = 0;
  for (const e of metin.matchAll(ETIKET)) {
    const [tam, tur, ad] = e;
    const ust = yigit[yigit.length - 1];
    if (e.index > son) ust.cocuk.push(metin.slice(son, e.index));
    son = e.index + tam.length;
    if (tur === "#" || tur === "^") {
      const dugum = { tur, ad, cocuk: [] };
      ust.cocuk.push(dugum);
      yigit.push(dugum);
    } else if (tur === "/") {
      if (yigit.length === 1 || yigit[yigit.length - 1].ad !== ad)
        throw new Error(`kapanmayan blok: ${ad}`);
      yigit.pop();
    } else {
      ust.cocuk.push({ tur, ad });
    }
  }
  if (yigit.length !== 1) throw new Error(`kapanmayan blok: ${yigit[yigit.length - 1].ad}`);
  kok.cocuk.push(metin.slice(son));
  return kok;
};

const uret = (dugum, yigit) =>
  dugum.cocuk.map((c) => {
    if (typeof c === "string") return c;
    const deger = coz(yigit, c.ad);
    if (c.tur === "#") {
      const liste = Array.isArray(deger) ? deger : deger ? [deger] : [];
      return liste.map((oge) => uret(c, [...yigit, oge])).join("");
    }
    if (c.tur === "^") {
      const bos = Array.isArray(deger) ? deger.length === 0 : !deger;
      return bos ? uret(c, yigit) : "";
    }
    if (deger === undefined || deger === null) return "";
    return c.tur === "!" ? String(deger) : kacisla(deger);
  }).join("");

export const olustur = (metin, veri) => uret(ayristir(metin), [veri]);

Ad çözümü bir kapsam yığıtı üzerinden yapılır: blok içine girildiğinde o turun ögesi yığıta eklenir, ad önce en içteki kapsamda aranır, bulunamazsa dışarı doğru gidilir. Bu sayede yineleme bloğunun içinden dıştaki alanlara da erişilebilir.

Kaçışlamanın Öntanımlı Olması

Kaçışlama (escaping), işaretleme dilinde yapısal anlamı olan karakterlerin karakter varlığı (character entity) biçimine çevrilmesidir. Bu dönüşüm yapılırsa veriden gelen metin çıktıda metin olarak kalır; yapılmazsa işaretlemenin parçası olur.

Değerlendiricide kaçışlama öntanımlıdır ve ham çıktı ancak {{! ad }} yazılarak, yani şablonu yazan kişinin açık kararıyla elde edilir. Sıralamanın tersi de yazılabilirdi; farkı, hangisinin unutulduğunda ne olduğudur. Kaçışlama öntanımlıysa unutmanın bedeli görünürde bir tuhaflıktır. Ham çıktı öntanımlıysa unutmanın bedeli, veriden gelen metnin belge yapısına karışmasıdır.

Aşağıdaki blok aynı veriyi iki yer tutucuya birden verir ve farkı gösterir. İşaret metni zararsızdır: bir vurgulama ögesi.

// dene.mjs — ayni veriyle kacislamali ve kacislamasiz uretimi karsilastirir
import { olustur } from "./sablon.mjs";

const SABLON = `<ul class="katalog">
{{# kitaplar }}  <li>{{ baslik }} — <span class="yazar">{{ yazar }}</span></li>
{{/ kitaplar }}{{^ kitaplar }}  <li class="bos">Sonuc yok</li>
{{/ kitaplar }}</ul>
<p class="not">{{ not }}</p>
<p class="not-ham">{{! not }}</p>`;

const VERI = {
  kitaplar: [
    { baslik: "Kayip Zamanin Izinde", yazar: "Marcel Proust" },
    { baslik: "Kar & Buz <arsiv>", yazar: "Sube 3" },
  ],
  not: "<mark>ayrilmis</mark>",
};

const cikti = olustur(SABLON, VERI);
console.log(cikti);
console.log("---");
console.log("cikti bayt:", Buffer.byteLength(cikti));
console.log("mark ogesi sayisi:", (cikti.match(/<mark>/g) ?? []).length);
console.log("--- bos liste ---");
console.log(olustur(SABLON, { kitaplar: [], not: "yok" }));
<ul class="katalog">
  <li>Kayip Zamanin Izinde — <span class="yazar">Marcel Proust</span></li>
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Sube 3</span></li>
</ul>
<p class="not">&lt;mark&gt;ayrilmis&lt;/mark&gt;</p>
<p class="not-ham"><mark>ayrilmis</mark></p>
---
cikti bayt: 278
mark ogesi sayisi: 1
--- bos liste ---
<ul class="katalog">
  <li class="bos">Sonuc yok</li>
</ul>
<p class="not">yok</p>
<p class="not-ham">yok</p>

Aynı veri iki farklı sonuç vermiştir. Kaçışlanan yer tutucuda işaret metni ekranda okunacak bir metindir; ham yer tutucuda belgeye eklenmiş bir ögedir. Sayım bunu doğrular: çıktıda bir tek vurgulama ögesi vardır ve o da ham yazılan yerden gelmiştir.

Kitap başlığındaki & ve açılı ayraçlar da kaçışlanmıştır. Bu, güvenlikten önce bir doğruluk meselesidir: kaçışlanmasaydı başlığın bir bölümü ekranda hiç görünmezdi, çünkü işaretleme çözümleyicisi onu etiket başlangıcı sayardı.

Metin Bağlamı ile Öznitelik Bağlamı

Tek bir kaçışlama işlevi, değerin belgenin neresine konduğunu bilmez. Metin içine konan bir değerde açılı ayraçlar önemlidir; öznitelik değerine konan bir değerde tırnak önemlidir. Yukarıdaki işlev her ikisini de dönüştürdüğü için iki bağlamda da doğru sonuç verir, ama bunun neden gerekli olduğu ayrıca gösterilmelidir.

// oznitelik.mjs — kacislamanin oznitelik degeri uzerindeki etkisi
import { olustur } from "./sablon.mjs";

const KACISLAMALI = `<a class="sube" title="{{ ad }}" href="/sube/{{ kod }}">Sube</a>`;
const HAM = `<a class="sube" title="{{! ad }}" href="/sube/{{ kod }}">Sube</a>`;
const VERI = { ad: 'Merkez "Eski Kitaplar" Salonu', kod: "s3" };

// Cozumleyici oznitelik degerini ilk kapanis tirnaginda bitirir; asagidaki desen
// ayni kurali uygular ve degerin nerede kesildigini gosterir.
const baslikDegeri = (metin) => metin.match(/title="([^"]*)"/)[1];

for (const [ad, sablon] of [["kacislamali", KACISLAMALI], ["ham", HAM]]) {
  const cikti = olustur(sablon, VERI);
  console.log(ad);
  console.log("  cikti :", cikti);
  console.log("  title :", JSON.stringify(baslikDegeri(cikti)));
}
kacislamali
  cikti : <a class="sube" title="Merkez &quot;Eski Kitaplar&quot; Salonu" href="/sube/s3">Sube</a>
  title : "Merkez &quot;Eski Kitaplar&quot; Salonu"
ham
  cikti : <a class="sube" title="Merkez "Eski Kitaplar" Salonu" href="/sube/s3">Sube</a>
  title : "Merkez "

Ham çıktıda öznitelik değeri “Merkez “ ile bitmiştir: şube adındaki tırnak, özniteliği kapatmıştır. Geri kalan sözcükler artık değerin parçası değil, etiketin gövdesindeki başka bir şeydir. Kaçışlanan çıktıda ise değer bütün olarak durur ve çözümleyici karakter varlıklarını okurken şube adını eksiksiz geri verir.

Buradan çıkan kural şudur: öznitelik değerleri her zaman tırnak içine alınır ve şablona konan değer bu tırnağı kapatabilecek karakterlerden arındırılır. Tırnaksız öznitelik yazımı, kaçışlama işlevinin dönüştürmediği boşluk gibi karakterleri de yapısal hâle getirdiği için bu düzende hiç kullanılmaz.

Düzen ve Sayfa

Her sayfanın belge iskeleti aynıdır: aynı dil bildirimi, aynı başlık ögesi, aynı gövde çerçevesi. Bu ortak kısmı her şablona kopyalamak yerine bir düzen şablonu yazılır ve sayfanın kendi çıktısı ona yerleştirilir. Yerleştirme, düzenin içindeki tek ham yer tutucuyla yapılır — çünkü oraya konan şey veri değil, zaten kaçışlanmış bir üretim çıktısıdır.

Şablonun gördüğü veri yapısına görünüm modeli (view model) denir. Görünüm modeli, veri kaynağıyla şablon arasındaki sözleşmedir: şablon kayıtların nereden geldiğini bilmez, kayıtları okuyan kod da işaretlemeyi bilmez.

// gorunum.mjs — katalog sayfasini sunucuda uretip sunar (sablon.mjs ile ayni dizinde)
import { createServer } from "node:http";
import { olustur } from "./sablon.mjs";

const KITAPLAR = [
  { baslik: "Kayip Zamanin Izinde", yazar: "Marcel Proust", sube: "Merkez" },
  { baslik: "Tutunamayanlar", yazar: "Oguz Atay", sube: "Sube 3" },
  { baslik: "Kar & Buz <arsiv>", yazar: "Derleme", sube: "Sube 3" },
];

const DUZEN = `<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>{{ baslik }}</title></head>
<body><h1>{{ baslik }}</h1>{{! icerik }}</body></html>
`;

const KATALOG = `<p class="ozet">{{ sayi }} kayit</p>
<ul class="katalog">
{{# kitaplar }}  <li>{{ baslik }} — <span class="yazar">{{ yazar }}</span> ({{ sube }})</li>
{{/ kitaplar }}{{^ kitaplar }}  <li class="bos">Sonuc yok</li>
{{/ kitaplar }}</ul>`;

// Gorunum modeli: sablonun gordugu tek veri yapisi. Sorgu ve kayitlar burada
// birlestirilir; sablonun kendisi veri kaynagini hic bilmez.
const gorunumModeli = (sorgu) => {
  const q = (sorgu ?? "").toLocaleLowerCase("tr");
  const kitaplar = q ? KITAPLAR.filter((k) => k.baslik.toLocaleLowerCase("tr").includes(q)) : KITAPLAR;
  return { kitaplar, sayi: kitaplar.length };
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  if (adres.pathname !== "/katalog") return yanit.writeHead(404).end();

  const icerik = olustur(KATALOG, gorunumModeli(adres.searchParams.get("q")));
  const belge = olustur(DUZEN, { baslik: "Kutuphane Katalogu", icerik });

  yanit.setHeader("Content-Type", "text/html; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(belge));
  yanit.writeHead(200).end(belge);
}).listen(8311, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8311"));
#!/usr/bin/env bash
# gorunum.mjs'yi baslatir, uretilen belgeyi ve suzulmus sonucu gosterir, durdurur.
node gorunum.mjs > /dev/null &
sunucu=$!
sleep 1
A=http://127.0.0.1:8311

echo "--- tum katalog ---"
curl -sS "$A/katalog"
echo "--- suzgec: q=kar ---"
curl -sS "$A/katalog?q=kar"
echo "--- eslesmeyen sorgu ---"
curl -sS "$A/katalog?q=zzz" | grep -c 'class="bos"'

kill "$sunucu"
--- tum katalog ---
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title></head>
<body><h1>Kutuphane Katalogu</h1><p class="ozet">3 kayit</p>
<ul class="katalog">
  <li>Kayip Zamanin Izinde — <span class="yazar">Marcel Proust</span> (Merkez)</li>
  <li>Tutunamayanlar — <span class="yazar">Oguz Atay</span> (Sube 3)</li>
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Derleme</span> (Sube 3)</li>
</ul></body></html>
--- suzgec: q=kar ---
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title></head>
<body><h1>Kutuphane Katalogu</h1><p class="ozet">1 kayit</p>
<ul class="katalog">
  <li>Kar &amp; Buz &lt;arsiv&gt; — <span class="yazar">Derleme</span> (Sube 3)</li>
</ul></body></html>

Boş sonuç dalı ayrıca sınanmıştır: eşleşmeyen sorguda class="bos" geçen satır sayısı birdir. Bu, boş listenin şablonda ele alınmış olduğunu doğrular; ele alınmasaydı çıktıda gövdesiz bir liste ögesi kalırdı.

Bağlantı noktası 8311 keyfîdir ve boş olmalıdır.

Şablonun Taşıyamayacağı Yük

Şablon bir görünüm aracıdır ve yükü arttıkça bozulur. Üç sınır pratikte belirleyicidir.

Hesap şablonda yapılmaz. Gecikmiş ödünç işleminin gün sayısı, ceza tutarı ya da sıralama ölçütü görünüm modelinde hesaplanır. Şablonda hesap yapılırsa aynı kural iki sayfada iki farklı sonuç vermeye başlar ve sınanamaz hâle gelir.

Veri erişimi şablonda yapılmaz. Yineleme bloğunun içinden bir sorgu çalıştırılabilseydi sayfadaki satır sayısı kadar sorgu doğardı. Görünüm modeli, üretim başlamadan önce ihtiyaç duyulan her şeyi hazır getirir.

Ham çıktı sayılabilir olmalıdır. Ham yer tutucu, sistemde denetlenmesi gereken tek noktadır. Sayısı azaldıkça ve yalnızca üretim çıktısı yerleştirilen yerlerde kaldıkça şablon katmanının doğruluğu gözle sınanabilir kalır.

Özet

  • Şablon değerlendiricisi önce şablonu çözümler, sonra veriyle üretir; verinin içeriği çözümleme bittikten sonra geldiği için şablon yapısını değiştiremez.
  • Kaçışlama, işaretlemede yapısal anlamı olan karakterleri karakter varlığına çevirir ve öntanımlı olmalıdır; ham çıktı şablonu yazanın açık kararıyla istenir.
  • Ölçüm iki bağlamı ayırır: metin bağlamında kaçışlanmayan değer belgeye öge ekler, öznitelik bağlamında ise değeri ilk tırnakta keser.
  • Düzen şablonu ortak belge iskeletini taşır; sayfa çıktısı oraya tek bir ham yer tutucuyla yerleşir, çünkü yerleşen şey veri değil zaten üretilmiş çıktıdır.
  • Görünüm modeli şablon ile veri kaynağı arasındaki sözleşmedir; hesap ve veri erişimi şablonda değil, görünüm modelini kuran kodda yapılır.

Sonraki Adım

Şimdiye kadar veri hep içeriden geldi: diskteki dosyalar, bellekteki kayıtlar. Kütüphane servisinin gerçek işleyişinde dışarıdan da veri gelir — üye fotoğrafı, bağış yapılan kitabın kapak görüntüsü, şubeden yüklenen sayım dosyası. Bu içerik sunucuya gelirken boyutu bilinmez, türü bildirildiği gibi olmayabilir ve nereye yazılacağı bir karardır. Sonraki ders yüklemeyi bu üç soruyla ele alır: sunucu sınırı aşan bir yükü ne zaman ve nasıl reddeder, dosyanın türüne uzantısına bakmadan nasıl karar verir, ve yazılan dosya sunulan dizinden neden ayrı tutulur?

İ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