Herkese merhaba,
Bu yazıda dinamik web sayfalarının nasıl hazırlanabileceğinden bahsedelim biraz. Bunun için basit bir proje seçtim, konuyu bu proje üzerinden anlatacağım. Bahsettiğim proje şu: Bir web uygulaması tasarlayacağız. Bu uygulamanın sadece bir sayfası ve bu tek sayfanın da bir <textarea>
ve bir <button>
elementi olacak. <textarea>
'ya yazı yazacağız sonra, <button>
ile bu yazıyı server’a göndereceğiz. Sonra server’a gönderilen yazının kod kısımlarına kod görünümü kazandıracağız yani metni yeniden biçimlendirip yazıyı kullanıcıya göndereceğiz. Gelen yanıta göre bu tek sayfa üzerinde yeni HTML elementleri oluşacak.
Web sunucusu için flask
kütüphanesini, yazacağımız metinlere kod görünümü kazandırmak için pygments
ve markdown
kütüphanelerini kullanacağız.
Önce gerekli kütüphaneleri yükleyelim.
pip install flask, markdown, pygments
Şimdi, öncelikle bu basit projenin dosya düzenini göstereyim, siz de aşağıdakine benzer bir dizin ağaç yapısı oluşturun isterseniz:
.
├── app.py
├── static
│ └── js
│ └── main.js
├── templates
│ └── main.html
└── utils.py
3 directories, 4 files
Öncelikle app.py
’yi oluşturalım, sonra da sırayla diğer dosyaları oluşturalım.
app.py’de basit bir route
tanımlayalım.
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def main():
return render_template("main.html")
if __name__ == "__main__":
app.run()
Yukardaki kodları herhalde açıklamama gerek yoktur diye tahmin ediyorum. Az önce projeyi tanıtırken <button>
yardımıyla server’a yazı göndereceğimizi söylemiştim. Server’a bir veri göndermek istediğimiz zaman bir XMLHttpRequest
nesnesi kullanırız ve bu nesne istemci tarafında çalışır. Farklı XMLHttpRequest
fiilleri var, biz bu örnekte POST
fiilini kullanacağız. O halde, istemciden gelen talebi alabilmek için yukardaki app.py
dosyasındaki main route
’u için izin verilen http metodlarını tanımlayalım.
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def main():
return render_template("main.html")
if __name__ == "__main__":
app.run()
POST
metodu ile değişik türden veriler alabiliriz. Ne türden veriler istediğimizi POST isteğine ekleyeceğimiz FORM
nesneleri ile belirteceğiz. Sunucu da gelen formun anahtar kelimelerine göre ne yapması gerektiğine karar verecek. Bu örnekte, formun içinde create
isimli bir anahtar olacak. Sunucu bu anahtar vasıtasıyla gelen isteğin nasıl değerlendirilmesi gerektiğini belirleyecek.
Şimdi, yukardaki kodları güncelleyelim biraz:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def main():
if request.method == "POST":
if "create" in request.form:
pass
return render_template("main.html")
if __name__ == "__main__":
app.run()
Evet, bu kısımda da anlaşılmayan bir kısım yoktur diye tahmin ediyorum. Yukarda anlattıklarımın koda dökülmüş hali sadece. app.py
şimdilik böyle kalsın, sıradaki dosyayı oluşturmaya geçelim. O dosyayı oluşturduktan sonra app.py
’ye tekrar döner ve kalan kısımları tamamlarız.
Şimdi, dinamik web sayfası içeriklerinin oluşmasını sağlayacak olan js
kodlarını yazacağız. Bu örnekte responsive
html içerikleri oluşturmak için bootstrap
sınıflarını kullanacağım.
JS dosyamızda, göndereceğimiz metni bir sınıf ile gösterelim; bu sınıftan örnek oluşturduğumuz zaman, html elementi oluşsun. Aşağıdaki js
classını inceleyin lütfen.
class Post {
constructor(id, parent, content) {
var div = document.createElement("div");
div.id = `container-${id}`;
div.className = "card-body container text-light rounded";
div.style.backgroundColor = "black";
var p = document.createElement("p");
p.id = `content-${id}`
p.innerHTML = content;
div.append(p);
document.getElementById(parent).append(div);
}
}
Burada yaptığımız şey de sanıyorum çok karışık değil. Post
isimli bir sınıf tanımladık. Sınıfın kurucu fonksiyonu constructor
üç adet parametre alıyor. Kurucu fonksiyon altında da her bir yazı için oluşacak olan html elementlerini tanımladık. Son olarak da oluşturacağımız html element grubunu parent
ne olacaksa ona ekledik. class
yerine function
da kullanabilirdik elbette.
Sınıfı oluşturduğumuza göre, istemci ile sunucu arasındaki iletişimi sağlayacak mekanizmayı tanımlayalım şimdi. Yukarda bahsettiğimiz <button>
nesnesine id
olarak btn-send
yazacağım. Bu buttona bastığımz zaman istemcinin sunucuya bir XMLHttpRequest
göndermesi gerekiyor. Henüz html
sayfasını oluşturmadık ama js
dosyasını doldurmaya devam edebiliriz. Şimdi bu button için onclick
olayında çalışacak bir fonksiyon yazıyorum.
static/js/main.js
class Post {
constructor(id, parent, content) {
var div = document.createElement("div");
div.id = `container-${id}`;
div.className = "card-body container text-light rounded";
div.style.backgroundColor = "black";
var p = document.createElement("p");
p.id = `content-${id}`
p.innerHTML = content;
div.append(p);
document.getElementById(parent).append(div);
}
}
document.getElementById("btn-send").onclick = function (e) {
var form = new FormData();
form.append("create", true);
form.append("content", document.getElementById("textarea").value);
fetch("/", {
method: "POST",
body: form
})
.then(function(response) {
if (response.status === 201) {
return response.json();
} else {
throw new Error("Request failed.");
}
})
.then(function(post) {
new Post(post.id, "posts", post.content);
})
.catch(function(error) {
console.error(error);
});
}
İzninizle yukardaki kodları açıklayayım biraz. Post
sınıfını zaten tanıtmıştık, tekrar tanıtmaya gerek yok. Post
’un hemen altında, id
değeri btn-send
olacak olan bir <button>
için onclick
fonksiyonunun nasıl olması gerektiğini tanımladık. Bu fonksiyonun içinde önce yeni bir Form
nesnesi oluşturduk. Sonra form
nesnesinin anahtarlarını ve bu anahtarların değerlerini yazdık. Hatırlayın, sunucu tarafında if "create" in request.form:
gibi bir sorgu yazmıştık. İşte yukardaki formun içinde create
isimli bir anahtarın olma sebebi bu. Değerinin true
veya false
olmasının bu proje için bir önemi yok ama server’a göndereceğimiz formun içinde create
isimli bir anahtar olmalı çünkü server tarafında bu anahtara uygun bir işlem var. Yazının içeriğini <textarea>
elementinin değerinden alarak forma ekliyoruz. Sonra da formu sunucuya fetch
metodu ile gönderiyoruz. Sunucu, gelen isteğe göre uygun bir yanıt gönderecek (yanıta dair olan kodları server
tarafında henüz yazmadık, birazdan yazacağız.). Sonra da bu yanıt önce bir response.json()
nesnesine dönüştürülecek sonra da bu nesnenin nitelikleri kullanılarak sayfa içinde yeni bir Post
nesnesi oluşturulacak.
Bu proje için js kodları bu kadar; bir Post
sınıfı ve <button
’un onclick
olayı için çalışan bir fonksiyon tanımladık.
Şimdi app.py
’nin geri kalan kısımlarını tanımlamaya devam edelim. Aşağıdaki kodları inceleyin lütfen:
from flask import Flask, render_template, request, Response, json
app = Flask(__name__)
datas = []
@app.route("/", methods=["GET", "POST"])
def main():
if request.method == "POST":
if "create" in request.form:
data = {
"id": len(datas),
"content": request.form["content"]
}
datas.append(data)
return Response(json.dumps(data), 201)
return render_template("main.html")
if __name__ == "__main__":
app.run()
Burada, daha önce yazdığımız app.py
’den farklı olarak, Response
ve json
kütüphanelerini programın içine aktardık sonra da data
isminde bir sözlük oluşturup, sözlüğü datas
isimli bir list
eye ekledik. Bu liste veritabanını temsil ediyor. Gerçek bir projede, list
yerine gerçek bir veritabanı sistemi kullanmalıyız.
Şimdi gelin templates/main.html
sayfasını oluşturalım:
templates/main.html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div>
<div id="form" class="input-group mb-3 container rounded mx-auto w-100">
<textarea id="textarea" class="form-control"></textarea>
<button id="btn-send" class="btn btn-outline-secondary">Send</button>
</div>
<div id="posts" class="container"></div>
<script type="text/javascript" src="{{ url_for('static', filename='js/main.js') }}"></script>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
Burada, ilk satırdaki <link>
ve son üç satırdaki <script>
bootstrap’i aktif etmek için yazıldı. Bunları çıkarırsanız, şöyle bir kod yazdık aslında:
<div>
<div id="form" class="input-group mb-3 container rounded mx-auto w-100">
<textarea id="textarea" class="form-control"></textarea>
<button id="btn-send" class="btn btn-outline-secondary">Send</button>
</div>
<div id="posts" class="container"></div>
<script type="text/javascript" src="{{ url_for('static', filename='js/main.js') }}"></script>
</div>
Buradaki html elementlerinin class
nitelikleri bootstrap
sınıflarıdır dolayısıyla her birinin oluşturacağımız html elementlerinin görüntüsüne katkısı var. Hangi bootstrap sınıfına ihtiyacınız olduğunu, bootstrap sınıflarını incelemeden karar veremezsiniz. Bootstrap’in resmi web sayfasında oldukça ayrıntılı bir şekilde her sınıfın ne türden bir özelliğe sahip olduğu yazıyor. İncelemenizi tavsiye ederim. Neyse açıklamaya devam edelim.
Gördüğünüz gibi, dışta bir <div>
ve bu div
’in iki tane child
elementi var. Bu elementlerin tipleri de <div>
. Divlerden birisinin id
değeri form
; <textarea>
elementini ve yazıyı gönderirken kullanacağımız <button>
elementini içeriyor; diğerinin id
değeri de posts
; oluşacak olan gönderileri içerecek olan div
bu. Son satırda da yukarda yazdığımız main.js
dosyasını template içine aktarıyoruz.
Aslında projemiz bitti sayılır. Yani şu haliyle istemci tarafında yazı yazıp sunucuya gönderebilir, sunucu tarafından da yanıtı alıp html sayfasının değişmesini sağlayabiliriz. Ancak bu projede, yazacağımız yazılara kod görünümü kazandırmaktan bahsetmiştim. Şimdi gelin bunun kodlarını oluşturalım.
Bir çırpıda kod görünümü kazandıran kodların tamamını paylaşayım, açıklamasını sonra yapayım:
utils.py
import re
from markdown import markdown
from pygments import highlight
from pygments.styles import get_all_styles
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
class MetaHTMLCodeFormat(type):
def __call__(cls, text: str):
if not isinstance(text, str):
return
return super().__call__(cls.reformat(text))
def reformat(cls, text: str):
d_container = "<div class=\"container\">\n"
d_flex = "<div class=\"d-flex\">\n"
d_rows = "<div class=\"bg-dark pt-2 pl-2 pr-2 rounded-left text-light\">\n"
d_close = "\n</div>\n"
d_code = "<div class=\"bg-dark pt-2 pl-2 pr-2 rounded-right container\">\n"
patterns = re.findall(f"```(?:(?!```).)*```", text, re.DOTALL)
for code in sorted(set(patterns), key=patterns.index):
html = "".join(
[
d_container,
d_flex,
d_rows,
"```\n",
*map(lambda i: f"{i + 1}\n", range(len(code.split("\n")) - 2)),
"```\n",
d_close,
d_code,
code,
d_close,
d_close,
d_close
]
)
text = text.replace(code, html)
return text
class HTMLCodeFormat(str, metaclass=MetaHTMLCodeFormat):
def __init__(self, text: str):
super().__init__()
def highlight(self, style: str = "github-dark"):
if style not in get_all_styles():
return self
return "".join(
[
markdown(self, extensions=["fenced_code", "codehilite"]),
"<style>",
HtmlFormatter(style=style, full=True, cssclass="codehilite").get_style_defs(),
"</style>"
]
)
Yukardaki proje ağaç yapısında, utils.py
dosyası, app.py
ile aynı dizinde yer alıyor demiştik. Açıklamayı birazdan yapacağım ama önce app.py
’ye de son şeklini verelim:
app.py
from flask import Flask, render_template, request, Response, json
from utils import HTMLCodeFormat
app = Flask(__name__)
datas = []
@app.route("/", methods=["GET", "POST"])
def main():
if request.method == "POST":
if "create" in request.form:
data = {
"id": len(datas),
"content": request.form["content"]
}
datas.append(data)
data["content"] = HTMLCodeFormat(data["content"]).highlight()
return Response(json.dumps(data), 201)
return render_template("main.html")
if __name__ == "__main__":
app.run()
Evet, bu küçük projenin bütün dosyaları bu kadar. Şimdi utils.py
’nin içindeki kodları açıklamaya koyulabilirim:
Öncelikle metaclass
’ın ne olduğunu bilmiyorsanız aşağıdaki başlığı okumanızı tavsiye ederim.
Ancak, yukardaki metaclass
’ı ve class
’ı fonksiyon olarak da kullanabilirdik elbette. Burada bu classların kafanızı karıştırmasını istemem, mesela reformat
fonksiyonunu MetaHTMLCodeFormat
sınıfından, highlight
fonksiyonunu da HTMLCodeFormat
sınıfından dışarı çıkarıp kullanabilirsiniz. Özelleştirilmiş bir str
sınıfı oluşturmak istemiştim sadece. Meta sınıfların nasıl kullanılabileceğine dair bir örnek olarak da görebilirsiniz.
Neyse, bu sınıf kısmıyla alakalı açıklamayı yaptıktan sonra açıklanması gereken kod parçalarından bahsedeyim.
MetaHTMLCodeFormat
sınıfının reformat
fonksiyonuna bakalım. Burada aşağıdaki gibi bir ifade görüyorsunuz.
patterns = re.findall(f"```(?:(?!```).)*```", text, re.DOTALL)
Dıştan içe doğru gidelim:
re.findall
ile bir str
içindeki belirli bir örüntüyü ararsınız. str
içinde bu belirli örüntüye uygun kaç tane parça varsa o parçalar bir liste olarak geri döndürülür.
Burada aradığımız örüntü f"```(?:(?!```).)*```"
örüntüsüdür. Burada str
içinde üç çentik ile başlayan, üç çentik ile biten ama arasında üç çentik hariç herşeyi içerebilen bir örüntüyü arıyoruz. Bu regex kalıbında ünlem işareti ve iki nokta üst üste işaretinin kullanılabilmesi için re.DOTALL
ifadesinin, re.findall
fonksiyonuna argüman olarak verilmesi gerekir.
(?:)
→ Örüntünün bir veya daha fazla tekrarını arar ama grubun içindeki benzer patternleri bulmaz.
Örnek yapalım:
import re
assert re.findall("(?:\d{2})", "1213a1234", re.DOTALL) == ["12", "13", "12", "34"]
assert re.findall("(?:\d{2}-)+\d{2}", "12-13-a-12-34", re.DOTALL) == ["12-13", "12-34"]
assert re.findall("(?:\d{2}-)+\d{2}", "12-13-14-a-12-34-36", re.DOTALL) == ["12-13-14", "12-34-36"]
(?!)
→ Negatif bir anlamı var. Örüntü aramasında bu kalıba uyan örüntüler yok sayılır.
Örnek yapalım:
import re
assert re.findall("(?:\d)+(?!\s)(?:\d)+", "1213a1234", re.DOTALL) == ["1213", "1234"]
+
işareti, 1
veya daha fazla eşleşme; *
işareti ise, 0
veya daha fazla eşleşme için kullanılır. \d
sayının; \s
ise, str
’nin yerini tutar.
Umarım aşağıdaki ifadenin ne anlama geldiği şimdi daha iyi anlaşılıyordur:
patterns = re.findall(f"```(?:(?!```).)*```", text, re.DOTALL)
Daha sonra da patterns
listesinden özgün elemanları olan bir liste oluşturduk. Burada set
kullandım çünkü kodun ilerleyen kısımlarında text = text.replace(code, html)
gibi bir ifade var. Eğer sadece özgün elemanlar olmasaydı, tekrar eden kod
satırları replace
edilirken tekrar sayısı kadar replace
gerçekleşirdi. Bu da bizim istemediğimiz bir senaryo. Varsa tekrar eden kod yapıları, tek seferde hepsi değişsin diye özgün elemanlar kullanıyoruz.
Sonra da for
döngüsüne giriyoruz. Burada da yazdığımız yazıda kod yapısına uyan örüntüler yeniden biçimlendirilir ve yazdığımız yazıdaki örüntü ile bu yeni biçim yer değiştirilir.
En sonda da highlight
fonksiyonuyla içeriğimizi yeniden biçimlendirip, html’de kod görünümü kazanacak hale getiriyoruz.
Sonuç olarak aşağıdaki gibi dinamik bir web sayfa elde ediyoruz.
Button’a basmadan önce:
Button’a bastıktan sonra:
Button’a basmadan önce:
Button’a bastıktan sonra:
Evet, bu örnekte, dinamik web sayfası hazırlamak için sunucu ile istemci arasında iletişim kurduk, sunucu tarafında istenen veriyi hazır hale getirip istemci tarafına tekrar yolladık. İstemcinin gördüğü web sayfası sunucudan gelen cevaba göre değiştirildi. Bu değiştirme işlemi için html elementlerini js
’de dinamik olarak tanımladık. Sonuç olarak yazdığımız yazıyı biçimlendirerek sayfaya ekleyen bir mekanizma kurduk. Benzer şekilde, eklediğimiz yazıyı silebileceğimiz veya değiştirebileceğimiz mekanizmaları da oluşturabiliriz.
Not: Web uygulamasını tasarlarken, istemcinin sunucuya belirli süreler için en fazla kaç tane istek gönderebileceğini belirleyen sınırları ve html elementlerini değiştirme yetkisinin her kullanıcıda mı yoksa belirli kullanıcılarda mı olacağını düşünmeliyiz.
Bu başlık için anlatacaklarım şimdilik bu kadar. Umarım faydası olur.
Herkese iyi günler.