Ders 04 / 13
Görünümler ve Şablonlar
Şablon bir kez derlenir ve yerel değerler her çizimde kaçışlanır. Ders kaçışlamanın ne yaptığını, eksik yerel değerin hata verdiğini, kısmi şablonda çift kaçışlamanın doğduğunu ve derlenmiş şablonun kalıcı olduğunu ölçer.
Yayın Güncelleme
Bu derste
- Şablon motorunun yerel değerleri kaçışladığını ve kaçışlamanın iki kez uygulanırsa ne verdiğini ölçer
- Eksik bir yerel değerin sessizce boş kalmadığını, hata verdiğini gösterir
- Kısmi şablonun çıktısı dış şablona yerel değer olarak verildiğinde çift kaçışlamanın doğduğunu ölçer
- Derlenmiş şablonun uygulama deposunda durduğunu ve kalışının kalıcı olduğunu gösterir
İçindekiler
Önceki ders, denetleyicinin ürünü hazırladığını ve bildirim mesajını (flash) sonraki isteğe taşıdığını gösterdi. Hazırlanan değer sonunda bir **görünüm (view)**e girer ve görünüm onu kullanıcının göreceği metne çevirir. Görünümün kaynağı bir **şablon (template)**tir: sabit metin ile {{ad}} gibi doldurulacak yerlerden oluşan bir dizgi. Bu dersin iki sorusu var. Değer şablona girerken ne olur: yerel değer olarak verilen <b>kalem</b> çıktıda etiket mi olur, metin mi? Ve şablonun kendisi, çizildikten sonra nerede durur ve hangi isteğe kadar kalır?
Çekirdeğin kalis.rb dosyası bir kez daha aşağıda verilir. Blok kaynaktır, çıktısı yoktur ve require_relative ile çekilir.
# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci
# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
def initialize(gelen)
@gelen = gelen
@yeni = {}
end
def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }
def []=(anahtar, deger)
@yeni[anahtar] = deger
end
def birakilan = @yeni
end
# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu
def initialize(uygulama, oturum)
@uygulama = uygulama
@params = {}
@oturum = oturum
@flash = Flash.new(oturum.delete(:flash) || {})
@istek_deposu = {}
end
end
# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
attr_reader :depo
def initialize
@oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
@depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
kuyruk: [], onbellek: {}, sayac: 0 }
end
def istek(istemci)
baglam = Baglam.new(self, @oturumlar[istemci])
sonuc = yield baglam
baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
sonuc
end
end
# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
uygulama = Uygulama.new
ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?
gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
end
tur = if gorulen[2] != "--" then "kalici"
elsif gorulen[1] != "--" then "oturum"
elsif gorulen[0] != "--" then "sonraki"
else "bu_istek"
end
[tur, ["A1", *gorulen].join(" "), ilk]
end
Kaçışlama
Çekirdeğin görünüm katmanı iki yardımcıdan oluşur. kacisla metindeki dört karakteri karşılığıyla değiştirir. sablon_derle bir şablon kaynağını bir kez bir lambda’ya çevirir; o lambda çağrıldıkça yerel değerleri kacisla’dan geçirerek yerlerine koyar. Kaçışlama (escaping), kullanıcıdan gelen metnin sayfada işaretleme olarak değil metin olarak görünmesini sağlar.
# kacisla.rb — dort karakterin kacislanmasi ve iki kez kacislama
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
p kacisla('<script>alert("x")</script>')
p kacisla("Tom & Jerry")
p kacisla(kacisla("<b>"))
p kacisla(nil)
p kacisla(12)
"<script>alert("x")</script>" "Tom & Jerry" "&lt;b&gt;" "" "12"
İlk satır dört karakterin hepsini gösterir: < ve > etiket sınırlarını, " öznitelik sınırını, & ise kaçışlamanın kendi başlangıcını korur. Etiket olarak yorumlanabilecek bir metin artık yalnızca metindir. İkinci satırda & önce kaçışlandı ve & oldu. Üçüncü satır çift kaçışlamanın sonucudur: <b> ilk geçişte <b> oldu, ikinci geçişte & karakterleri yeniden değişti ve metin &lt;b&gt; biçimine geldi. Sayfada bu, kullanıcıya <b> yazısını gösterir. Son iki satır kacisla’nın her girdiyi önce dizgiye çevirdiğini gösterir: nil boş dizgi, 12 ise "12" oldu.
Kaçışlamanın şablon motorunda olması bir tercihtir. Denetleyici ya da model değeri kaçışlayıp saklasaydı, aynı veri bir konsolda, bir dışa aktarmada ya da başka bir görünümde < yazısıyla görünürdü. Kaçışlama çıktının biçimine aittir, bu yüzden çizim anında ve yalnız çizilen yerde yapılır. Değerin kendisi olduğu gibi saklanır ve her görünüm kendi biçimine göre kaçışlar; veriyi iki kez dönüştürmek, çift kaçışlamanın ilk nedenidir.
ÇY18. Bu ölçüm yalnız dört karakter içindir. Tek tırnak çekirdekte kaçışlanmaz; tek tırnakla sınırlanan bir öznitelik değerine kullanıcı metni koymak bu kaçışlamayla güvenli değildir.
Eksik Yerel Değer
Şablondaki her {{ad}} yerine bir yerel değer gerekir. Çekirdeğin lambda’sı yerel değeri fetch ile okur. Bu, eksik bir anahtarın sessizce boş dizgi vermesini değil, hata vermesini sağlar.
# eksik.rb — yerel deger verilmeyen yer
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
uygulama = Uygulama.new
sablon = sablon_derle(Baglam.new(uygulama, {}), "urunler/goster", "<h1>{{ad}}</h1>")
p sablon.call(ad: "kalem")
begin
sablon.call({})
rescue KeyError => e
p e.class
end
p sablon.call(ad: "kalem", fiyat: 12)
"<h1>kalem</h1>" KeyError "<h1>kalem</h1>"
Doğru yerel değerle şablon <h1>kalem</h1> verdi. Yerel değer verilmeyince KeyError çıktı; hata iletisi basılmadı çünkü iletinin biçimi sürüme bağlıdır. Üçüncü çağrı, şablonda karşılığı olmayan fazladan bir yerel değerle çalıştı ve hata vermedi: şablon yalnız kendi yerlerini arar, fazlalığı yok sayar. Eksik değerin hata olması, fazla değerin olmaması bir tasarım dengesidir. Bir şablonu yazan, yerel değerlerin eksiksiz geldiğini ister; ama bir denetleyici şablona gereğinden çok değer geçirebilir, bu hata sayılmaz.
ÇY19. Ölçülen davranış fetch kullanan çekirdeğe aittir. Eksik değeri boş dizgi sayan bir şablon motoru, aynı şablonda hata vermeden boş bir başlık üretir.
Kısmi Şablon ve Çift Kaçışlama
Büyük bir sayfa küçük şablonlara bölünür. Bir şablonun başka bir şablonun içinde çizilen parçası kısmi şablon (partial) adını alır. Çekirdekte kısmi şablon için ayrı bir yardımcı yoktur; derlenmiş bir şablon başka bir şablonun yerel değerine verilebilir. Aşağıdaki yardımcı bu dersin kendi saf Ruby yardımcısıdır ve çekirdeğe eklenmez.
# kismi.rb — kismi sablonun ciktisini dis sablona vermek
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
def kismi(b, ad, yerel) = b.uygulama.depo[:sablonlar].fetch(ad).call(yerel)
uygulama = Uygulama.new
b = Baglam.new(uygulama, {})
sablon_derle(b, "urunler/satir", "<li>{{ad}}</li>")
sablon_derle(b, "urunler/liste", "<ul>{{govde}}</ul>")
satir = kismi(b, "urunler/satir", ad: "kalem")
p satir
p kismi(b, "urunler/liste", govde: satir)
p kismi(b, "urunler/liste", govde: kacisla("a&b"))
"<li>kalem</li>" "<ul><li>kalem</li></ul>" "<ul>a&amp;b</ul>"
Kısmi şablon <li>kalem</li> verdi. Bu çıktı dış şablona yerel değer olarak verilince dış şablon onu da kaçışladı ve liste <ul><li>kalem</li></ul> oldu: sayfada kullanıcı etiketi değil, etiketin yazısını görür. Çekirdeğin modelinde bir dizginin zaten çizilmiş ve güvenli olduğunu işaretlemenin yolu yoktur; her yerel değer, kaynağı ne olursa olsun, kaçışlanır. Üçüncü satır aynı sorunun öteki yüzünü gösterir: değer çağıran tarafta önceden elle kaçışlanırsa dış şablon onu bir kez daha kaçışlar ve & karakteri yeniden değişir. Çift kaçışlamadan kurtulmanın yolu, değeri hiç kaçışlamadan vermek değildir; o durumda çekirdek yine kaçışlar. Kısmi şablonun çıktısı bu yüzden dış şablona yerel değer olarak taşınamaz; çıktıyı bu modelde dış şablonun kaynağına gömmek gerekir.
ÇY20. Gerçek şablon motorları çizilmiş çıktıyı güvenli diye işaretlemenin bir yolunu sunar ve çift kaçışlama sorununu o yolla çözer. Çekirdek bunu modellemez; bu yüzden ölçülen davranış, işaret taşıyan bir motorda görülmez.
Yerel Değerin Türü
Şablon yerel değer olarak dizgi bekler, ama kacisla her girdiyi önce to_s ile dizgiye çevirir. Dizgi olmayan değerlerin sayfaya nasıl girdiğine bakmak, denetleyicinin şablona ne verebileceğini gösterir.
# tur.rb — dizgi olmayan yerel degerler
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
p kacisla(:ad)
p kacisla(1.5)
p kacisla([1, "<"])
p kacisla({ a: 1 })
"ad"
"1.5"
"[1, "<"]"
"{a: 1}"
:ad değeri ile ondalık sayının çıktısı beklenen biçimdedir. Dizi ve karma yapı ise Ruby’nin kendi gösterimleriyle dizgiye çevrildi ve ardından kaçışlandı: dizideki " karakterleri ", < karakteri < oldu. Sayfa bu durumda kullanıcıya Ruby’nin iç gösterimini verir, ki bu çoğunlukla istenen çıktı değildir. Denetleyici şablona gösterilecek metni hazır vermelidir; şablon yalnız dizgiyi kaçışlar, biçimlendirme yapmaz.
Bir Şablon, Çok Çizim
Derlenmiş şablon uygulama deposunda tek bir kayıt olarak durur. Aynı şablonun farklı yerel değerlerle çağrılması depoyu büyütmez; çizilen çıktı da saklanmaz.
# cizim.rb — bir sablon, uc cizim
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
uygulama = Uygulama.new
b = Baglam.new(uygulama, {})
sablon_derle(b, "urunler/goster", "<h1>{{ad}}</h1>")
depo = uygulama.depo[:sablonlar]
p depo["urunler/goster"].call(ad: "kalem")
p depo["urunler/goster"].call(ad: "defter")
p depo["urunler/goster"].call(ad: "silgi")
p depo.size
p depo.keys
"<h1>kalem</h1>" "<h1>defter</h1>" "<h1>silgi</h1>" 1 ["urunler/goster"]
Üç çağrı üç ayrı çıktı verdi ve hiçbiri ötekini etkilemedi: yerel değerler her çağrıda lambda’ya girdi olarak geçer, lambda’nın içinde durmaz. Depoda yine tek kayıt vardı ve anahtarı şablonun adıydı. Çizim bu yüzden bu istek kalışındadır: çıktı yalnız çağrıyı yapan koda döner, hiçbir yere bırakılmaz. Kalıcı olan yalnızca şablonun derlenmiş hâlidir.
Şablon Adı ve Arama
İlk derste şablon adının denetleyici ve eylem adından türediğini gördük. Bu adın bir anlamı yok değildir: depodaki derlenmiş şablonlar bu adla aranır. Adı üreten sözleşme ile adı arayan depo arasında bir bağ vardır, ama bu bağ yalnız adın eşleşmesiyle kurulur.
# arama.rb — turetilen adla sablon aramak
require_relative "kalis"
# Sozlesme: sablon adi yazilmaz, denetleyici ve eylem adindan turetilir.
def sablon_adi(params) = "#{params[:denetleyici]}/#{params[:eylem]}"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
uygulama = Uygulama.new
b = Baglam.new(uygulama, {})
depo = uygulama.depo[:sablonlar]
ad = sablon_adi(denetleyici: "urunler", eylem: "goster")
p depo[ad]
begin
depo.fetch(ad)
rescue KeyError => e
p e.class
end
sablon_derle(b, ad, "<h1>{{ad}}</h1>")
p depo.fetch(ad).call(ad: "kalem")
nil KeyError "<h1>kalem</h1>"
Şablon derlenmeden önce depo[ad] nil verdi; fetch ise KeyError fırlattı. Hangisinin seçileceği, eksik şablonun sessizce mi yoksa bir hatayla mı belirtileceğini seçer. Derlemeden sonra aynı ad şablonu buldu ve çizim yapıldı. Sözleşme adı üretir, depo adı arar; birinin adı üretmesi ötekinin o adla bir şablon derlediği anlamına gelmez. ÇY21. Ölçüm tek bir ad içindir ve adın üretildiği ve derlendiği anda tutarlı olduğunu varsayar.
Derlenmiş Şablonun Kalışı
Şablon bir kez derlenir ve sonuç uygulama deposuna yazılır. Derlenmiş şablonun hangi isteğe kadar okunduğunu çekirdeğin sınıflandırıcısına sorabiliriz.
# gorunumler.rb — derlenmis sablonun kalisi
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
yaz = ->(b) { sablon_derle(b, "urunler/goster", "<h1>{{ad}}</h1>") }
oku = ->(b) { b.uygulama.depo[:sablonlar]["urunler/goster"]&.call(ad: "<b>kalem</b>") }
p kalis(yaz, oku)
["kalici", "A1 A2 A3 B4", "<h1><b>kalem</b></h1>"]
Sonuç ["kalici", "A1 A2 A3 B4", "<h1><b>kalem</b></h1>"] oldu. Şablon dört okumanın dördünde de vardı, ve B istemcisi de onu okudu. Kalış kalıcıdır, çünkü şablon istekte değil uygulama deposunda durur. Üçüncü sütun aynı zamanda kaçışlamanın etkisini gösterir: yerel değer olarak verilen <b>kalem</b> çıktıda etiket değil metin oldu.
Burada ölçülen şey çizilmiş çıktı değildir; çizim her çağrıda yeniden yapılır. Kalıcı olan, şablonun derlenmiş hâlidir. Bu fark önemlidir: derleme pahalı bir iştir ve bir kez yapılır, çizim ise ucuzdur ve her istekte tekrarlanır.
Sınırlayıcı Ölçüm
Derlemenin bir kez yapılması, şablon kaynağı değiştiğinde yeni kaynağın görülmeyeceği anlamına da gelir. Çekirdek aynı ad için ikinci bir derlemede mevcut şablonu korur.
# sinir.rb — ayni adla ikinci derleme eski sablonu korur
require_relative "kalis"
# Gorunum: sablon bir kez derlenir; yerel degerler her cizimde kacislanir.
def kacisla(metin) = metin.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
def sablon_derle(b, ad, kaynak)
b.uygulama.depo[:sablonlar][ad] ||= lambda do |yerel|
kaynak.gsub(/\{\{(\w+)\}\}/) { kacisla(yerel.fetch(Regexp.last_match(1).to_sym)) }
end
end
uygulama = Uygulama.new
b = Baglam.new(uygulama, {})
depo = uygulama.depo[:sablonlar]
sablon_derle(b, "urunler/goster", "<h1>{{ad}}</h1>")
p depo["urunler/goster"].call(ad: "a")
sablon_derle(b, "urunler/goster", "<h2>{{ad}}</h2>")
p depo["urunler/goster"].call(ad: "a")
depo.delete("urunler/goster")
sablon_derle(b, "urunler/goster", "<h2>{{ad}}</h2>")
p depo["urunler/goster"].call(ad: "a")
"<h1>a</h1>" "<h1>a</h1>" "<h2>a</h2>"
İkinci derleme yeni kaynağı görmedi: şablon hâlâ <h1>a</h1> verdi. Çekirdek, adı daha önce derlenmiş bir şablonu yeniden derlemez; yeni kaynak sessizce yok sayılır. Şablon depodan silindikten sonra aynı çağrı <h2>a</h2> verdi. Yani derlenmiş şablonu yenilemenin tek yolu, onu depodan çıkarmaktır. ÇY22. Bu ölçüm bellekte tutulan bir depo içindir; kaynak dosyadan okunan bir çerçevede değişikliği algılama ayrıca tasarlanır.
Özet
- Şablon motoru her yerel değeri kaçışlar; kaçışlama dört karakteri değiştirir ve iki kez uygulanırsa
&lt;gibi bir çıktı verir. - Eksik bir yerel değer
KeyErrorverir; fazladan bir yerel değer yok sayılır. - Kısmi şablonun çıktısı dış şablona yerel değer verilirse yeniden kaçışlanır; çekirdekte çizilmiş çıktıyı güvenli diye işaretlemenin yolu yoktur.
- Bu dersin kalışı kalıcıdır: derlenmiş şablon uygulama deposunda durur ve bütün isteklerde, bütün istemcilerde okunur.
- Aynı adla ikinci derleme eski şablonu korur; yenilemek için şablonun depodan silinmesi gerekir.
Sonraki Adım
Denetleyici bildirimi taşıdı ve görünüm şablonu uygulama deposunda tuttu; kaydedilen ürün de aynı depoda duruyor. Bir sonraki konu bu depoyu açar: kaydedilen bir ürünün başka bir tarayıcıda da görünmesi için verinin nerede durması gerektiğini ve model katmanının bu kalışı nasıl kurduğunu ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.