İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

Blok 3 · Ders 24

Tarayıcıdan veri çekmek: fetch ve API

Dış bir API'den JSON çekip sayfada göstermek: yükleniyor ve hata durumları, arama, filtreleme ve CORS.

Bu dersin sonunda

  • Bir API'nin ne olduğunu anlatıp Data Dragon'un champion.json yanıtında istediğin bilgiye giden yolu bulabilirsin
  • Ders 15'teki fetch ve async/await kodunu tarayıcıya taşıyıp sonucu konsolda ve Network sekmesinde inceleyebilirsin
  • Sayfanda yükleniyor, hata ve boş durumlarını kullanıcıya açıkça gösterebilirsin
  • Gelen veriyi kartlara döküp Türkçe harflere dayanıklı bir arama kutusu ve rol filtresi ekleyebilirsin
  • CORS hatasının neden çıktığını ve kimin düzeltebileceğini açıklayabilirsin
  • Resimleri loading="lazy" ile geç yükleyip farkı Network sekmesinde görebilirsin
Bu sayfada
  1. API: programlar arasındaki kapı
  2. champion.json'un içine bakmak
  3. Tarayıcıda fetch: Node'dan farkı ne?
  4. Network sekmesinde isteği incelemek
  5. Yükleniyor, hata ve boş durumları
  6. Şampiyonları kartlara dökmek
  7. Arama kutusu ve rol filtresi
  8. Resimleri geç yüklemek
  9. CORS: tarayıcının izin denetimi
  10. Sık hatalar
  11. Görevler
  12. Kendini sına
  13. Kayıt noktası

Şimdiye kadar sayfalarındaki verinin hepsini sen yazdın: şampiyon dizileri, maç günlüğü kayıtları. Gerçek uygulamalar veriyi çoğu zaman başka bir yerden, bir sunucudan alır. Bu derste Riot'un herkese açık veri deposu Data Dragon'dan 170'ten fazla şampiyonun güncel listesini tarayıcıda çekecek, kartlara dökecek, arama kutusu ve rol filtresi ekleyeceksin.

Yolda internetin üç gerçeğiyle tanışacaksın: istek zaman alır, bazen hiç sonuçlanmaz ve tarayıcı her siteden veri okumana izin vermez. İyi bir sayfa bu üçünü de kullanıcıya açıkça söyler.

API: programlar arasındaki kapı

Şampiyon kartları yapmak istiyorsun. Bütün şampiyonların adını, unvanını ve rolünü elle bir diziye yazabilirsin. Ama 170'ten fazla şampiyon var, her yamada bir şeyler değişiyor ve elle yazdığın liste birkaç hafta içinde eskiyor. Doğrusu, bu bilgiyi zaten tutan yerden, yani Riot'tan istemek.

Ders 15'te bunun ilk örneğini gördün: programın bir adrese istek attı, cevap olarak JSON aldı. Bir programın başka programlara böyle kurallı bir kapı açmasına API denir.

Web API'lerinde dükkân ekranındaki her düğme bir adrestir. Belli bir veriyi veren adrese uç nokta denir.

Riot oyunun sabit verilerini (şampiyonlar, eşyalar, sihirdar büyüleri, simgeler) Data Dragon'da herkese açık tutar; anahtar, hesap ya da kayıt istemez. Bütün adresleri https://ddragon.leagueoflegends.com ile başlar. Bu derste iki uç noktasını kullanacağız:

Uç nokta (adresin devamı)Ne verir?
/api/versions.jsonBütün sürümler, en yenisi başta
/cdn/16.20.1/data/tr_TR/champion.jsonO sürümdeki bütün şampiyonların özeti, Türkçe

İkinci adreste iki parça değişir: 16.20.1 sürüm, tr_TR dil kodu (Türkçe, Türkiye). Sürümü elle yazarsan bir sonraki yamada eskir; bu yüzden önce sürüm listesini isteyip en yenisini alacak, adresi onunla kuracağız. Riot'un belgesi bir uyarı da yapıyor: Data Dragon her yamadan sonra elle güncellendiği için yeni yamanın çıktığı gün hemen güncellenmeyebilir.

champion.json'un içine bakmak

Bu ders için C:\kod\sampiyon-listesi adında yeni bir proje klasörü oluştur ve VS Code'da File → Open Folder ile aç. Kod yazmadan önce cevaba gözünle bak: Ders 15'te versions.json'u tarayıcıda açmıştın, şimdi şampiyon listesini aç. Adresteki sürümü versions.json'un ilk elemanıyla değiştir:

https://ddragon.leagueoflegends.com/cdn/16.20.1/data/tr_TR/champion.json

Ekran bir anda yazıyla dolar: yaklaşık 163 kB'lık tek bir JSON metni. Chrome, Edge ya da Brave'de üstteki Pretty-print kutusunu işaretlersen girintili görünür. Yapısı şöyle; Ahri'nin kaydını kısalttık, … olan yerlerde başka alanlar ve 171 şampiyon daha var:

champion.json (kısaltılmış)
{    "type": "champion",    "format": "standAloneComplex",    "version": "16.20.1",    "data": {        "Aatrox": { … },        "Ahri": {            "version": "16.20.1",            "id": "Ahri",            "key": "103",            "name": "Ahri",            "title": "Dokuz Kuyruklu Tilki",            "blurb": "Ruhlar âleminin sihrine doğuştan bağlı olan Ahri, …",            "info": { "attack": 3, "defense": 4, "magic": 8, "difficulty": 5 },            "image": { "full": "Ahri.png", "sprite": "champion0.png", … },            "tags": ["Mage", "Assassin"],            "partype": "Mana",            "stats": { "hp": 590, "hpperlevel": 104, "mp": 418, … }        },        …    }}
veri yanit.json()'un verdiği obje type: "champion" version: "16.20.1" data anahtarlar şampiyonların kimliği (id) Aatrox: {…} …ve 171 şampiyon daha Ahri: name: "Ahri" title: "Dokuz Kuyruklu Tilki" tags: ["Mage", "Assassin"] image: { full: "Ahri.png" } veri.data.Ahri.title "Dokuz Kuyruklu Tilki" Object.values(veri.data) [ {Aatrox}, {Ahri}, … ] 173 eleman
Şekil 24.1 champion.json iç içe kutulardır. Bir bilgiye, dıştan içe adım adım inerek ulaşırsın.

Dört şeye dikkat et:

  • data bir dizi değil, obje. Anahtarları şampiyonların kimliği (id). Kart dizmek için dizi gerekir: Object.values(veri.data) (Ders 12) bütün değerleri sırayla bir diziye koyar.
  • id ile name farklı olabilir. id İngilizce harfli, boşluksuz bir kimliktir; ekrana name'i yaz. Wukong'un kimliği MonkeyKing; Nunu'nun adı Türkçe dosyada Nunu ve Willump, kimliği Nunu.
  • tags Türkçe dosyada da İngilizce kalır: Fighter, Tank, Mage, Assassin, Marksman, Support. Bunlar ekran yazısı değil, kodun süzme yaparken kullanacağı etiketler. Türkçelerini kendimiz yazacağız.
  • title ve blurb dile göre değişir. Adresteki dil kodu en_US olursa aynı alanlar İngilizce gelir:
Alantr_TRen_US
Ahri'nin title alanıDokuz Kuyruklu Tilkithe Nine-Tailed Fox
Nunu'nun name alanıNunu ve WillumpNunu & Willump
Ahri'nin tags alanıMage, AssassinMage, Assassin

Yolları sayfa yazmadan denemek için Node yeter. Proje klasöründe deneme.js adında bir dosya aç; bu dersteki küçük Node denemelerini hep bu dosyada yapacaksın (her seferinde içini sil). İçine champion.json'un küçük bir kopyasını obje olarak yaz ve VS Code'un terminalinde çalıştır:

deneme.js — sampiyon-listesi — Visual Studio Code
deneme.js
const veri = {    type: "champion",    version: "16.20.1",    data: {        Ahri: {            name: "Ahri",            title: "Dokuz Kuyruklu Tilki",            tags: ["Mage", "Assassin"]        },        Garen: {            name: "Garen",            title: "Demacia'nın Kudreti",            tags: ["Fighter", "Tank"]        },        Jinx: {            name: "Jinx",            title: "Delifişek",            tags: ["Marksman"]        }    }} console.log(veri.version)console.log(veri.data.Ahri.title)console.log(veri.data.Garen.tags[1])console.log(Object.keys(veri.data)) const sampiyonlar = Object.values(veri.data)console.log(sampiyonlar.length)console.log(sampiyonlar[2].name)
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
PS C:\kod\sampiyon-listesi> node deneme.js16.20.1Dokuz Kuyruklu TilkiTank[ 'Ahri', 'Garen', 'Jinx' ]3Jinx
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Üstte deneme.js, altta terminal. VS Code'da sampiyon-listesi klasörünü açtığın için terminal de orada açılır.

veri.data.Garen.tags[1] yolunu soldan sağa oku: veri, onun içindeki data kutusu, onun içindeki Garen, Garen'in tags dizisi, dizinin 1 numaralı elemanı. Gerçek dosyada da yol aynı; yalnız data'nın içinde 173 şampiyon var.

Tarayıcıda fetch: Node'dan farkı ne?

fetch tarayıcının da yerleşik bir fonksiyonu; Ders 15'teki kod tarayıcıda da çalışır. Aynı klasörde iki dosya oluştur. Önce sayfanın iskeleti:

index.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1">    <title>Şampiyon Listesi</title>    <script src="uygulama.js" defer></script></head><body>    <h1>Şampiyonlar</h1></body></html>

Sonra uygulama.js. Ders 15'teki jsonGetir'i aynen yaz ve onu kullanan bir yukle fonksiyonu ekle:

uygulama.js
async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} async function yukle() {    const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")    const surum = surumler[0]    const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/champion.json`)    const sampiyonlar = Object.values(veri.data)    console.log("En yeni sürüm:", surum)    console.log("Şampiyon sayısı:", sampiyonlar.length)    console.log("İlk şampiyon:", sampiyonlar[0].name, "-", sampiyonlar[0].title)} yukle()
Konsol (F12 → Console)örnek, sende farklı çıkabilir
En yeni sürüm: 16.20.1Şampiyon sayısı: 173İlk şampiyon: Aatrox - Darkin Kılıcı

Sayfayı Ders 18'deki gibi Live Server ile aç: index.html'e sağ tıkla, Open with Live Server'ı seç. Tarayıcıda http://127.0.0.1:5500/index.html açılır. Sayfada yalnız başlık var; asıl iş görünmüyor. F12 ile geliştirici araçlarını aç, Console sekmesine geç: yukarıdaki üç satır oradadır. Sürüm ve sayı sende farklı olabilir, Riot her yamada listeyi günceller.

İkinci isteğin adresi birincinin cevabına bağlı (sürümü bilmeden şampiyon dosyasının adresini kuramazsın); bu yüzden iki await sırayla bekler. Kod Ders 15'tekiyle neredeyse aynı, ama ortam farklı:

Node (Ders 15)Tarayıcı (bu ders)
Kod ne zaman çalışır?node dosya.mjs yazıncaSayfa açılınca, <script> ile
Çıktı nereye yazılır?TerminaleGeliştirici araçlarında Console'a
Dosyanın en üstünde await.mjs dosyasında olurKlasik <script>'te olmaz
Ağ hatasının mesajıTypeError: fetch failedTypeError: Failed to fetch
Başka sitelerin verisiHer adresten okunurYalnız sunucu izin verirse (CORS)
İstekleri izlemekElinde araç yokNetwork sekmesi

Tarayıcıda yakalanmayan bir hata Node'daki gibi programı kapatmaz: yalnız o fonksiyon yarıda kalır, sayfa açık durur ve konsola kırmızı bir satır düşer. Bu iyi bir şey, ama hata sessizce geçer: konsolu açmazsan bir sorun olduğunu fark etmezsin. Bu yüzden tarayıcıda çalışırken geliştirici araçları hep açık kalsın.

Sık hataTypeError: yanit.json is not a function

Ne zaman çıkar? fetch'in önüne await yazmayı unutunca.

uygulama.js
async function yukle() {    const yanit = fetch("https://ddragon.leagueoflegends.com/api/versions.json")    const surumler = await yanit.json()    console.log(surumler[0])} yukle()
Konsol (F12 → Console)
Uncaught (in promise) TypeError: yanit.json is not a function    at yukle (uygulama.js:3:34)    at uygulama.js:7:1

await olmadan yanit bir yanıt değil, yanıtın sözüdür, yani bir Promise. Promise'in json diye bir metodu yoktur. Baştaki Uncaught (in promise) "bir Promise reddedildi ve hiçbir catch yakalamadı" demek; async fonksiyonların içindeki yakalanmayan hatalar konsolda böyle görünür.

Nasıl düzelir? const yanit = await fetch(...). Her fetch'in ve her json()'un önünde await olmalı.

Sık hataTypeError: Cannot convert undefined or null to object

Ne zaman çıkar? Gövdeyi json() yerine text() ile okuyunca ya da yanit.json()'un önündeki await'i unutunca.

uygulama.js
async function yukle() {    const yanit = await fetch("https://ddragon.leagueoflegends.com/cdn/16.20.1/data/tr_TR/champion.json")    const veri = await yanit.text()    const sampiyonlar = Object.values(veri.data)    console.log(sampiyonlar.length)} yukle()
Konsol (F12 → Console)
Uncaught (in promise) TypeError: Cannot convert undefined or null to object    at Object.values (<anonymous>)    at yukle (uygulama.js:4:32)

text() gövdeyi çevirmeden, düz metin olarak verir. Metnin data diye bir alanı yoktur; veri.data undefined olur ve Object.values(undefined) bu hatayı verir. await'siz yanit.json() da aynı yere çıkar, çünkü o zaman elindeki şey veri değil bir Promise'tir. Mesaj seni 4. satıra götürür ama asıl yanlış bir satır yukarıda.

Nasıl düzelir? JSON bekleyen her yerde const veri = await yanit.json(). Ne geldiğinden emin değilsen önce console.log(veri) ile bak: konsolda {"type":"champion"… diye başlayan düz bir yazı görürsen elinde metin var; obje olsaydı açılıp kapanan bir ağaç görürdün.

Network sekmesinde isteği incelemek

Console, kodunun ne yazdığını gösterir. Network sekmesi ise tarayıcının gerçekte ne gönderip ne aldığını gösterir. Ders 18'de ona ilk kez baktın; şimdi bir isteği baştan sona okuyacağız. Aşağıdaki adımlardaki numaralar şekildeki numaralarla aynı.

  1. Sayfan açıkken F12'ye bas ve Network sekmesine geç (1).
  2. Süzgeç çubuğunda Fetch/XHR'a tıkla (2). Böylece yalnız fetch ile yapılan istekleri görürsün; resimler ve stil dosyaları listeyi kalabalıklaştırmaz.
  3. Araç çubuğundaki Disable cache kutusunu işaretle ve sayfayı yenile (F5). Network sekmesi yalnız açıkken olan istekleri kaydeder; sayfa açılırken yapılan istekleri görmek için yenilemen gerekir. Kutu boşsa tarayıcı iki JSON dosyasını bir önceki açılıştan kalan kopyadan, yani önbellekten verebilir; o zaman Size sütununda boyut yerine (disk cache) yazar ve bu dersteki denemelerin bir kısmı (yavaş bağlantı, internetsiz açılış) hiçbir şey göstermez. Kutu geliştirici araçları açıkken geçerlidir; bu ders boyunca işaretli bırak.
  4. Listede versions.json ve champion.json belirir. Status sütununda 200, Type sütununda fetch yazar. Initiator sütunu isteği başlatan satırı gösterir: uygulama.js:2, yani jsonGetir'deki fetch; üstüne tıklarsan o satıra gidersin. Şimdi champion.json satırına tıkla (3).
  5. Açılan bölmenin Headers sekmesinde (4) önce General bölümü gelir: isteğin adresi (Request URL), yöntemi (Request Method: GET) ve sonucu (Status Code: 200 OK). Altındaki Response headers bölümünde sunucunun cevapla birlikte gönderdiği ek bilgi satırları, yani başlıklar (headers) var. Access-Control-Allow-Origin: * satırını aklında tut (6); CORS bölümünde lazım olacak.
  6. Preview sekmesi (5) JSON'u açılıp kapanan bir ağaç olarak gösterir: data'yı aç, Ahri'yi aç, title'ı bul. Response sekmesi aynı cevabın ham metnidir.
Chrome geliştirici araçlarının Network sekmesi, turuncu numaralarla işaretli. 1: üstteki sekmelerden Network seçili. 2: süzgeç düğmelerinden Fetch/XHR seçili ve çerçeveli. 3: listede iki satır var: versions.json, durum 200, tür fetch, başlatan uygulama.js:2, boyut 1.9 kB; champion.json, durum 200, tür fetch, başlatan uygulama.js:2, boyut 37.2 kB. Altta champion.json'a tıklayınca açılan bölme: 4 Headers sekmesi, 5 yanındaki Preview ve Response sekmeleri. General bölümünde Request URL https://ddragon.leagueoflegends.com/cdn/16.20.1/data/tr_TR/champion.json, Request Method GET, Status Code yeşil noktayla 200 OK. Response headers bölümünde 6 numarayla çerçevelenmiş Access-Control-Allow-Origin yıldız satırı; çevresinde Access-Control-Allow-Methods GET, HEAD, Content-Encoding gzip ve Content-Type application/json satırları.
Şekil 24.2 Gerçek Network sekmesi (Chrome 153). Üstte Fetch/XHR süzgecinden geçen iki istek, altta champion.json'a tıklayınca açılan Headers sekmesi. Numaralar adımlardakiyle aynı.

Size sütununa bak: champion.json için yaklaşık 37 kB yazar, oysa dosyanın kendisi 163 kB. Sunucu dosyayı sıkıştırıp gönderir (Response headers'taki Content-Encoding: gzip satırı bunu söyler), tarayıcı açar; yolda giden, küçük olandır. Network'ün kendi ayarlarını açarsan (araç çubuğunun en sağındaki dişli simgesi) Big request rows seçeneğiyle Size hücresinde iki sayıyı birden görürsün: üstteki ağdan gelen boyut, alttaki dosyanın asıl boyutu.

Yükleniyor, hata ve boş durumları

Sen kodu yazarken internetin hızlı, sunucu ayakta. Metroda telefondan giren biri için champion.json saniyeler sürebilir; tünelde hiç gelmeyebilir. Boş bir sayfaya bakan kullanıcı "bozuk" der ve çıkar. İyi bir arayüz her an dört durumdan birindedir ve hangisinde olduğunu kullanıcıya söyler.

Yükleniyor "Şampiyonlar yükleniyor…" Dolu kartlar ve sayaç: "173 şampiyondan 4…" Boş kart yok, açıklama var: "…uyan şampiyon yok." Hata "…yüklenemedi…" ayrıntı konsolda veri geldi, uyan var veri geldi, uyan yok istek başarısız yeniden dene
Şekil 24.3 Sayfanın dört durumu. Kullanıcı her an hangisinde olduğunu ekrandan anlayabilmeli.

Önce sayfaya durumu yazacak yeri aç. index.html'in <body> kısmını şöyle değiştir:

index.html
<body>    <header>        <h1>Şampiyonlar</h1>        <p id="surum"></p>    </header>    <main>        <p id="durum" aria-live="polite">Şampiyonlar yükleniyor…</p>        <ul id="liste" class="kartlar"></ul>    </main></body>

İlk durum metni HTML'de yazılı: JavaScript daha çalışmadan, sayfa açılır açılmaz "yükleniyor" görünür. aria-live="polite" niteliği ekran okuyucuya "bu yazı değişince yeni hâlini oku" der (Ders 19'daki erişilebilirlik); görme engelli bir kullanıcı da yüklemenin bittiğini ya da hatayı duyar.

Şimdi uygulama.js'i güncelle. jsonGetir aynı kalıyor; yukle'yi try / catch ile sar ve dosyanın başına öğeleri bulan satırları ekle:

uygulama.js
const surumYazisi = document.querySelector("#surum")const durum = document.querySelector("#durum")const liste = document.querySelector("#liste") let sampiyonlar = []let surum = "" // jsonGetir fonksiyonu burada, değişmedi async function yukle() {    durum.textContent = "Şampiyonlar yükleniyor…"    durum.classList.remove("hata")    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        surum = surumler[0]        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/champion.json`)        sampiyonlar = Object.values(veri.data)        surumYazisi.textContent = `Yama ${surum} · ${sampiyonlar.length} şampiyon`        ciz()    } catch (hata) {        console.error(hata)        durum.textContent = "Şampiyonlar yüklenemedi. İnternet bağlantını denetleyip sayfayı yenile."        durum.classList.add("hata")    }} function ciz() {    durum.textContent = `${sampiyonlar.length} şampiyon yüklendi.`} yukle()
  • sampiyonlar ve surum en üstte let ile açıldı, çünkü birazdan başka fonksiyonlar da (ciz, kart yapan fonksiyon) onları okuyacak (Ders 10'daki kapsam). yukle içinde const ile açsaydın dışarıdan görünmezlerdi.
  • ciz şimdilik yalnız sayıyı yazıyor; bir sonraki bölümde kartları çizecek.
  • Hata iki yere gidiyor: console.error ayrıntıyı konsola kırmızı yazar (geliştirici için), durum anlaşılır bir cümleyi ekrana yazar (kullanıcı için). Kullanıcıya TypeError: Failed to fetch göstermek yerine ne yapacağını söylemek daha iyidir.
  • hata sınıfının kırmızı rengini birazdan stil dosyasında vereceğiz. Durum her değiştiğinde önce eski hata sınıfını kaldırmayı unutma; yoksa başarılı bir yüklemeden sonra da yazı kırmızı kalır.

Dersin sonunda, arama kutusu da eklenmiş hâliyle, üç durum şöyle görünecek:

Aynı sayfanın üç durumu alt alta. 1: Yükleniyor; başlık, arama kutusu ve rol menüsünün altında gri renkli Şampiyonlar yükleniyor yazısı. 2: Hata; aynı yerde kırmızı renkli Şampiyonlar yüklenemedi, İnternet bağlantını denetleyip sayfayı yenile yazısı. 3: Boş; Yama 16.20.1, 173 şampiyon satırı görünüyor, arama kutusunda teemoo yazıyor, altında Bu aramaya uyan şampiyon yok yazısı var, hiç kart yok.
Şekil 24.4 Bitmiş sayfanın yükleniyor, hata ve boş durumlarındaki gerçek görüntüsü.

Durumları kendi gözünle gör. Network sekmesindeki Disable cache kutusu işaretli olsun; yoksa tarayıcı dosyaları önbellekten verir, ne yavaşlama ne hata görürsün.

  • Yükleniyor: Network sekmesinde No throttling yazan menüden 3G'yi seç ve sayfayı yenile. Her şey yavaşlar, "Şampiyonlar yükleniyor…" yazısı birkaç saniye ekranda kalır. Denemeden sonra menüyü yeniden No throttling yap; yoksa bu sekmede açtığın her sayfa yavaş gelir.
  • Hata: champion.json adresindeki ${surum} yerine geçici olarak 16.99.1 yaz ve kaydet. Data Dragon 403 döndürür, jsonGetir hata fırlatır, ekranda kırmızı mesaj çıkar. Konsolda iki kırmızı satır görürsün; ne anlama geldiklerini Sık hatalar bölümündeki ilk kart anlatıyor. Sonra adresi geri al.
  • İnternetsiz: Wi-Fi'ı kapatıp sayfayı yenile. Live Server senin bilgisayarında çalıştığı için sayfa açılır ama Data Dragon'a ulaşılamaz. Network'teki Offline seçeneği bu iş için uygun değil: Live Server'a giden isteği de keser, sayfanın kendisi de açılmaz.
  • Boş: Arama kutusunu birazdan, Arama kutusu ve rol filtresi bölümünde ekleyeceğiz; olmayan bir ad aratınca bu durum çıkacak.

Şampiyonları kartlara dökmek

Veri elimizde. Şimdi her şampiyon için bir kart yapacağız: resim, ad, unvan ve rol rozetleri. Ders 22'de öğrendiğin createElement ve append yeter. Önce yolun tamamına bir bak:

veri.data obje: { Aatrox: {…}, Ahri: {…}, … } Object.values(…) sampiyonlar dizi: [Aatrox, Ahri, Akali, …], 173 eleman filter + uyuyorMu arama: "demacia", rol: Hepsi gorunenler [Garen, Jarvan IV, Quinn, Xin Zhao] map(kartOlustur) [li, li, li, li] 4 kart: resim, ad, unvan, rozetler liste.append(...) <ul id="liste"> kartlar sayfada görünür
Şekil 24.5 Veriden kartlara: her adım bir öncekinin sonucunu alır. Örnekte arama kutusunda "demacia" yazıyor.

Bir şampiyon objesi alıp bir <li> kartı döndüren fonksiyonu yaz. Rol etiketlerinin Türkçesi için de bir obje gerekiyor:

uygulama.js
const ROL_ADLARI = {    Assassin: "Suikastçı",    Fighter: "Dövüşçü",    Mage: "Büyücü",    Marksman: "Nişancı",    Support: "Destek",    Tank: "Tank"} function kartOlustur(sampiyon) {    const kart = document.createElement("li")    kart.classList.add("kart")     const resim = document.createElement("img")    resim.src = `https://ddragon.leagueoflegends.com/cdn/${surum}/img/champion/${sampiyon.image.full}`    resim.alt = ""    resim.width = 96    resim.height = 96     const ad = document.createElement("h2")    ad.textContent = sampiyon.name     const unvan = document.createElement("p")    unvan.textContent = sampiyon.title     const roller = document.createElement("p")    roller.classList.add("roller")    for (const rol of sampiyon.tags) {        const rozet = document.createElement("span")        rozet.textContent = ROL_ADLARI[rol]        roller.append(rozet)    }     kart.append(resim, ad, unvan, roller)    return kart}
  • Resim adresi: Data Dragon'da şampiyon simgeleri …/cdn/<sürüm>/img/champion/<dosya adı> adresindedir. Dosya adını kendin uydurma, verideki image.full alanından al (aşağıdaki hata kartına bak).
  • resim.alt = "": Kartta adı zaten yazılı. Boş alt, ekran okuyucuya "bu resim süs, atla" der; yoksa her kartta ad iki kez okunur.
  • resim.width ve resim.height: HTML'deki width="96" ve height="96" niteliklerinin aynısı. Resim gelmeden önce yerini ayırır; neden önemli olduğunu geç yükleme bölümünde göreceksin.
  • textContent: Metinler Riot'un sunucusundan geliyor. innerHTML kullansaydın, bir gün o metinlerde bir etiket olsa tarayıcı onu çalıştırırdı (Ders 22). Dışarıdan gelen her metin textContent ile yazılır.
  • ROL_ADLARI[rol]: Hiç değişmeyecek bir ayar listesi olduğu için adı büyük harfle (Ders 17'deki DUSMANLAR gibi). rol bir değişken olduğu için köşeli parantezle okunur (Ders 4): "Mage" için "Büyücü" gelir.

ciz'i de kartları çizecek hâle getir:

uygulama.js
function ciz() {    liste.textContent = ""    liste.append(...sampiyonlar.map(kartOlustur))    durum.textContent = `${sampiyonlar.length} şampiyon gösteriliyor.`}

liste.textContent = "" listenin içini boşaltır. sampiyonlar.map(kartOlustur) her şampiyon için kartOlustur'u çağırır ve kartlardan oluşan bir dizi verir (Ders 11). append tek tek öğe ister, dizi değil; baştaki ... diziyi açıp elemanlarını tek tek verir (Ders 12'deki yayma).

Görünüş için klasöre stil.css dosyasını ekle ve index.html'in <head> kısmına, <script> satırının üstüne <link rel="stylesheet" href="stil.css"> yaz. Izgara Ders 21'deki satır; renkleri istediğin gibi değiştirebilirsin:

stil.css
* {    box-sizing: border-box;} body {    margin: 0;    font-family: system-ui, sans-serif;    background: #0f1420;    color: #e8e6df;} header, main, footer {    max-width: 1100px;    margin: 0 auto;    padding: 0 16px;} h1 {    margin: 24px 0 4px;    color: #c8aa6e;} #surum, #durum, footer {    color: #9aa3b5;} #durum.hata {    color: #ff8f8f;} .araclar {    display: flex;    flex-wrap: wrap;    gap: 12px;} .araclar label {    display: flex;    flex-direction: column;    gap: 4px;} input, select {    font: inherit;    padding: 8px 10px;    border: 1px solid #3a4560;    border-radius: 8px;    background: #182033;    color: inherit;} .kartlar {    display: grid;    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));    gap: 12px;    padding: 0;    list-style: none;} .kart {    padding: 14px 10px;    border: 1px solid #2a3550;    border-radius: 12px;    background: #182033;    text-align: center;} .kart h2 {    margin: 8px 0 2px;    font-size: 16px;} .kart p {    margin: 0;    font-size: 13px;    color: #9aa3b5;} .roller span {    display: inline-block;    margin: 8px 2px 0;    padding: 2px 8px;    border-radius: 999px;    background: #2a3550;    font-size: 12px;    color: #e8e6df;} footer {    margin-top: 32px;    padding-bottom: 24px;    font-size: 12px;}

Sayfayı yenile. Arama kutusu ve rol menüsü bir sonraki bölümde gelecek; onlar eklenince sayfa şöyle görünecek:

127.0.0.1:5500/index.html
Şampiyon Listesi sayfası koyu zeminde. Üstte altın renkli Şampiyonlar başlığı, altında Yama 16.20.1 · 173 şampiyon yazısı, Ara kutusu ve Hepsi seçili Rol menüsü. Altında 173 şampiyondan 173 tanesi gösteriliyor yazısı ve dört sütunlu kart ızgarası: Aatrox, Darkin Kılıcı, Dövüşçü; Ahri, Dokuz Kuyruklu Tilki, Büyücü ve Suikastçı; Akali; Akshan; Alistar; Ambessa; Amumu; Anivia. Her kartın üstünde resim yerine baş harfi yazan renkli bir kare var.
Dersin sonundaki sayfa. Şampiyon resimleri Riot'a ait olduğu için bu görüntüde onların yerine baş harfli yer tutucular koyduk; sende gerçek simgeler görünecek.
Sık hataWukong'un resmi gelmiyor

Ne zaman çıkar? Resim adresini şampiyonun adından (name) kurunca. Konsolda hata yoktur; 173 kartın 152'sinde resim gelir, 21'inde boşluk kalır.

uygulama.js
resim.src = `https://ddragon.leagueoflegends.com/cdn/${surum}/img/champion/${sampiyon.name}.png`

Ahri'de ad ile dosya adı aynı olduğu için sorun yok. Ama Wukong'un dosyası Wukong.png değil MonkeyKing.png; Kai'Sa'nınki Kaisa.png, Jarvan IV'ünki JarvanIV.png. Olmayan bir dosya için Data Dragon resim yerine 403 ile küçük bir XML hata sayfası gönderir. Network sekmesinde Img süzgecini seçersen Wukong.png isteğinin başarısız olduğunu görürsün; tarayıcı, resim beklerken XML geldiği için isteği net::ERR_BLOCKED_BY_ORB hatasıyla keser. Adres büyük-küçük harfe de duyarlı: ahri.png da 403 alır.

Nasıl düzelir? Dosya adını hep verideki image.full alanından al: ${sampiyon.image.full}. Veri sana adın nasıl yazılacağını söylüyorsa kendin kurmaya kalkma.

Arama kutusu ve rol filtresi

173 kartın içinde Thresh'i bulmak için kaydırmak yorucu. Bir arama kutusu (ad ya da unvan) ve bir rol menüsü ekleyelim. index.html'de <main>'in en başına, #durum paragrafının üstüne şunu koy:

index.html
<div class="araclar">    <label>        Ara        <input type="search" id="arama" placeholder="Ahri, tilki…">    </label>    <label>        Rol        <select id="rol">            <option value="">Hepsi</option>            <option value="Assassin">Suikastçı</option>            <option value="Fighter">Dövüşçü</option>            <option value="Mage">Büyücü</option>            <option value="Marksman">Nişancı</option>            <option value="Support">Destek</option>            <option value="Tank">Tank</option>        </select>    </label></div>

Her seçeneğin value'su verideki İngilizce etiket, görünen yazısı Türkçe. Kod value ile çalışır, kullanıcı yazıyı görür. "Hepsi"nin value'su boş metin: "rol süzme" demek.

Türkçe harf tuzağı yine karşında

Aramayı büyük-küçük harfe duyarsız yapmak için iki tarafı da küçültüp includes ile karşılaştıracaksın. Hangi kuralla küçülteceksin? Ders 9'da kural şuydu: metin hangi dildeyse onun kuralını kullan. Burada iki dil aynı kutuda: unvanlar Türkçe (Kumların İmparatoru), adlar İngilizce kökenli (Irelia, Jarvan IV). İkisini de dene:

deneme.js
console.log("Irelia".toLowerCase())console.log("Irelia".toLocaleLowerCase("tr"))console.log("Kumların İmparatoru".toLowerCase().includes("imparator"))console.log("Kumların İmparatoru".toLocaleLowerCase("tr").includes("imparator"))
Çıktı
ireliaıreliafalsetrue

Türkçe kural Irelia'yı ırelia yapıyor: kullanıcı irelia yazarsa bulunmaz. İngilizce kural da İmparatoru'nu bozuyor: İ küçülünce iki karakterli bir i̇ çıkar ve imparator eşleşmez. Hangisini seçersen bir yarısı bozuluyor.

Çözüm: iki tarafı da önce Türkçe kuralla küçült, sonra bütün ı harflerini i yap. Böylece "ı mı yazdı, i mi?" farkı aramada önemini yitirir:

deneme.js
function aramaBicimi(metin) {    return metin.toLocaleLowerCase("tr").replaceAll("ı", "i")} console.log(aramaBicimi("Irelia"))console.log(aramaBicimi("İRELİA"))console.log(aramaBicimi("Jarvan IV"))console.log(aramaBicimi("Kumların İmparatoru"))
Çıktı
ireliaireliajarvan ivkumlarin imparatoru

Kullanıcı irelia, IRELIA ya da İrelia yazsın; şampiyonun adı Irelia olsun: hepsi aynı biçime iner. Aramada iki tarafa da aynı fonksiyonu uygulamak şart; yalnız birine uygularsan yine eşleşmez.

Süzme fonksiyonu

Bir şampiyonun aramaya ve role uyup uymadığını söyleyen fonksiyonu DOM'dan ayrı yaz. Böylece onu sayfa açmadan, Node'da deneyebilirsin:

deneme.js
function aramaBicimi(metin) {    return metin.toLocaleLowerCase("tr").replaceAll("ı", "i")} function uyuyorMu(sampiyon, aranan, rol) {    const metin = aramaBicimi(`${sampiyon.name} ${sampiyon.title}`)    const metinUyuyor = metin.includes(aramaBicimi(aranan))    const rolUyuyor = rol === "" || sampiyon.tags.includes(rol)    return metinUyuyor && rolUyuyor} const ornekler = [    { name: "Ahri", title: "Dokuz Kuyruklu Tilki", tags: ["Mage", "Assassin"] },    { name: "Irelia", title: "Bıçakların Dansı", tags: ["Fighter", "Assassin"] },    { name: "Leona", title: "Seher Işığı", tags: ["Tank", "Support"] },    { name: "Lux", title: "Işık Leydisi", tags: ["Mage", "Support"] }] function adlar(aranan, rol) {    return ornekler.filter((sampiyon) => uyuyorMu(sampiyon, aranan, rol)).map((sampiyon) => sampiyon.name)} console.log(adlar("", ""))console.log(adlar("IRELIA", ""))console.log(adlar("ışık", ""))console.log(adlar("ışı", ""))console.log(adlar("", "Support"))console.log(adlar("a", "Mage"))console.log(adlar("teemo", ""))
Çıktı
[ 'Ahri', 'Irelia', 'Leona', 'Lux' ][ 'Irelia' ][ 'Lux' ][ 'Leona', 'Lux' ][ 'Leona', 'Lux' ][ 'Ahri' ][]

Satır satır sınırları dene:

  • Boş arama her metnin içinde vardır ("lux".includes("") true verir); boş arama ve "Hepsi" bütün listeyi getirir.
  • ışık yalnız Lux'u buldu, Leona'yı bulmadı. Leona'nın unvanı Seher Işığı: Türkçede ışık sözcüğü ek alınca ışığı olur ve k harfi ğ'ye döner. Arama harf harf eşleştirir; ışı yazınca ikisi de çıkar.
  • Rol koşulu includes ile: Leona'nın etiketleri ["Tank", "Support"]; rolü ikinci sırada olsa da bulunur. tags[0] === rol yazsaydın Lux, Leona gibi destekleri kaçırırdın.
  • teemo hiçbir şeye uymadı ve boş dizi geldi. Sayfada bu, boş durumdur.

Şimdi uyuyorMu'yu ve aramaBicimi'yi uygulama.js'e taşı ve ciz'i süzerek çizecek hâle getir. Dosyanın başına iki öğeyi, sonuna iki olay dinleyicisini ekle:

uygulama.js
const aramaKutusu = document.querySelector("#arama")const rolSecimi = document.querySelector("#rol") function ciz() {    if (sampiyonlar.length === 0) {        return    }    const aranan = aramaKutusu.value.trim()    const rol = rolSecimi.value    const gorunenler = sampiyonlar.filter((sampiyon) => uyuyorMu(sampiyon, aranan, rol))     liste.textContent = ""    liste.append(...gorunenler.map(kartOlustur))     if (gorunenler.length === 0) {        durum.textContent = "Bu aramaya uyan şampiyon yok."    } else {        durum.textContent = `${sampiyonlar.length} şampiyondan ${gorunenler.length} tanesi gösteriliyor.`    }} aramaKutusu.addEventListener("input", ciz)rolSecimi.addEventListener("change", ciz)

Her harf yazıldığında ve her rol seçildiğinde ciz baştan çalışır: tam listeden süzer, listeyi boşaltır, kalanları çizer, sayacı günceller. Veriyi bir kez çektik; süzme her seferinde bellekteki sampiyonlar dizisinden yapılıyor, yeni istek gitmiyor. trim() kullanıcının yanlışlıkla bıraktığı boşlukları atar (Ders 9).

En baştaki if, Ders 6'daki erken çıkış: veri daha gelmediyse ciz hiçbir şey yapmaz. Neden gerekli? Arama kutusu sayfa açılır açılmaz yazılabilir. Veri yüklenirken ya da yükleme başarısız olduktan sonra biri kutuya yazarsa ciz boş diziden süzer, "Bu aramaya uyan şampiyon yok." yazar ve ekrandaki "yükleniyor" ya da hata mesajını siler.

Denedik: o üç satır yokken, kırmızı hata mesajı ekrandayken kutuya a yazınca mesaj "Bu aramaya uyan şampiyon yok." oldu. Kullanıcı internetinin gittiğini değil, aradığı şampiyonun olmadığını sanar; boş durumla hatayı karıştıran sayfanın ta kendisi. Erken çıkışla hata mesajı yerinde kalır. Yüklenirken yazılan arama da boşa gitmez: veri gelince yukle ciz'i çağırır, o da kutudaki yazıyla süzer.

127.0.0.1:5500/index.html
Arama kutusunda demacia yazıyor, Rol menüsünde Hepsi seçili. Altında 173 şampiyondan 4 tanesi gösteriliyor yazısı ve dört kart: Garen, Demacia'nın Kudreti, Dövüşçü ve Tank; Jarvan IV, Demacia'nın Simgesi, Dövüşçü ve Tank; Quinn, Demacia'nın Kanatları, Nişancı ve Suikastçı; Xin Zhao, Demacia'nın Hizmetkârı, Dövüşçü ve Tank.
Arama kutusunda demacia yazıyor: unvanında Demacia geçen dört şampiyon kaldı. Resimlerin yerinde yine yer tutucular var.
Sık hataSayaç 15 diyor, ekranda 345 kart var

Ne zaman çıkar? ciz içindeki liste.textContent = "" satırını unutunca. Hata mesajı yoktur, sayaç da doğrudur; ama her harfte yeni kartlar eskilerin altına eklenir.

Denedik: ilk açılışta 173 kart vardı. Kutuya ah yazınca sayaç "173 şampiyondan 15 tanesi gösteriliyor." dedi, ekranda 345 kart vardı. Kutuyu temizleme düğmesiyle (×) boşaltınca 518 oldu. a harfi 157, ah 15, boş kutu 173 kart ekledi; hiçbiri silinmedi.

Nasıl düzelir? Çizmeden önce listeyi boşalt: liste.textContent = "". Bir listeyi her seferinde baştan çizen her fonksiyon önce eskisini temizlemeli.

Resimleri geç yüklemek

Sayfa açılınca tarayıcı 173 kartın hepsini oluşturur ve her <img> için Data Dragon'a bir istek atar. Denedik: bu sürümde 173 simgenin toplamı yaklaşık 4,9 milyon bayt, yani 5 MB'a yakın. JSON'un ağdan gelen hâli ise 37 kB. Kullanıcı ekranda belki 12 kart görüyor; kalan 160'tan fazla resim için mobil verisini harcamak boşuna.

Çözüm tek bir nitelik: loading="lazy". Tarayıcıya "bu resmi şimdi indirme, kullanıcı ona yaklaşınca indir" der.

kartOlustur'daki resim satırlarına bir satır ekle:

uygulama.js
const resim = document.createElement("img")resim.src = `https://ddragon.leagueoflegends.com/cdn/${surum}/img/champion/${sampiyon.image.full}`resim.alt = ""resim.width = 96resim.height = 96resim.loading = "lazy"

HTML'de yazsaydın <img src="…" alt="" width="96" height="96" loading="lazy"> olurdu; resim.loading = "lazy" aynı niteliği JavaScript'ten koyar.

Ekran şu an gördüğün kartlar: resimleri geldi Yakında kaydırmadan önce indirilir; sen gelince hazırdır Uzakta henüz istenmedi; kaydırınca istenir
Şekil 24.6 Geç yüklemede yalnız ekrandaki ve ekrana yakın resimler indirilir; uzaktakiler kaydırınca istenir.

Tarayıcı resmi tam ekrana girdiği anda değil, biraz önce ister; böylece kaydırırken boş kart görmezsin. Ne kadar önce isteyeceğine tarayıcı kendisi karar verir. Chrome 153 ile denedik: 1000 × 760 piksellik bir pencerede ilk açılışta 173 resmin 48'i istendi, sayfanın sonuna kadar kaydırınca kalan 125'i geldi. Telefon boyutunda (390 × 844) ilk açılışta yalnız 18 resim istendi. loading="lazy" olmadan ise 173'ü birden istenir.

Bağlantı yavaşladıkça tarayıcı daha uzaktaki resimleri de önceden ister, çünkü her resim daha geç gelir. Aynı pencerede Network'ün kısma menüsünden Slow 4G seçince ilk açılışta 78, 3G seçince 168 resim istendi.

Kendin ölç: Network sekmesinde Img süzgecini seç ve sayfayı yenile. Panelin en altındaki sayaç, süzgeçten kaç isteğin geçtiğini yazar (48 / 54 requests gibi). Sonra sayfayı yavaşça aşağı kaydır: listeye yeni .png satırları eklenir, sayı artar. resim.loading = "lazy" satırını silip yenilersen 173'ünün birden geldiğini görürsün.

width ve height neden önemli? Resim gelmeden önce tarayıcı onun ne kadar yer kaplayacağını bilmezse resme hiç yer ayırmaz; resim gelince kart büyür ve altındaki her şey aşağı kayar. Okumakta olduğun yazı bir anda zıplar. Boyutları baştan verirsen yer ayrılır, hiçbir şey kaymaz.

Geç yüklemede bu daha da önemli: boyutu bilinmeyen resim gelene kadar sıfır yükseklikte durur, kartlar kısa çizilir ve tarayıcı ekrana yakın sandığı fazladan resimleri de ister. Denedik: aynı pencerede width ve height satırlarını silince ilk açılışta 48 yerine 72 resim istendi.

Dosyaların son hâli

Bütün parçalar yerinde. index.html şöyle olmalı (en alttaki not, Riot'un istediği hayran projesi notu):

index.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1">    <title>Şampiyon Listesi</title>    <link rel="stylesheet" href="stil.css">    <script src="uygulama.js" defer></script></head><body>    <header>        <h1>Şampiyonlar</h1>        <p id="surum"></p>    </header>    <main>        <div class="araclar">            <label>                Ara                <input type="search" id="arama" placeholder="Ahri, tilki…">            </label>            <label>                Rol                <select id="rol">                    <option value="">Hepsi</option>                    <option value="Assassin">Suikastçı</option>                    <option value="Fighter">Dövüşçü</option>                    <option value="Mage">Büyücü</option>                    <option value="Marksman">Nişancı</option>                    <option value="Support">Destek</option>                    <option value="Tank">Tank</option>                </select>            </label>        </div>        <p id="durum" aria-live="polite">Şampiyonlar yükleniyor…</p>        <ul id="liste" class="kartlar"></ul>    </main>    <footer>        <p>Şampiyon Listesi was created under Riot Games' "Legal Jibber Jabber" policy using assets owned by Riot Games. Riot Games does not endorse or sponsor this project.</p>    </footer></body></html>

uygulama.js'in tamamı. Kendi dosyanı bununla karşılaştır; sıra önemli: öğeleri bulan satırlar en üstte, olay dinleyicileri ve yukle() çağrısı en altta.

uygulama.js
const surumYazisi = document.querySelector("#surum")const durum = document.querySelector("#durum")const liste = document.querySelector("#liste")const aramaKutusu = document.querySelector("#arama")const rolSecimi = document.querySelector("#rol") const ROL_ADLARI = {    Assassin: "Suikastçı",    Fighter: "Dövüşçü",    Mage: "Büyücü",    Marksman: "Nişancı",    Support: "Destek",    Tank: "Tank"} let sampiyonlar = []let surum = "" async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} async function yukle() {    durum.textContent = "Şampiyonlar yükleniyor…"    durum.classList.remove("hata")    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        surum = surumler[0]        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/champion.json`)        sampiyonlar = Object.values(veri.data)        surumYazisi.textContent = `Yama ${surum} · ${sampiyonlar.length} şampiyon`        ciz()    } catch (hata) {        console.error(hata)        durum.textContent = "Şampiyonlar yüklenemedi. İnternet bağlantını denetleyip sayfayı yenile."        durum.classList.add("hata")    }} function kartOlustur(sampiyon) {    const kart = document.createElement("li")    kart.classList.add("kart")     const resim = document.createElement("img")    resim.src = `https://ddragon.leagueoflegends.com/cdn/${surum}/img/champion/${sampiyon.image.full}`    resim.alt = ""    resim.width = 96    resim.height = 96    resim.loading = "lazy"     const ad = document.createElement("h2")    ad.textContent = sampiyon.name     const unvan = document.createElement("p")    unvan.textContent = sampiyon.title     const roller = document.createElement("p")    roller.classList.add("roller")    for (const rol of sampiyon.tags) {        const rozet = document.createElement("span")        rozet.textContent = ROL_ADLARI[rol]        roller.append(rozet)    }     kart.append(resim, ad, unvan, roller)    return kart} function aramaBicimi(metin) {    return metin.toLocaleLowerCase("tr").replaceAll("ı", "i")} function uyuyorMu(sampiyon, aranan, rol) {    const metin = aramaBicimi(`${sampiyon.name} ${sampiyon.title}`)    const metinUyuyor = metin.includes(aramaBicimi(aranan))    const rolUyuyor = rol === "" || sampiyon.tags.includes(rol)    return metinUyuyor && rolUyuyor} function ciz() {    if (sampiyonlar.length === 0) {        return    }    const aranan = aramaKutusu.value.trim()    const rol = rolSecimi.value    const gorunenler = sampiyonlar.filter((sampiyon) => uyuyorMu(sampiyon, aranan, rol))     liste.textContent = ""    liste.append(...gorunenler.map(kartOlustur))     if (gorunenler.length === 0) {        durum.textContent = "Bu aramaya uyan şampiyon yok."    } else {        durum.textContent = `${sampiyonlar.length} şampiyondan ${gorunenler.length} tanesi gösteriliyor.`    }} aramaKutusu.addEventListener("input", ciz)rolSecimi.addEventListener("change", ciz)yukle()

Toplam 105 satır, ama her fonksiyon tek bir iş yapıyor: jsonGetir istek atar, yukle durumları yönetir, kartOlustur bir kart yapar, uyuyorMu süzer, ciz ekranı günceller. Bir şey bozulunca hangi fonksiyona bakacağını bilirsin.

CORS: tarayıcının izin denetimi

Data Dragon'dan veri çekerken tarayıcı seni hiç durdurmadı. Şimdi Riot'un sitesindeki yama notları sayfasını kendi sayfanda göstermek istediğini düşün. Şampiyon listene dokunmadan denemek için aynı klasörde küçük bir deneme sayfası aç:

cors.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <title>CORS Denemesi</title>    <script src="cors.js" defer></script></head><body>    <h1>CORS denemesi</h1></body></html>

cors.js'e yama notları sayfasını isteyen kodu yaz ve sayfayı http://127.0.0.1:5500/cors.html adresinden aç:

cors.js
async function yamaNotlariniGetir() {    const yanit = await fetch("https://www.leagueoflegends.com/tr-tr/news/game-updates/")    const sayfa = await yanit.text()    console.log(sayfa.length)} yamaNotlariniGetir()
Konsol (F12 → Console)
Access to fetch at 'https://www.leagueoflegends.com/tr-tr/news/game-updates/' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.GET https://www.leagueoflegends.com/tr-tr/news/game-updates/ net::ERR_FAILED 200 (OK)Uncaught (in promise) TypeError: Failed to fetch    at yamaNotlariniGetir (cors.js:2:25)    at cors.js:7:1

Ortadaki satırın sonuna bak: 200 (OK). Sunucu sayfayı başarıyla gönderdi, ama tarayıcı cevabı koduna vermedi ve isteği net::ERR_FAILED ile başarısız saydı. Network sekmesinde de bu isteğin Status sütununda CORS error yazar. İlk satırı parçala: "127.0.0.1:5500 kökeninden gelen bu fetch isteği CORS politikası yüzünden engellendi: istenen kaynakta Access-Control-Allow-Origin başlığı yok."

Önce mesajdaki origin (köken) sözcüğü. Kavramı Ders 23'ten tanıyorsun: localStorage kökene bağlıydı, 127.0.0.1:5500 ile localhost:5500 ayrı depolar kullanıyordu. Ders 7'deki Git uzak deposu origin ile ise yalnız adı aynı.

Sayfan http://127.0.0.1:5500/index.html olsun. İstek attığı adreslerin kökeni onunkiyle aynı mı?

İstek attığı adresAynı köken mi?
http://127.0.0.1:5500/veri/champion.jsonEvet: yalnız yol farklı
http://127.0.0.1:3000/Hayır: port farklı
http://localhost:5500/Hayır: alan adı farklı (aynı bilgisayar olsa da)
https://127.0.0.1:5500/Hayır: protokol farklı
https://ddragon.leagueoflegends.com/…Hayır: üçü de farklı

Tarayıcının temel güvenlik kuralı aynı köken kuralıdır (same-origin policy): bir sayfadaki JavaScript, yalnız kendi kökeninden gelen cevapları okuyabilir. Sebebi senin güvenliğin. Bu kural olmasaydı girdiğin herhangi bir site, arka planda bankanın sitesine istek atıp cevabı okuyabilirdi; tarayıcın o isteğe senin oturum bilgilerini de ekleyebildiği için cevapta senin hesabın olurdu.

Ama farklı kökenden veri okumak her zaman yasak değil. Sunucu izin verebilir. İznin adı CORS.

1. Data Dragon'dan istemek Sayfan 127.0.0.1:5500 fetch(adres) Sunucunun cevabı 200, JSON ve izin: Access-Control- Allow-Origin: * Tarayıcı: izin var ✓ JSON, kodundaki yanit.json() satırına ulaşır 2. leagueoflegends.com'dan istemek Sayfan 127.0.0.1:5500 fetch(adres) Sunucunun cevabı 200, sayfa geldi ama izin başlığı yok Tarayıcı: izin yok ✗ cevap koduna verilmez: Failed to fetch
Şekil 24.7 İki istek de sunucuya ulaştı ve cevap döndü. Cevabı koduna vermeyen sunucu değil, tarayıcı.

Data Dragon'un iznini Network sekmesinde zaten gördün: champion.json'un Response headers bölümündeki Access-Control-Allow-Origin: * satırı (Network şeklindeki 6). Yıldız "her kökenden gelen sayfa bu cevabı okuyabilir" demek. Aynı bölmede biraz aşağı inersen Request headers bölümünde Origin: http://127.0.0.1:5500 satırını da görürsün: tarayıcı başka bir kökene istek atarken sayfanın nereden geldiğini kendisi yazar.

İstersen aynı başlıklara terminalden de bakabilirsin. curl.exe adresteki dosyayı indiren bir komut satırı aracı; -I yalnız başlıkları ister, -H isteğe bir başlık ekler. Bu aracı Ders 26'da ayrıntılı göreceksin, şimdilik kalıp olarak yaz:

Terminal — PowerShell
PS C:\kod\sampiyon-listesi> curl.exe -I -H "Origin: http://127.0.0.1:5500" https://ddragon.leagueoflegends.com/cdn/16.20.1/data/tr_TR/champion.jsonHTTP/1.1 200 OKContent-Type: application/jsonContent-Length: 162972Connection: keep-aliveAccess-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, HEADAccess-Control-Max-Age: 3000...

Origin başlığıyla tarayıcının yaptığını taklit ettik, "ben bu kökenden geliyorum" dedik; sunucu Access-Control-Allow-Origin: * ile cevap verdi. Data Dragon bu satırı her cevaba koyuyor, -H kısmını silsen de gelir; bazı sunucular ise izni yalnız Origin taşıyan isteklere yazar. Content-Length: 162972 da dosyanın sıkıştırılmamış boyutu (163 kB). Windows PowerShell'de .exe'yi yazmayı unutma: tek başına curl, PowerShell'de başka bir komutun takma adıdır ve bu seçenekleri anlamaz.

Node'da neden böyle bir hata yoktu?

Aynı kodu Node'da çalıştır. Hiçbir engel çıkmaz; sayfa gelir:

deneme.js
async function yamaNotlariniGetir() {    const yanit = await fetch("https://www.leagueoflegends.com/tr-tr/news/game-updates/")    const sayfa = await yanit.text()    console.log(yanit.status, sayfa.length)} yamaNotlariniGetir()
Çıktıörnek, sende farklı çıkabilir
200 517981

CORS bir tarayıcı kuralıdır. Tarayıcı başkalarının yazdığı sayfaları senin bilgisayarında, senin oturumlarınla çalıştırır; bu yüzden onları birbirinden korumak zorunda. Node'daki program ise senin kendi programın, kimsenin sayfası değil; tarayıcının denetimi orada yok. Bu fark çözüm yolunu da gösteriyor.

CORS hatası alınca ne yaparsın?

Hatanın çözümü senin sayfanın kodunda değil, cevabı veren sunucuda. İzni sunucu verir; sen sayfadan kendine izin veremezsin.

  • Sunucu senin değilse: O sitenin bu iş için bir API'si var mı bak. Riot'un oyun verisi için Data Dragon izin veriyor; yama notları sayfası ise bir API değil, insanlar için yapılmış bir web sayfası.
  • Ara sunucu: Tarayıcı kendi sunucuna sorar, senin sunucun (Node, CORS'a takılmaz) asıl siteye sorar ve cevabı geri verir. Bunu Ders 30'da yazacaksın.
  • Sunucu seninse: Ya cevabına izin başlığını eklersin ya da sayfanı ve API'ni aynı sunucudan, aynı kökenden verirsin. Blok 4'te kendi API'ni yazınca sayfanı da aynı sunucudan vereceksin; köken aynı olduğu için CORS'a hiç takılmayacaksın.

Şunları yapma: isteğe mode: "no-cors" eklemek hatayı susturur ama cevap "opak" gelir: durum kodu 0, gövde boş, içini yine okuyamazsın. İnternette bulduğun hazır "CORS proxy" siteleri verini başkasının sunucusundan geçirir. CORS'u kapatan tarayıcı eklentileri de kendi güvenliğini kapatır ve sayfanı açan başka hiç kimsenin tarayıcısında işe yaramaz.

Sık hatalar

Bu derste karşılaştığın hataların çoğu bölümlerin içinde. Birkaçı daha:

Sık hataGET …/champion.json 403 (Forbidden)

Ne zaman çıkar? Adresteki sürüm, dil kodu ya da dosya adı yanlış olunca. Data Dragon'da olmayan her dosya 403 (Forbidden, yasak) döndürür; çoğu site böyle bir durumda 404 (Not Found, bulunamadı) döndürür. Dersteki sayfada ${surum} yerine 16.99.1 yazınca konsolda iki kırmızı satır çıkar:

Konsol (F12 → Console)
GET https://ddragon.leagueoflegends.com/cdn/16.99.1/data/tr_TR/champion.json 403 (Forbidden)Error: İstek başarısız: 403    at jsonGetir (uygulama.js:22:15)    at async yukle (uygulama.js:34:22)

İlk satırı tarayıcı kendisi yazar: başarısız biten her isteği, senin kodun hatayı yakalasa bile konsola not eder. Satırdaki adrese tıklarsan Network sekmesi açılır; o istek orada 403 durumuyla kırmızı görünür. İkinci satır console.error(hata)'nın çıktısı. fetch 403'te hata fırlatmadı; hatayı jsonGetir'deki ok denetimi fırlattı. O denetim olmasaydı Ders 15'teki SyntaxError: Unexpected token '<', "<?xml vers"... is not valid JSON hatasını görürdün: json(), Data Dragon'un XML hata sayfasını JSON sanıp çevirmeye çalışırdı.

Nasıl düzelir? Kırmızı satırdaki adresi oku: sürüm versions.json'da var mı, dil kodu tam olarak tr_TR mi, dosya adı verideki gibi mi (büyük-küçük harf dahil)? Adres düzelince istek 200 döner.

Sık hataTypeError: Failed to fetch

Ne zaman çıkar? Tarayıcı kullanılabilir hiçbir cevap alamayınca: internet yoksa, alan adı yanlışsa ya da CORS izni yoksa. Node'daki karşılığı TypeError: fetch failed idi (Ders 15). Sayfamız bu hatayı yakalar ve ekrana hata durumunu yazar. Wi-Fi kapalıyken konsolda önce tarayıcının ağ satırı, sonra console.error'un yazdığı ayrıntı görünür:

Konsol (F12 → Console)
GET https://ddragon.leagueoflegends.com/api/versions.json net::ERR_INTERNET_DISCONNECTEDTypeError: Failed to fetch    at jsonGetir (uygulama.js:20:25)    at yukle (uygulama.js:32:32)    at uygulama.js:105:1

Failed to fetch sebebi söylemez; sebebi hemen üstündeki GET satırının sonunda ya da Network sekmesinde ararsın: net::ERR_INTERNET_DISCONNECTED internetin yok, net::ERR_NAME_NOT_RESOLVED alan adı bulunamadı demek. CORS'ta o satırda net::ERR_FAILED yazar, Network'ün Status sütununda da CORS error.

Nasıl düzelir? Önce adresi tarayıcının adres çubuğunda aç: açılmıyorsa sorun bağlantıda ya da adreste. Açılıyorsa ama fetch başarısızsa konsolda CORS mesajı var mı bak.

Sık hataKartlarda İngilizce unvanlar var

Ne zaman çıkar? Adresteki dil kodu en_US kalınca; çoğu zaman internette bulduğun bir örneği olduğu gibi kopyaladığında. Hata mesajı yoktur; Ahri'nin kartında "Dokuz Kuyruklu Tilki" yerine "the Nine-Tailed Fox" yazar.

Nasıl düzelir? Adresteki dil kodunu tr_TR yap. Büyük-küçük harfe ve alt çizgiye dikkat: tr_tr ya da tr-TR yazarsan Data Dragon 403 döndürür ve hata durumuna düşersin. Desteklenen bütün dil kodları https://ddragon.leagueoflegends.com/cdn/languages.json adresinde.

Görevler

Görevlerin her birini sampiyon-listesi klasöründe yap. Sayfa görevlerinde yeni bir .html ve .js dosyası aç; Live Server açıkken adres çubuğunda dosya adını değiştirerek (/buyuler.html gibi) açabilirsin. Data Dragon'un başka dosyalarını tarayıcıda açıp önce gözünle okumayı unutma.

Görev 1

Sihirdar büyüleri

buyuler.html ve buyuler.js adında yeni bir sayfa yaz. Sayfa en yeni sürümün sihirdar büyüleri dosyasını Türkçe çeksin:

Metin
https://ddragon.leagueoflegends.com/cdn/<sürüm>/data/tr_TR/summoner.json

Dosyada başka oyun modlarının büyüleri de var; yalnız Sihirdar Vadisi'nde kullanılanları, yani modes dizisinde "CLASSIC" olanları bir listede Sıçra · 300 sn biçiminde göster. Yüklenirken "Büyüler yükleniyor…", bitince kaç büyü olduğunu yazsın. 16.20.1 sürümünde ekranda şunlar olmalı:

Metin
Sihirdar Vadisi'nde 9 büyü var.Bariyer · 180 snArındır · 240 snTutuştur · 180 snBitkinlik · 240 snSıçra · 300 snHayalet · 240 snŞifa · 240 snÇarp · 15 snIşınlan · 300 sn
İpucu 1

Önce adresi tarayıcıda aç ve bir büyünün alanlarına bak: name, cooldown, modes. Dosyanın yapısı champion.json'a benziyor: büyüler data objesinin içinde.

İpucu 2

Object.values(veri.data) ile diziye çevir, filter ile buyu.modes.includes("CLASSIC") olanları seç. Bekleme süresi bir dizi: cooldown[0].

İpucu 3

Her büyü için createElement("li"), textContent ve listeye append. jsonGetir'i bu dosyaya da kopyala; iki istek, ders sayfasındaki gibi sırayla.

Çözümü göster
buyuler.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <title>Sihirdar Büyüleri</title>    <script src="buyuler.js" defer></script></head><body>    <h1>Sihirdar Büyüleri</h1>    <p id="buyuDurum">Büyüler yükleniyor…</p>    <ul id="buyuListesi"></ul></body></html>
buyuler.js
const buyuDurum = document.querySelector("#buyuDurum")const buyuListesi = document.querySelector("#buyuListesi") async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} async function buyuleriYukle() {    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surumler[0]}/data/tr_TR/summoner.json`)        const vadiBuyuleri = Object.values(veri.data).filter((buyu) => buyu.modes.includes("CLASSIC"))        for (const buyu of vadiBuyuleri) {            const satir = document.createElement("li")            satir.textContent = `${buyu.name} · ${buyu.cooldown[0]} sn`            buyuListesi.append(satir)        }        buyuDurum.textContent = `Sihirdar Vadisi'nde ${vadiBuyuleri.length} büyü var.`    } catch (hata) {        console.error(hata)        buyuDurum.textContent = "Büyüler yüklenemedi."    }} buyuleriYukle()

Sınır denemesi: summoner.json'da 34 büyü var; süzgeci kaldırırsan aynı adlı büyüler birden çok kez görünür (Bariyer iki, Sıçra üç kez: her biri başka bir oyun modu için). Sürüm adresini 16.99.1 yaparsan ekranda "Büyüler yüklenemedi." yazmalı, konsolda Error: İstek başarısız: 403 görünmeli.

Bir de CORS'u dene: versions.json adresini yama notları sayfasının adresiyle (https://www.leagueoflegends.com/tr-tr/news/game-updates/) değiştir. Ekranda yine "Büyüler yüklenemedi." yazar, ama konsolda bu kez Access to fetch at … has been blocked by CORS policy ile başlayan satır ve TypeError: Failed to fetch görünür. Kullanıcıya giden mesaj iki denemede de aynı, sebep farklı; sebebi hep konsoldan okursun. Denemeden sonra adresi geri al.

Görev 2

Yeniden dene düğmesi

Ders sayfasında yükleme başarısız olunca kullanıcı sayfayı yenilemek zorunda. Hata durumunda mesajın altında bir Yeniden dene düğmesi çıksın; düğmeye basınca yükleme baştan denensin, yükleme sırasında ve başarıdan sonra düğme görünmesin. Mesajı da "…denetleyip yeniden dene." diye değiştir. Denemek için Network'te Disable cache işaretliyken Wi-Fi'ı kapatıp sayfayı yenile: hata ve düğme çıkmalı. Düğmeye basarsan yine hata gelmeli ve düğme yine görünmeli. Wi-Fi'ı açıp düğmeye basınca kartlar gelmeli, düğme kaybolmalı.

İpucu 1

Düğmeyi HTML'e baştan koy ama gizli: <button type="button" id="tekrar" hidden>Yeniden dene</button>. JavaScript'te tekrarDugmesi.hidden = true gizler, false gösterir.

İpucu 2

Düğmeyi yukle gizlesin (en başta) ve catch göstersin. Tıklama olayına yeni bir fonksiyon yazma; yukle'nin kendisini ver.

Çözümü göster

index.html'de #durum paragrafının hemen altına düğmeyi ekle:

index.html
<p id="durum" aria-live="polite">Şampiyonlar yükleniyor…</p><button type="button" id="tekrar" hidden>Yeniden dene</button>

uygulama.js'de beş satır değişiyor ya da ekleniyor (vurgulu satırlar). İlk satırı öteki querySelector satırlarının yanına, son satırı öteki olay dinleyicilerinin yanına koy:

uygulama.js
const tekrarDugmesi = document.querySelector("#tekrar") async function yukle() {    durum.textContent = "Şampiyonlar yükleniyor…"    durum.classList.remove("hata")    tekrarDugmesi.hidden = true    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        surum = surumler[0]        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/champion.json`)        sampiyonlar = Object.values(veri.data)        surumYazisi.textContent = `Yama ${surum} · ${sampiyonlar.length} şampiyon`        ciz()    } catch (hata) {        console.error(hata)        durum.textContent = "Şampiyonlar yüklenemedi. İnternet bağlantını denetleyip yeniden dene."        durum.classList.add("hata")        tekrarDugmesi.hidden = false    }} tekrarDugmesi.addEventListener("click", yukle)

Bunu denedik: ilk yüklemede istek başarısız olunca hata ve düğme çıktı; düğmeye basıldığında istek yine başarısızdı, hata ve düğme yeniden göründü; bağlantı gelince düğmeyle 173 kart yüklendi ve düğme gizlendi. yukle her başladığında düğmeyi gizlediği için kullanıcı yükleme sürerken ikinci kez basamaz.

Görev 3

Sihirdar simgeleri

simgeler.html ve simgeler.js adında bir sayfa yaz. Sayfa en yeni sürümün sihirdar simgeleri dosyasını çeksin:

Metin
https://ddragon.leagueoflegends.com/cdn/<sürüm>/data/tr_TR/profileicon.json

Her simgeyi 64 × 64 piksellik bir resim olarak göster; resimlerin hepsi geç yüklensin. Yan yana dizmek için CSS gerekmez: <img> satır içi (inline) bir öğedir (Ders 21), satır dolunca alta geçer. Yüklenirken "Simgeler yükleniyor…", bitince kaç simge olduğunu yazsın. 16.20.1 sürümünde şu çıkmalı:

Metin
5126 simge var. Aşağı kaydırdıkça yüklenecekler.

Sonra Network sekmesinde ölç: Img süzgecini seç, sayfayı yenile ve panelin en altındaki sayaçtan ilk açılışta kaç simge istendiğini oku. Sayfayı biraz aşağı kaydırıp yeniden oku.

İpucu 1

Önce adresi tarayıcıda aç: data objesinde her simge { id, image: { full } } biçiminde. Resmin adresi şampiyon resmininkine benzer: …/cdn/<sürüm>/img/profileicon/<image.full>.

İpucu 2

Sürümü iki adreste kullanacaksın, önce bir değişkene koy: const surum = surumler[0]. Resim satırları için kartOlustur'a bak: width, height, loading. Simgelerin adı yok; alt için Simge ${simge.id} yazabilirsin.

Çözümü göster
simgeler.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <title>Sihirdar Simgeleri</title>    <script src="simgeler.js" defer></script></head><body>    <h1>Sihirdar simgeleri</h1>    <p id="simgeDurum">Simgeler yükleniyor…</p>    <div id="galeri"></div></body></html>
simgeler.js
const simgeDurum = document.querySelector("#simgeDurum")const galeri = document.querySelector("#galeri") async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} async function simgeleriYukle() {    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        const surum = surumler[0]        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/profileicon.json`)        const simgeler = Object.values(veri.data)        for (const simge of simgeler) {            const resim = document.createElement("img")            resim.src = `https://ddragon.leagueoflegends.com/cdn/${surum}/img/profileicon/${simge.image.full}`            resim.alt = `Simge ${simge.id}`            resim.width = 64            resim.height = 64            resim.loading = "lazy"            galeri.append(resim)        }        simgeDurum.textContent = `${simgeler.length} simge var. Aşağı kaydırdıkça yüklenecekler.`    } catch (hata) {        console.error(hata)        simgeDurum.textContent = "Simgeler yüklenemedi."    }} simgeleriYukle()

Chrome 153 ile denedik: 1000 × 760 piksellik pencerede ilk açılışta 5126 simgenin 420'si istendi, 2000 piksel aşağı kaydırınca sayı 870'e çıktı. Telefon boyutunda (390 × 844) ilk açılışta 150 simge istendi. Senin sayıların pencerenin boyutuna göre değişir; önemli olan, 5126'dan çok daha az olmaları ve kaydırdıkça artmaları.

Bu sayfada resim.loading = "lazy" satırını silip denemeye kalkma: 5126 isteğin hepsi birden gider. Simgelerden 302 tanesinin boyutuna baktık, ortalaması 115 kB; hepsi birlikte yarım gigabaytı geçer. Telefonundan mobil veriyle giren biri için bu, hiç bakmayacağı resimlere harcanan yarım gigabayt demek.

Görev 4

Bütçene göre eşya

esyalar.html ve esyalar.js adında bir sayfa yaz. Sayfa en yeni sürümün item.json dosyasını (…/data/tr_TR/item.json) çeksin ve bir sayı kutusuna yazılan altınla Sihirdar Vadisi'nde alınabilecek eşyaları, en ucuzdan pahalıya listelesin: Can İksiri · 50 altın. Bir eşya şu üç koşulu sağlamalı: satın alınabilir (gold.purchasable true), Sihirdar Vadisi'nde var (maps["11"] true) ve tek bir şampiyona özel değil (requiredChampion alanı yok). Fiyat gold.total alanında. Yazı şu durumları doğru göstersin:

  • Veri gelene kadar kutu kapalı ve "Eşyalar yükleniyor…" yazıyor. Bir form öğesini kapatmak için HTML'de disabled niteliğini yazarsın: <input … disabled>. Kapalı kutuya yazı yazılamaz. Ders 23'teki hidden gibi, JavaScript'te aynı adlı bir özelliktir: altinKutusu.disabled = false kutuyu açar.
  • Kutu boşsa: "Ne kadar altının olduğunu yaz."
  • Hiç eşya yoksa (örneğin -5): "-5 altınla alınabilecek eşya yok."
  • Varsa: "50 altınla 4 eşya alabilirsin."

16.20.1 sürümünde 0 altınla 3, 49 ile 3, 50 ile 4, 75 ile 5 eşya çıkmalı.

İpucu 1

item.json büyük bir dosya; tarayıcıda açıp bir eşyaya bak: name, gold: { total, purchasable }, maps: { "11": true, … }. "11" Sihirdar Vadisi'nin harita numarası; anahtar sayıyla başladığı için köşeli parantezle okunur.

İpucu 2

Süzme işini DOM'dan ayrı bir fonksiyona koy: alinabilirler(tumEsyalar, altin) uygun eşyaları ucuzdan pahalıya sıralı döndürsün (filter, ardından toSorted). Önce Node'da birkaç elle yazılmış eşyayla dene.

İpucu 3

Kutunun değeri metindir: boş mu diye altinKutusu.value === "" ile bak, değilse Number ile sayıya çevir (Ders 23). Her input olayında listeyi boşaltıp baştan çiz.

Çözümü göster

Önce süzme fonksiyonunu Node'da, sınır değerleriyle dene. Örnek eşyalar item.json'dan alındı:

deneme.js
function alinabilirler(tumEsyalar, altin) {    return tumEsyalar        .filter((esya) => esya.gold.purchasable && esya.maps["11"] && !esya.requiredChampion)        .filter((esya) => esya.gold.total <= altin)        .toSorted((a, b) => a.gold.total - b.gold.total)} const ornekler = [    { name: "Kontrol Totemi", gold: { total: 75, purchasable: true }, maps: { "11": true } },    { name: "Can İksiri", gold: { total: 50, purchasable: true }, maps: { "11": true } },    { name: "Görünmez Totem", gold: { total: 0, purchasable: true }, maps: { "11": true } },    { name: "Kalista'nın Kara Kargısı", gold: { total: 0, purchasable: true }, maps: { "11": true }, requiredChampion: "Kalista" },    { name: "Can Simidi", gold: { total: 1600, purchasable: true }, maps: { "11": false } },    { name: "Ebedi Kılıç", gold: { total: 3500, purchasable: true }, maps: { "11": true } }] for (const altin of [-5, 0, 49, 50, 75, 3500]) {    const adlar = alinabilirler(ornekler, altin).map((esya) => esya.name)    console.log(altin, adlar)}
Çıktı
-5 []0 [ 'Görünmez Totem' ]49 [ 'Görünmez Totem' ]50 [ 'Görünmez Totem', 'Can İksiri' ]75 [ 'Görünmez Totem', 'Can İksiri', 'Kontrol Totemi' ]3500 [ 'Görünmez Totem', 'Can İksiri', 'Kontrol Totemi', 'Ebedi Kılıç' ]

Kalista'ya özel kargı 0 altına rağmen hiç çıkmıyor, ARAM'a özel Can Simidi de. 49 ile 50 arasındaki fark <='nun işi: < yazsaydın tam 50 altının Can İksiri'ni almaya yetmezdi.

esyalar.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <title>Bütçene Göre Eşya</title>    <script src="esyalar.js" defer></script></head><body>    <h1>Bütçene göre eşya</h1>    <label>        Altının        <input type="number" id="altinKutusu" min="0" step="50" disabled>    </label>    <p id="esyaDurum">Eşyalar yükleniyor…</p>    <ul id="esyaListesi"></ul></body></html>
esyalar.js
const altinKutusu = document.querySelector("#altinKutusu")const esyaDurum = document.querySelector("#esyaDurum")const esyaListesi = document.querySelector("#esyaListesi") let esyalar = [] async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} function alinabilirler(tumEsyalar, altin) {    return tumEsyalar        .filter((esya) => esya.gold.purchasable && esya.maps["11"] && !esya.requiredChampion)        .filter((esya) => esya.gold.total <= altin)        .toSorted((a, b) => a.gold.total - b.gold.total)} function esyalariCiz() {    esyaListesi.textContent = ""    if (altinKutusu.value === "") {        esyaDurum.textContent = "Ne kadar altının olduğunu yaz."        return    }    const altin = Number(altinKutusu.value)    const uygunlar = alinabilirler(esyalar, altin)    for (const esya of uygunlar) {        const satir = document.createElement("li")        satir.textContent = `${esya.name} · ${esya.gold.total} altın`        esyaListesi.append(satir)    }    if (uygunlar.length === 0) {        esyaDurum.textContent = `${altin} altınla alınabilecek eşya yok.`    } else {        esyaDurum.textContent = `${altin} altınla ${uygunlar.length} eşya alabilirsin.`    }} async function esyalariYukle() {    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surumler[0]}/data/tr_TR/item.json`)        esyalar = Object.values(veri.data)        altinKutusu.disabled = false        esyalariCiz()    } catch (hata) {        console.error(hata)        esyaDurum.textContent = "Eşyalar yüklenemedi."    }} altinKutusu.addEventListener("input", esyalariCiz)esyalariYukle()

Sayfada denedik (16.20.1): 0 → 3 eşya (Görünmez Totem, Uzak Görüş Dönüşümü, Kâhin Merceği), 49 → 3, 50 → 4, 74 → 4, 75 → 5, 3499 → 249, 3500 → 251, -5 → "alınabilecek eşya yok". Listede aynı adlı iki eşya görürsen şaşırma: item.json'da bazı eşyalar farklı anahtarlarla iki kez kayıtlı (Tanrıça'nın Gözyaşı hem 3070 hem 323070 anahtarında) ve ikisi de üç koşulu sağlıyor. Kutu veri gelmeden açık olsaydı, her sayı boş diziden süzülür ve "eşya yok" yazardı: yanlış bir boş durum.

Görev 5

Kaynak türleri

Her şampiyonun bir kaynak türü var: partype alanı (Mana, Enerji, Öfke…). kaynaklar.html ve kaynaklar.js adında bir sayfa yaz: en yeni champion.json'u çeksin, hangi kaynak türünü kaç şampiyonun kullandığını sayıp çoktan aza sıralı bir liste göstersin, üstte de toplamı yazsın. Dikkat: verideki bir şampiyonun partype'ı boş metin (""); onu "Belirtilmemiş" diye say. 16.20.1 sürümünde liste böyle başlamalı:

Metin
173 şampiyon, 14 farklı kaynak türü.Mana: 145Enerji: 6Yok: 6Öfke: 4
İpucu 1

Saymak için boş bir obje kullan: anahtar kaynak türü, değer sayı. Her şampiyonda sayilar[tur] = (sayilar[tur] ?? 0) + 1 (Ders 12'deki ??).

İpucu 2

Boş metin yanlış sayılan (falsy) bir değerdir: sampiyon.partype || "Belirtilmemiş" boşsa yedek adı verir.

İpucu 3

Object.entries(sayilar) [tür, sayı] çiftlerinden bir dizi verir; toSorted((a, b) => b[1] - a[1]) çoktan aza sıralar. Listeyi yazarken çifti parçala: for (const [tur, sayi] of sirali).

Çözümü göster

Sayma fonksiyonunu önce Node'da dene. Örneklerdeki kaynak türleri gerçek veriden; Bel'Veth'inki gerçekten boş:

deneme.js
function kaynakSay(sampiyonlar) {    const sayilar = {}    for (const sampiyon of sampiyonlar) {        const tur = sampiyon.partype || "Belirtilmemiş"        sayilar[tur] = (sayilar[tur] ?? 0) + 1    }    return Object.entries(sayilar).toSorted((a, b) => b[1] - a[1])} const ornekler = [    { name: "Ahri", partype: "Mana" },    { name: "Garen", partype: "Yok" },    { name: "Akali", partype: "Enerji" },    { name: "Bel'Veth", partype: "" },    { name: "Zed", partype: "Enerji" },    { name: "Lux", partype: "Mana" },    { name: "Shen", partype: "Enerji" }] console.log(kaynakSay(ornekler))console.log(kaynakSay([]))
Çıktı
[  [ 'Enerji', 3 ],  [ 'Mana', 2 ],  [ 'Yok', 1 ],  [ 'Belirtilmemiş', 1 ]][]

Boş dizi boş bir liste verir, çökme yok. Sayıları eşit olanlar (Yok ve Belirtilmemiş) ilk görüldükleri sırada kalır: toSorted eşitleri yer değiştirmez.

kaynaklar.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <title>Kaynak Türleri</title>    <script src="kaynaklar.js" defer></script></head><body>    <h1>Şampiyonların kaynak türleri</h1>    <p id="kaynakDurum">Şampiyonlar yükleniyor…</p>    <ol id="kaynakListesi"></ol></body></html>
kaynaklar.js
const kaynakDurum = document.querySelector("#kaynakDurum")const kaynakListesi = document.querySelector("#kaynakListesi") async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} function kaynakSay(sampiyonlar) {    const sayilar = {}    for (const sampiyon of sampiyonlar) {        const tur = sampiyon.partype || "Belirtilmemiş"        sayilar[tur] = (sayilar[tur] ?? 0) + 1    }    return Object.entries(sayilar).toSorted((a, b) => b[1] - a[1])} async function kaynaklariYukle() {    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        const veri = await jsonGetir(`https://ddragon.leagueoflegends.com/cdn/${surumler[0]}/data/tr_TR/champion.json`)        const sampiyonlar = Object.values(veri.data)        const sirali = kaynakSay(sampiyonlar)        for (const [tur, sayi] of sirali) {            const satir = document.createElement("li")            satir.textContent = `${tur}: ${sayi}`            kaynakListesi.append(satir)        }        kaynakDurum.textContent = `${sampiyonlar.length} şampiyon, ${sirali.length} farklı kaynak türü.`    } catch (hata) {        console.error(hata)        kaynakDurum.textContent = "Şampiyonlar yüklenemedi."    }} kaynaklariYukle()

Sayfada 16.20.1 ile çıkan liste: Mana 145, Enerji 6, Yok 6, Öfke 4, Hiddet 2, Akış 2, sonra birer şampiyonla Kan Kuyusu, Belirtilmemiş, Cesaret, Kalkan, Hırs, Isı, İnat, Kızıl Akım. Sayıların toplamı 173 olmalı; olmuyorsa bir şampiyonu atlıyor ya da iki kez sayıyorsun.

Görev 6

İki sürüm arasında gelen şampiyonlar

surumler.html ve surumler.js adında bir sayfa yaz. İki <select> en yeni 20 sürümle dolsun; varsayılan olarak "eski"de ikinci, "yeni"de birinci sürüm seçili olsun. Karşılaştır düğmesine basınca iki sürümün champion.json'u aynı anda istensin ve yeni sürümde olup eskisinde olmayan şampiyonların adları listelensin. Durum yazısı "16.12.1 → 16.13.1: 1 yeni şampiyon." ya da "16.20.1 → 16.20.1: yeni şampiyon yok." gibi olsun. Sürümler yüklenene kadar düğme kapalı kalsın.

İpucu 1

<option>'ları createElement("option") ile yap; value ve textContent'e sürümü yaz. versions.json dizisinin ilk 20 elemanı için slice(0, 20).

İpucu 2

İki isteğin birbirini beklemesi için sebep yok: const [eskiVeri, yeniVeri] = await Promise.all([...]) (Ders 15 ve Ders 12'deki dizi parçalama).

İpucu 3

Yeni gelenler: yeni verinin anahtarlarından (Object.keys(yeniVeri.data)), eski veride karşılığı undefined olanlar. Ekrana anahtarı değil name'i yaz.

Çözümü göster

Karşılaştırma fonksiyonunu önce Node'da dene:

deneme.js
function yeniGelenler(eskiVeri, yeniVeri) {    return Object.keys(yeniVeri.data)        .filter((id) => eskiVeri.data[id] === undefined)        .map((id) => yeniVeri.data[id].name)} const eski = { data: { Ahri: { name: "Ahri" }, Garen: { name: "Garen" } } }const yeni = { data: { Ahri: { name: "Ahri" }, Garen: { name: "Garen" }, MonkeyKing: { name: "Wukong" } } } console.log(yeniGelenler(eski, yeni))console.log(yeniGelenler(yeni, yeni))console.log(yeniGelenler(yeni, eski))
Çıktı
[ 'Wukong' ][][]

Anahtar MonkeyKing ama ekrana Wukong yazılıyor. Aynı sürüm iki kez seçilince de, sürümler ters seçilince de liste boş: şampiyonlar oyundan çıkarılmadığı için eskiden yeniye gidince yalnız eklenenler görünür.

surumler.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <title>Yeni Şampiyonlar</title>    <script src="surumler.js" defer></script></head><body>    <h1>İki sürüm arasında gelen şampiyonlar</h1>    <label>        Eski sürüm        <select id="eskiSurum"></select>    </label>    <label>        Yeni sürüm        <select id="yeniSurum"></select>    </label>    <button type="button" id="karsilastir" disabled>Karşılaştır</button>    <p id="farkDurum">Sürümler yükleniyor…</p>    <ul id="yenilerListesi"></ul></body></html>
surumler.js
const eskiSecim = document.querySelector("#eskiSurum")const yeniSecim = document.querySelector("#yeniSurum")const karsilastirDugmesi = document.querySelector("#karsilastir")const farkDurum = document.querySelector("#farkDurum")const yenilerListesi = document.querySelector("#yenilerListesi") async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error("İstek başarısız: " + yanit.status)    }    const veri = await yanit.json()    return veri} function sampiyonAdresi(surum) {    return `https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/champion.json`} function yeniGelenler(eskiVeri, yeniVeri) {    return Object.keys(yeniVeri.data)        .filter((id) => eskiVeri.data[id] === undefined)        .map((id) => yeniVeri.data[id].name)} function secenekEkle(secim, surum) {    const secenek = document.createElement("option")    secenek.value = surum    secenek.textContent = surum    secim.append(secenek)} async function surumleriYukle() {    try {        const surumler = await jsonGetir("https://ddragon.leagueoflegends.com/api/versions.json")        for (const surum of surumler.slice(0, 20)) {            secenekEkle(eskiSecim, surum)            secenekEkle(yeniSecim, surum)        }        eskiSecim.value = surumler[1]        karsilastirDugmesi.disabled = false        farkDurum.textContent = "İki sürüm seç ve Karşılaştır'a bas."    } catch (hata) {        console.error(hata)        farkDurum.textContent = "Sürümler yüklenemedi."    }} async function karsilastir() {    const eski = eskiSecim.value    const yeni = yeniSecim.value    farkDurum.textContent = "Şampiyon listeleri yükleniyor…"    yenilerListesi.textContent = ""    try {        const [eskiVeri, yeniVeri] = await Promise.all([            jsonGetir(sampiyonAdresi(eski)),            jsonGetir(sampiyonAdresi(yeni))        ])        const yeniler = yeniGelenler(eskiVeri, yeniVeri)        for (const ad of yeniler) {            const satir = document.createElement("li")            satir.textContent = ad            yenilerListesi.append(satir)        }        if (yeniler.length === 0) {            farkDurum.textContent = `${eski} → ${yeni}: yeni şampiyon yok.`        } else {            farkDurum.textContent = `${eski} → ${yeni}: ${yeniler.length} yeni şampiyon.`        }    } catch (hata) {        console.error(hata)        farkDurum.textContent = "Şampiyon listeleri yüklenemedi."    }} karsilastirDugmesi.addEventListener("click", karsilastir)surumleriYukle()

Sayfada denedik: 16.19.1 → 16.20.1 yeni şampiyon yok; 16.12.1 → 16.13.1 ve 16.1.1 → 16.20.1 yalnız Locke; 16.20.1 → 16.1.1 (ters) yok. Gerçek veri sürpriz de saklıyor: 16.14.1 → 16.15.1 60 "yeni şampiyon" gösteriyor ve listede Ahri, Alistar, Amumu gibi eski adlar var. O sürümün champion.json'unda 173 değil 233 kayıt var: Jade_Ahri gibi 60 ek kaydın kimliği farklı, adı ve unvanı eski şampiyonlarla aynı. Sonraki sürümde liste yine 173'e dönmüş. Kodun doğru, veri tuhaf: dışarıdan gelen veriye körü körüne güvenme, şüpheli bir sonuç görünce kaynağı tarayıcıda aç ve gözünle bak.

Kendini sına

Soru 1

Konsola hangi sırayla ne yazılır?

JavaScript
console.log("1") async function yukle() {    console.log("2")    const yanit = await fetch("https://ddragon.leagueoflegends.com/api/versions.json")    console.log("3")} yukle()console.log("4")
Cevabı göster

1, 2, 4, 3. yukle() çağrılınca fonksiyon ilk await'e kadar hemen çalışır ve 2 yazar. await fonksiyonu duraklatır, sıra çağrının altındaki satıra geçer: 4. Cevap gelince fonksiyon kaldığı yerden devam eder: 3. Sayfa bu bekleme sırasında donmaz.

Soru 2

Sayfan http://127.0.0.1:5500/index.html. Hangi adreslere yapılan istekler aynı kökene gider?

  1. http://127.0.0.1:5500/veri/champion.json
  2. http://localhost:5500/index.html
  3. http://127.0.0.1:3000/
  4. https://127.0.0.1:5500/
Cevabı göster

Yalnız 1. Köken protokol, alan adı ve port üçlüsüdür; yol sayılmaz. 2'de alan adı farklı (localhost, aynı bilgisayarı gösterse de başka bir ad), 3'te port, 4'te protokol farklı. Bu üçüne yapılan istekler ancak sunucu CORS ile izin verirse okunabilir.

Soru 3

Kullanıcı Jarvan IV yazınca şampiyon bulunuyor, jarvan iv yazınca bulunmuyor. Neden?

JavaScript
function uyuyorMu(ad, aranan) {    return ad.toLocaleLowerCase("tr").includes(aranan.toLocaleLowerCase("tr"))}
Cevabı göster

Türkçe kural büyük I'yı noktasız ı yapar: "Jarvan IV" küçülünce "jarvan ıv" olur. Kullanıcı Jarvan IV yazınca o da aynı biçime iner ve eşleşir. jarvan iv ise noktalı i ile yazılmış; "jarvan ıv"'in içinde "jarvan iv" yok. Denedik: birincisi true, ikincisi false. Çözüm, iki tarafı da küçülttükten sonra ı harflerini i yapmak: dersteki aramaBicimi.

Soru 4

CORS hatası aldın. Kendi sayfanın kodunu değiştirerek düzeltebilir misin? Aynı istek Node'da neden çalışıyor?

Cevabı göster

Hayır. İzni cevabı veren sunucu verir (Access-Control-Allow-Origin başlığıyla); sayfa kendine izin veremez. Sunucu senin değilse ya sitenin izin veren bir API'sini kullanırsın ya da isteği kendi sunucundan yaptırırsın (Blok 4). Node'da çalışmasının sebebi, CORS'un bir tarayıcı kuralı olması: tarayıcı başkalarının sayfalarını senin oturumlarınla çalıştırdığı için onları birbirinden korur; Node'daki program ise senin kendi programındır.

Soru 5

Network sekmesinde champion.json'un Size sütununda 37.2 kB yazıyor, ama Content-Length 162972 bayt diyor. Hangisi doğru?

Cevabı göster

İkisi de. Sunucu dosyayı sıkıştırarak gönderdi: ağdan gelen yaklaşık 37 kB, tarayıcının açtığı asıl dosya yaklaşık 163 kB. Size sütunu ağdan geleni gösterir; Big request rows açıkken altında asıl boyut da yazar. Kullanıcının mobil verisinden giden, küçük olandır.

Soru 6

Sayfanın en üstündeki tek büyük kapak resmine loading="lazy" koymak neden kötü bir fikir? Kartlardaki resimlere width ve height vermeseydin ne olurdu?

Cevabı göster

Geç yüklenen resim, tarayıcı yerleşimi hesaplayıp resmin ekranda olduğunu anlayana kadar istenmez; ilk bakışta görünen ana resim bu yüzden gecikir. Geç yükleme ekranın altında kalan çok sayıda resim içindir. Boyut verilmeseydi tarayıcı resimlere yer ayıramazdı: kartlar önce kısa çizilir, resimler geldikçe uzar ve sayfa kayardı. Kısa kartlar yüzünden ekrana yakın görünen kart da artar: denememizde ilk açılışta 48 yerine 72 resim istendi.

Soru 7

Data Dragon olmayan bir sürüm için 403 döndürdü. Sayfanda hangi durum görünür ve bunu hangi satırlar sağlar?

Cevabı göster

Hata durumu: kırmızı "Şampiyonlar yüklenemedi…" yazısı. fetch 403'te hata fırlatmaz; jsonGetir'deki if (!yanit.ok) { throw new Error("İstek başarısız: " + yanit.status) } satırı hatayı kendisi fırlatır. yukle'deki try / catch onu yakalar: console.error ayrıntıyı konsola, durum.textContent anlaşılır cümleyi ekrana yazar. ok denetimi olmasaydı json() gövdedeki XML'i çevirmeye çalışır ve yanıltıcı bir SyntaxError çıkardı.

Soru 8

Sayfa yüklenemedi, ekranda kırmızı hata mesajı var. Kullanıcı arama kutusuna ahri yazıyor. ciz'in başındaki erken çıkış olmasaydı ekranda ne yazardı? Bu neden kötü?

Cevabı göster

"Bu aramaya uyan şampiyon yok." Yükleme başarısız olduğu için sampiyonlar hâlâ boş dizi; ciz boş diziden süzer, geriye hiçbir şey kalmaz ve boş durumun mesajı hata mesajının yerine yazılır. Kullanıcı sorunun bağlantıda olduğunu değil, Ahri'nin listede olmadığını sanır. if (sampiyonlar.length === 0) { return } sayesinde veri gelmeden ciz hiçbir şey yapmaz ve hata mesajı yerinde kalır.

Kayıt noktası

Bu derste yeni bir proje klasörü açtın; Git'in onu tanıması için orada bir kez git init gerekiyor.

Bir sonraki ders Blok 3'ün projesi: bu dersin şampiyon listesini temel alıp favorileri, detay görünümü ve mobil uyumu olan bir şampiyon ansiklopedisi yazacak ve GitHub Pages ile internete koyacaksın.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.