dersblog

Javascript

Tamamen istemci, tarayıcı üzerinde işleyen kodlara ihtiyaç varsa Javascript kodlaması yaparız. Javascript kodları HTML içine bile gömülebilir, ve bu kodlar içinde olduğu HTML öğelerine erisebilirler. Bu sayede servis tarafında üretilmiş bir HTML gönderilmiş olsa bile hala o HTML üzerinde değişiklik yapılabilir. Bir girdi kutusuna girilen bilgi yeni bir liste yaratılmasını sağlayabilir mesela, her türlü ekleme, çıkarma, düzeltme işlemi kullanıcı tarafında halledilebilir.

Geliştirme Ortamı

Javascript geliştirme her zaman bir uygulama servisi gerektirmeyebilir sonuçta bir HTML kodlanıyor ve bu kodlar tarayıcıda düz dosya olarak yüklenebilirler, fakat biz gene de bir tane başlatalım. Python bilenler için en iyisi Flask, bir static dizini yaratılabilir, ve düz HTML dosyaları buraya koyulur, ve http://localhost:8080/static/dosya.html şeklinde erişilebilir.

Javascript ne zaman, nerede yüklenir, nasıl çağrılır?

Sablon olarak bir HTML suna benzeyebilir,

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  </head>  
  <script>
    function foo() {
        document.getElementById("output").innerText = "vs vs vs";
    }
  </script>    
  <body onload="foo()">
    <div id="output">
    </div>  
  </body>

</html>

Bu dosyada bir JS fonksiyonunu HTML içine gömdük, bu fonksiyon HTML yüklenir yüklenmez çağrılacaktır. Fonksiyon foo yu body onload çengeline takarak bunu yapmış olduk. Çengel derken İngilizce hook kavramına atıf yapılıyor, yani beni arama ben seni ararım tekniği, biz çağrılmasını istediğimiz fonksiyonu altyapıya veriyoruz (çengele takıyoruz), onun ne zaman çağrılacağını biz kontrol etmiyoruz, yeri gelince altyapı onu çağırıyor.

Kodlamayı direk sayfa içine Javascript gömerek, ya da ayrı bir js dosyasını sayfaya dahil ederek yapabiliriz. İkinci yöntem kod idaresi açısından daha rahattır. HTML içine

<script src="funcs.js"></script>

koyunca funcs.js otomatik olarak dahil edilecektir. Fakat dikkat, eğer tarayıcı önbelleklemesi (cache) açık ise, ki olağan durum budur, js dosyasında yapılan değişiklikler HTML tarayıcıda tekrar yüklense bile etki etmeyebilir, o zaman Developer tools, Network ve oradan Disable cache seçimi yapılırsa önbelleklenme kapatılmış olur, kod her seferinde tekrar yüklenir. Tabi Web'de her kullanıcının bunu yapmasını bekleyemeyiz, o zaman yeni kod sürümü yapacaksak yeni kod için yeni bir js dosya ismi kullanmak bir çözüm olabilir.

Log

Eğer kod işletimi sırasında bazı değerleri log bırakmak açısından düz metin olarak basmak istiyorsak, bunu console.log(..) ile yapabiliriz. Çıktıları görmek için Chrome içinde üst sağ köşede tıklama yapıp More tools ve Developer tools seçimi yaparız. Bu araç tarayıcının sağ kenarında çıkar, üstteki tab içinde Console seçimi yaparak log çıktılarını görmek mümkündür.

Bir diğer mesaj basma yöntemi alert çağrısı, fakat bu çağrı bir diyalog kutusu yaratır, tıklama yapıp kapatmak gerektiği için her yerde kullanılmıyor.

Node

Görsel kodlamada ilerlemeden önemli bir konuya değinelim, çünkü ileride JS kodlarını test etmek için faydalı olabilir. Node teknolojisi ünlü, bu servis tarafında Javascript anlamına geliyor. V8 motoruyla beraber JS kodlarının işletimi hızlandı, ve bazıları da düşündü ki "ben motoru alıp servis tarafında koştururum, böylece hem görsel hem servis kodlarını aynı dilde yazabilmiş olurum". Node buradan çıktı. Biz şu anda servis tarafı kodlamasını işlemiyoruz, fakat pür istemci için olsa bile bazı Javascript kodları hala görsel olmayan mantık içerebilir, ve bu kodları ayırıp, node üzerinden test etmek mümkündür, böylece sürekli tarayıcıyı açmak gerekmez, komut satırından Javascript test edebiliriz.

Kurmak için,

sudo apt install nodejs

Simdi mesela bir test1.js icinde

function add(a, b){
    return a+b;
}

console.log(add(3,4));

Bu kodu

node test1.js

ile işletince 7 değeri basılacaktır. Dikkat edersek görsel kod içinde kullanılan aynı console.log çağrısı var, ve bu çağrı otomatik olarak komut satırı ekranına çıktıyı basacağını bildi.

Node kodlarından dosya yüklemek bile mümkün,

const fs = require('fs')

file = fs.readFileSync("/home/user1/dir/dosya.json", 'utf8');
const res = JSON.parse(file);

Tabii üstteki kod görsel kodlama için uygun değil çünkü tarayıcı içindeki Javascript yerel dizindeki dosyalara erisemez, İnternet üzerinden dosya okumak için XMLHttpRequest gerekir, o konuya geliyoruz, fakat test amacıyla üstteki çağrı hala faydalıdır.

Sayfa bazında Javascript'in src include ile yüklediği dış kütüphaneleri Node ile require kullanarak alabiliriz. Mesela [2]'deki matematik kodlarını kullanmak istiyorum, gerekli math.js dosyasını indirip

var math = require('./math.js');
let N = 20;
A = math.round(math.random([N,N]),5);
B = math.round(math.random([N,N]),5);

C = math.multiply(A, B);

gibi bir kod işletebilirim. Üstte lineer cebir matris çarpım işlemi yapmış olduk.

Eğer kendim dahil edilebilir kodlar yazmak istiyorsam, mesela util.js içinde bir add fonksiyonumun dışarıdan dahil edilebilir olmasını istiyorsam,

function add(x,y) {
  return x+y;
}
function subtract(..,...) {
  return x-y;
}
module.exports = {
    add: add,
    subtract: subtract
};

Artık bu modülü üstteki tarif edildiği gibi dahil edip kullanabilirim,

var u = require("./util.js")
u.add(...)

Çengeller

Javascript fonksiyonları her türlü kullanıcı aksiyonu sayesinde çağrılabilir. Mesela bir URL bağlantısına tıklanınca bir fonksiyon çağrılsın istiyorsak a href='#' onclick='func2()...` diyebiliriz. Diğer pek çok çengel noktası vardır bunlar bir HTML referansından öğrenilebilir.

Temel Görsel İşlemler

Javascript içinden HTML sayfasının görsel öğelerine erişilebilir demiştik, bu öğelerden bilgi alınabilir, ve geri bilgi yazılıp görüntüde değişim yapılabilir.

HTML içinde id=".." ile işaretlenen etiketlerin, mesela <div> etiketi olsun, ana objesini kimlik değeri geçerek document.getElementById(..) ile alabiliriz, sonra bu obje üzerinde .innerText ile içerik değişimi yapabiliriz. Sadece düz metin değil HTML de geçmek mümkündür, bunun için .innerHTML.

Eğer bir metin giriş kutusu var ise, kimliği myInput olsun,

<form ..>
   <input id="myInput" type="text" name="myCountry" placeholder="Movie"/>
   <button onclick="func2()">Ok</button>
</form>

Düğmeye basılınca bir func2() Javascript çağrısı yapılmasını sağlarız, bu fonksiyon içinden

deger = document.getElementById("myInput").value;

ile kutudaki değeri okuyabiliriz.

Temel Programlama Yapıları, Kapsam

Değişken tanımlarken onların kapsamını (scope) iyi bilmek lazım, yani değişken hangi bölgelerde tanımlıdır, bir bölümde set edilince diğer yerde görünebilir mi, değer alımı yapılabilir mi?

Javascript'in olağan davranışı eğer değişken kapsamı tanımlanmamışsa, onun her yerde (global) kapsamı olmasıdır. Mesela

function func1() {
    console.log('in func1');    
    var1 = "aaaaaa";
}

function func2() {
    console.log('in func2');    
    console.log(var1);
}


func1();

func2();

script'i işleyince func1 içinde set edilen var1 değeri basılır. Bir fonksiyon içinde tanımlanan değer diğerinde görülebilmiştir.

Diğer kapsamlar let ve var ile yapılır, bunlardan birincisi değişkeni içinde olduğu kapsama sınırlar, diğeri içinde olduğu fonksiyona sınırlar. Mesela üstteki kodda func1 içinde let var2 = "bbbbbb" tanımlasam ve func2 içinden erişmeye uğraşsam hata mesajı alırım.

Javascript tiplemesi dinamiktir, yani statik olan C++ gibi dillerin tersini derleme aşaması yoktur ki bu aşamalarda bazı diller tip uyuşması şartı arayabiliyor, mesela a == b testi yapıyorsam derleyici a ve b tiplerinin uyumlu olma şartını isteyebilir. JS ile derleme süreci yok, hatta işlem anında JS yapabildiği kadar otomatik değişimi yapmaya uğraşır. Mesela 2 == "02" gibi bir eşitlik kontrolü "doğru" cevabını verecektir, sistem 02 metin değerini sayı yapıp 2 ile karşılaştırmasını gerçekleştirmiştir.

Anahtar bazlı hızlı erişilebilen sözlük (dictionary) yapısı pek çok diğer dilde olduğu gibi Javascript'te de mevcuttur. Yaratmak için direk kod içinde

d1 = {'a': 3, 'b': 2}

diyebilirdim, erişmek için d1['a'] çağrısı 3 değerini verir. Bir anahtarın sözlükte olup olmadığını kontrol için hasOwnProperty çağrısı var.

Sözlük değeri olarak bir liste, bir başka sözlük te kullanabilirdik. Eğer tüm anahtarları gezmek (iterate) istersem,

Object.keys(d1).forEach(function(key) {
    console.log(key);
})

Bu anahtarları kullanıp değerleri de alabilirdim muhakkak.

Listeler benzer şekilde direk kod içinde yaratılabilir, bir tane yaratalım, ve eski usul C vari bir şekilde onu gezelim,

l1 = [2, 4, 6]

for (let i=0; i<l1.length; i++) {
    console.log(l1[i]);
}

Listeler de forEach tekniği ile gezilebilir,

l1.forEach(function(key) {
    console.log(key);
})

Koşul komutları if, else tahmin edilebilecek şekilde ve C, Java da olduğu gibi işler.

Metinler (String)

Bir metin yaratmak icin yine direk kod icinde

s1 = "bir kelime";

diyebilirdik. Kelimeye ekler yapmak arti isareti kullanabilir,

s2 = s1 + " arti bir sey";

gibi. Kelimeler üzerine Python'dan tanıdık split çağrısı var, mesela

console.log(s2.split(" "));
[ 'bir', 'kelime', 'arti', 'bir', 'sey' ]

Bir metnin diğeri içinde olup olmadığını kontrol için includes, mesela

console.log("berber".includes("erb"));

çağrısı true döndürür.

Şablon kullanımı için ilginç bir sözdizimi var,

let header = "baslik";

let html = `<h2>${header}</h2><ul>`;

console.log(html);

Bu kodlar ekrana html içindeki değerleri basar ve oradaki ${header} için header değişkeninde görülen değer geçirilir. Yani html içindeki şablonda bir değer doldurması yapıyoruz, bu doldurma için geriye tek tırnak (backtick) içinde Javascript'teki değişken isimlerini kullanabiliyoruz.

JSON

Javascript'in dış dünya ile alışverisi en rahat JSON bazlı yapılır, bu sebeple kullanımını bilmek iyi olur. JSON sonuçta bir sözlük, ya da listenin metin halidir. Tabii sözluk içinde sözlük, onun içinde liste gibi istenildiği kadar çetrefil yapılari taşıyabilir bu sebeple kuvvetli bir temsil şeklidir. Dışarıdan gelen JSON formatındaki metni Javascript'te görülebilen ona karşılık olan yapılara çevirmek için JSON.parse, yapıları geri JSON metnine çevirmek için JSON.stringify kullanılır.

var json1 =  '{ "key1": "val1", "key2": "value2" }';
var json2 =  '[2,3,4,5]';

json1 = JSON.parse(json1);
json2 = JSON.parse(json2);

console.log(json1['key1']);
console.log(json2[2]);

Çerez (Cookie) Kullanımı

Çerezler sitelerin kullanıcı tarayıcısına yerel bırakabildiği 'bilgi kırıntılarıdır', bilgi notlarıdır. Çerezler sayesinde en basit statik HTML + Javascript bile kullanıcının o siteye özel bazı bilgileri kendi bilgisayarında depolamasını sağlayabilir. Mesela bir kullanıcı favori filmlerini seçer, Javascript o bilgiyi alıp bir çereze koyar, sonraki ziyaretinde kullanıcı (bizim kod üzerinden) o çereze erişir, ve tarayıcısında o bilgiyi görür. Böylece site tarafından hatırlanmış olur, bu arayüz tasarımı açısından iyi bir şeydir.

Javascript ile çerezlere erişim basittir, document.cookie ile kodladığım, kullanıcının ziyaret etmekte olduğu tüm çerezleri alabilirim, bu degiskene bir değer yazdığımda çerezi değiştirmiş olurum. Tabi belli bir formatı takip etmek iyidir, metin içinde ilk başta isim= tanımlarsak o çereze isim vermiş oluyoruz. Sonda bir ; sonrası expires=Wed, 05 Aug 2025 23:00:00 UTC gibi bir tarih vermek o çereze bir zamanaşımı veriyor, bu zamandan sonra o çerez geçersiz oluyor, ondan önce hatırlanıyor. Eğer zamanaşımı verilmezse olağan davranış çerezin silinmesidir.

Bazı çerez kullanım kalıpları şöyle olabilir, siteyi ziyaret edenlerin çerezi kontrol edilir, document.cookie.length < 1 ile, eğer çerez boş ise, hemen bir çerez atanır. Mesela alışveriş listesi (cart) için

empty = {"cart": {}}
document.cookie = 'bb=' + JSON.stringify(empty) + '; expires=Wed, 05 Aug 2025 23:00:00 UTC';

Böylece alışveriş için bir sözlük yarattım, onun JSON tanımını bir zamanaşımı ile çereze koydum. Çerez okurken

var elems = document.cookie.split("=");
movies = JSON.parse(elems[1]);

diyebilirim.

Site Çerezi

Eğer farklı sayfalardan aynı çereze erişilebilsin istiyorsam üstteki çerez metnine zamanaşımı ibaresinden sonra bir ;path=/ ifadesi ekleyebilirim, böylece çerez "site çerezi" haline gelir, ve her sayfa tüm çereze erisebilir. Benim kendi kullandığım bazı yardımcı fonksiyonlar alttaki gibidir,

all_apps = ['sayfa1','sayfa2'];

expires_path = '; expires=Wed, 05 Aug 2025 23:00:00 UTC;path=/';

function init_cookies() {

    if (document.cookie.length < 1) {
    empty = {}
    all_apps.forEach(function(app) {
        empty[app] = {};
    })
    document.cookie = 'bb=' + JSON.stringify(empty) + expires_path;
    } else {
    var elems = document.cookie.split("=");
    prefs = JSON.parse(elems[1]);    
    all_apps.forEach(function(app) {
        if (! prefs.hasOwnProperty(app)) {
        prefs[app] = {}
        }
    })
    document.cookie = 'bb=' + JSON.stringify(prefs) + expires_path; 
    }
}

function get_prefs() {
    var elems = document.cookie.split("=");
    prefs = JSON.parse(elems[1]);
    return prefs;   
}

function save_cookie(prefs) {
    document.cookie = 'bb=' + JSON.stringify(prefs) + expires_path;
}

Her sayfa başlangıcında init_cookies çağrılır, site çerezi ismi bb, bu çerez içinde all_apps de olan bölümler yoksa hemen eklenir (böylece yeni sayfa/uygulama ekleyince çerezlere hemen etki eder), ve her sayfa kendine ait bilgiye bir sözlük üzerinden erişir. Çerezleri almak için get_prefs geri yazmak için save_cookie.

Alternatif Depolama Yöntemleri

Eğer çerez kullanmak istemiyorsak ya da kullanmak mümkün değil ise (bir keresinde siteme eklediğim reklam servisinin Javascript kodu site seviyesinde ne çerez bulursa onları siliyordu, uygulamamızın çerezi yok oluyordu), alternatif depolama yöntemleri vardır. Bunlardan kalıcı olanı localStorage, oturum bazında olanı sessionStorage.

Her iki değişken her zaman kullanıma hazır durumdadır, ve anahtar / değer bazında çalışırlar. setItem('anahtar','değer') ile depolama yapılır, .getİtem('anahtar') ile depolanan değer geri alınır. Eğer localStorage kullanılmışsa tarayıcı kapatıldıktan sonra bile değerler sonraki ziyarette hatırlanır, sessionStorage ile oturum bitince değerler silinir.

Önemli nokta, her iki depolama sisteminin de metin (string) bazlı depolama yapması, yani çetrefil obje mesela dizin, sözlük gibi depolayınca onu olduğu gibi geri alabilmeyi bekleyemeyiz, o yüzden her iki durumda da depolamadan önce JSON.stringify ile metne çevirip, geri okurken de JSON.parse ile gerekli objelere çevirim yapmamız gerekir.

XMLHttpRequest

Statik Dosya

Uzaktaki bir JSON dosyasını alıp okumak istiyorsak,

url = "https://www.filanca.com/dir/file1.json";
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", url = url, false ); 
xmlHttp.send( null );
var res = JSON.parse(xmlHttp.responseText);
...

Fakat dikkat, eğer üstteki kod filanca.com sitesi dışındaki başka bir siteden geliyorsa, mesela HTML/JS kodu falanca.com'da eriştiğimiz json filanca.com'da olabilir, o zaman çoğunlukla bu erişim CORS denilen bir hata verir, her site dışarıdan gelen kodlara programsal erişim sağlamak istemiyor. En garanti olan sitenin kendi servis ettiği json dosyalarına erişmektir, bu da izafi dizin erişimi ile yapılabilir, mesela filanca.com/dir/ındex.html içindeki Javascript aynı dizindeki json dosyasına /dır/file1.json ile erişir.

Önbellekleme

JSON dosyalarına bir kez erişildiğinde bu dosyalara ikinci erişim onbellekten veriyi alır, bir daha Internet'e gidiş olmaz. Bu hem programlama hem performans açısından yardımcı olur, farklı sayfalar aynı json dosyasına erişiyorsa ikinci ve sonraki erişimler hızla işleyecektir, çetrefil onbellekleme kodlarına gerek kalmaz.

Ajax

XMLHttpRequest ile Ajax çağrısı yapmak ta mümkündür. İlk kez bu noktada bir pür uygulama servisi özelliği kullanacağız yani işlem kodlarının servis tarafında olduğu türden, üç katmanlı (three-tier) mimarinin orta katmanının gerekli olduğu yapı.

Diyelim ki bir taban yarattım (üçüncü katman) ona bir servis arayüzü üzerinden iletişim vermek istiyorum, get ile geçilen anahtar değeri tabanda bakılıp döndürülecek, set ile verilen değer ve anahtar tabana yazılacak. Servis tarafını Flask ile yazalım,

from flask import Flask, url_for, jsonify, request
import sys, os, sqlite3

app = Flask(__name__)

db = {"a": "ali", "v": "veli"}

@app.route('/get', methods=["PUT", "POST"])
def get():    
    data = request.get_json(force=True)   
    key = data['key']    
    return jsonify({'result': db[key]})

@app.route('/set', methods=["PUT", "POST"])
def set():    
    data = request.get_json(force=True)   
    key = data['key']
    value = data['value']
    db[key] = value
    print ("new db", db)
    return jsonify({'result': "OK"})

if __name__ == "__main__":
    app.run(host="localhost", port=8080)   

HTML/Javascript şöyle olabilir,

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  </head>
  <script>
  function set(key, val) {

    url = "http://localhost:8080/set";
    var xmlhttp = new XMLHttpRequest();   // new HttpRequest instance 
    xmlhttp.open("POST", url, false);
    xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
    xmlhttp.send(JSON.stringify({ "key": key, "value": val }));
  }

  function get(key) {
    url = "http://localhost:8080/get";
    const xhr = new XMLHttpRequest()
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        const response = JSON.parse(xhr.responseText)
        console.log(response)
      }
    }

    xhr.open('POST', url)
    xhr.setRequestHeader('Content-Type', 'application/json')
    xhr.send(JSON.stringify({"key": key}))
  }

  function foo() {
      //set("ah","ahmet");
      //get("a");
      get("v");
  }


</script>

  <body onload="foo()">
  </body>

</html>

URL Parametresi Okumak

Servis tarafı kodlarında olduğu gibi pür istemci tarafında işleyen Javascript kodlarında da ÜRL sonuna eklenen parametreler okunabilir. Mesela bir ürün programı yazmışsak, ürün listesinden bir ürüne tıklayınca onun detaylarını ayrı bir detay.html sayfasında görmek isteyebilirim. Hangi ürüne baktığımı bu sayfaya bir parametre ile geçmek isterim, mesela id=... gibi. Tüm URL /detay.html?id=12231 gibi bir çağrı olabilir.

Parametreyi ÜRL'den almak için location.search çağrısından tüm URL'i alırım, ve parametre kısmını ayırırım, bunlar o sayfa yüklenir yüklenmez işleyen bir Javascript fonksiyonu içinde yapılabilir,

<html>
  <script>
    function detail() {
      var id = location.search.split('id=')[1];
      var s = `Gonderilen urun id ${id}`;
      document.getElementById('output').innerHTML = s; 
    }
  </script>    
  <body onload='detail()'>
    <div id="output">
    </div>
  </body>

</html>

Örnek

Tarih Objeleri

Tarih objesi yaratmak için en rahat yöntem,

dt1 = new Date("2022-03-25");
console.log(dt1);
2022-03-25T00:00:00.000Z

Eğer yıl, ay, gün ayrı ayrı geçmek istersek biraz takla lazım, ay sıfır indisli, gün değil, alttaki gibi oluyor,

dt1 = new Date(2022, 3-1, 25+1);

Bir tarihe gün ekleyip yeni tarih elde etmek için

Date.prototype.addDays = function(days) {
    var date = new Date(this.valueOf());
    date.setDate(date.getDate() + days);
    return date;
}

dt2 = dt1.addDays(20);

İki tarih arası gün farkı

diff = dt2.getTime() - dt1.getTime();

step = diff / (1000*60*60*24);

Basit bir kronometre kodlaması,

const start = Date.now();

// burada bazi islemler...

const end = Date.now();
console.log("Islem zamani: " + (end - start)");

İşlem Sırasında Animasyon Göstermek

Uzun sürebilecek işlemler sırasında bir anımasyon GİF göstermek mümkündür. Bir teknik şudur, bir <div> içine koyulmuş bir anımasyon GİF var, bu <div> sayfa yüklendiğinde stili dışplay: nöne; ile kapalı tutulur, yani gösterilmez. İşlem için bir düğmeye basıldığında ilk önce stil block haline getirilir, yani anımasyon gösterilir, işlem bitince anımasyon yine kapatılır.

Burada tek problem üstte sıralanan tüm işlemlerin aynı iş parçacığı (thread) içinde olacağı için kapatma/açılma işlemlerinin doğru gösterilmemesi. Fakat eğer uzun sürecek işlemi başka bir iş parçacığı içine asenkron olarak işletirsek [3], istenen görüntü elde edilir. Kullanılan animasyon gif şurada.

<head>
  <script>
    function longLoop() {
        let count = 1000000000;
        let result = 0;
        for (let i = 1; i <= count; i++) {
          result = i;
        }
        return result;
    }

    function go() {
        let spinnerElement = document.getElementById('spinner');
        let resultElement = document.getElementById('result');
        spinnerElement.style.display = 'block';

        new Promise(resolve => setTimeout(() => {
            resolve(longLoop())
        }))
      .then((result) => {
          spinnerElement.style.display = 'none';
          resultElement.innerHTML = result;   
      })
    }
  </script>
</head>

<body>
  <button onclick="go();">Go</button>
  <span id="result"></span>
  <img style="display: none;" id="spinner" src="busy.gif">
  </img>  
</body>

Kaynaklar

[2] https://mathjs.org/download.html

[3] https://stackoverflow.com/questions/71943182/how-to-show-hide-animated-gif-during-the-execution-of-a-function/71944178#71944178


Yukarı