← Kursa Dön
📄 Text · 30 min

Modern Web API'leri

Tarayıcının Gizli Yetenekleri

Tarayıcılar artık sadece HTML gösteren kutular değil — birer işletim sistemi gibi çalışıyorlar. Konum bilgisi alabilir, bildirim gönderebilir, pano'ya erişebilir, DOM değişikliklerini izleyebilir, elementin ekranda görünüp görünmediğini anlayabilirsin. Bu güçlü API'ler, eskiden sadece native uygulamaların yapabildiği şeyleri web'de mümkün kılıyor.

Bu derste beş güçlü Web API'sini derinlemesine keşfedeceğiz: IntersectionObserver ile görünürlük izleme, ResizeObserver ile boyut değişiklikleri, MutationObserver ile DOM değişiklikleri, Geolocation ile konum, Notification ile bildirimler ve Clipboard API ile pano erişimi. Her biri gerçek dünya senaryolarıyla — lazy loading, responsive tasarım, canlı editörler, harita entegrasyonu gibi.


IntersectionObserver: Bir Element Ekranda mı?

Bir elementin viewport'ta (ekranda görünen alan) olup olmadığını anlamak eskiden zor ve pahalıydı. scroll event'ine listener ekler, getBoundingClientRect() ile hesaplama yapardık — ve bu, her scroll'da tetiklenerek performansı çökertiirdi.

IntersectionObserver bu sorunu verimli ve asenkron olarak çözer. Tarayıcı optimizasyonu ile çalışır, ana thread'i bloklamaz.

Bir güvenlik kamerası gibi düşün: sürekli etrafı taramak yerine, belirli bir alan hareket algıladığında seni uyarıyor.

// Temel kullanım
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log(`${entry.target.id} ekrana girdi!`);
    } else {
      console.log(`${entry.target.id} ekrandan çıktı`);
    }
  });
});

// İzlenecek elementleri belirle
const sections = document.querySelectorAll(".section");
sections.forEach(section => observer.observe(section));

// İzlemeyi durdur
// observer.unobserve(element);  // Tek element
// observer.disconnect();        // Tümü

Lazy Loading: Görselleri Gerektiğinde Yükle

Lazy loading'in en yaygın kullanımı — görselleri sadece ekrana yaklaştığında yükle:

function createLazyLoader() {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          // data-src'deki gerçek URL'yi src'ye aktar
          img.src = img.dataset.src;
          img.classList.add("loaded");

          // Bu element artık yüklendi, izlemeye gerek yok
          observer.unobserve(img);
        }
      });
    },
    {
      rootMargin: "200px", // Ekrana 200px kala yüklemeye başla
      threshold: 0,        // Piksel kadar görünürse yeterli
    }
  );

  // Tüm lazy görselleri izle
  document.querySelectorAll("img[data-src]").forEach(img => {
    observer.observe(img);
  });

  return observer;
}

// HTML:
// <img data-src="heavy-image.jpg" alt="..." class="lazy" />
createLazyLoader();

Sonsuz Scroll (Infinite Scroll)

function setupInfiniteScroll(loadMore) {
  // Sayfanın en altındaki "sentinel" elementi izle
  const sentinel = document.getElementById("scroll-sentinel");

  const observer = new IntersectionObserver(
    async (entries) => {
      if (entries[0].isIntersecting) {
        await loadMore(); // Yeni veri yükle
      }
    },
    { threshold: 1.0 } // Element tamamen görünür olduğunda
  );

  observer.observe(sentinel);
}

// HTML: <div id="scroll-sentinel"></div> (listenin altında)
setupInfiniteScroll(async () => {
  const newItems = await fetch("/api/items?page=next").then(r => r.json());
  renderItems(newItems);
});

Animasyon Tetikleme

// Scroll ile animasyon — element ekrana girdiğinde animasyon başlasın
const animationObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add("animate-in");
        animationObserver.unobserve(entry.target); // Bir kez çalışsın
      }
    });
  },
  { threshold: 0.2 } // %20'si görünürse
);

document.querySelectorAll(".animate-on-scroll").forEach(el => {
  animationObserver.observe(el);
});

💡 İpucu: rootMargin negatif değer alabilir — bu sayede element ekranın belirli bir iç bölgesine girdiğinde tetikleme yapabilirsin. Sticky header tespiti gibi senaryolarda kullanışlı.


ResizeObserver: Boyut Değişikliklerini İzle

Bir elementin boyutu değiştiğinde haberdar olmak istediğinde — grafikler, dashboard widget'ları, responsive bileşenler — ResizeObserver devreye girer.

window.onresize sadece pencere boyutu değiştiğinde çalışır. Ama bir <div>'in boyutu CSS animasyonu, içerik değişikliği veya komşu elementin değişmesiyle de değişebilir. ResizeObserver herhangi bir elementin boyut değişikliğini yakalar.

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`${entry.target.id}: ${width}x${height}`);
  }
});

const container = document.getElementById("chart-container");
observer.observe(container);

// Grafik container'ı yeniden boyutlandığında grafik de güncellensin
const chartObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;

    // Grafik kütüphanesine yeni boyutu bildir
    chart.resize(width, height);
    chart.render();
  }
});

chartObserver.observe(document.getElementById("chart-wrapper"));

Container Queries Benzeri Davranış

// Element boyutuna göre class ekle/çıkar
// CSS Container Queries olmadan benzer sonuç
const responsiveObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const el = entry.target;
    const width = entry.contentRect.width;

    el.classList.toggle("compact", width < 400);
    el.classList.toggle("medium", width >= 400 && width < 800);
    el.classList.toggle("wide", width >= 800);
  }
});

document.querySelectorAll(".responsive-widget").forEach(widget => {
  responsiveObserver.observe(widget);
});

MutationObserver: DOM Değişikliklerini Yakalama

MutationObserver, DOM ağacındaki değişiklikleri izler: element ekleme/silme, attribute değişikliği, metin içeriği değişikliği. Canlı editörler, extension'lar, analitik araçlar bu API'yi yoğun kullanır.

Bir güvenlik nöbetçisi gibi düşün: belirli bir odada herhangi bir değişiklik olduğunda (eşya taşınması, yeni eşya gelmesi) seni bilgilendiriyor.

// DOM değişikliklerini izle
const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    switch (mutation.type) {
      case "childList":
        // Alt elementler eklendi veya silindi
        mutation.addedNodes.forEach(node => {
          console.log("Eklendi:", node);
        });
        mutation.removedNodes.forEach(node => {
          console.log("Silindi:", node);
        });
        break;

      case "attributes":
        // Attribute değişti
        console.log(
          `${mutation.attributeName} değişti:`,
          mutation.target.getAttribute(mutation.attributeName)
        );
        break;

      case "characterData":
        // Metin içeriği değişti
        console.log("Metin değişti:", mutation.target.textContent);
        break;
    }
  });
});

// İzleme seçenekleri
observer.observe(document.getElementById("editor"), {
  childList: true,       // Alt element ekleme/silme
  attributes: true,      // Attribute değişiklikleri
  characterData: true,   // Metin değişiklikleri
  subtree: true,         // Alt ağaçtaki değişiklikler de
  attributeOldValue: true, // Eski attribute değerini de kaydet
});

// İzlemeyi durdur
// observer.disconnect();

Pratik Örnek: Otomatik Tablo İçeriği

// Tabloya yeni satır eklendiğinde otomatik numara ver
function autoNumberTable(table) {
  const observer = new MutationObserver((mutations) => {
    const tbody = table.querySelector("tbody");
    if (!tbody) return;

    const rows = tbody.querySelectorAll("tr");
    rows.forEach((row, index) => {
      const numberCell = row.querySelector(".row-number");
      if (numberCell) {
        numberCell.textContent = String(index + 1);
      }
    });
  });

  observer.observe(table.querySelector("tbody"), {
    childList: true, // Satır ekleme/silme
  });

  return observer;
}

Dark Mode Algılama

// <html> tag'ındaki class değişikliğini izle — tema değişimi
const themeObserver = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.attributeName === "class") {
      const isDark = document.documentElement.classList.contains("dark");
      console.log(`Tema değişti: ${isDark ? "dark" : "light"}`);
      // Grafikleri, haritayı vb. güncelle
    }
  });
});

themeObserver.observe(document.documentElement, {
  attributes: true,
  attributeFilter: ["class"], // Sadece class değişikliklerini izle
});

⚠️ Dikkat: MutationObserver çok sık tetiklenebilir. Ağır işlemler yapıyorsan requestAnimationFrame veya debounce ile birleştir. Aksi halde performans sorunu yaşarsın.


Geolocation API: Konum Bilgisi

Geolocation API, kullanıcının coğrafi konumunu almana olanak tanır. Harita uygulamaları, yakındaki restoranlar, teslimat uygulamaları — konum bilgisi gerektiren her senaryoda kullanılır.

// Tek seferlik konum al
navigator.geolocation.getCurrentPosition(
  (position) => {
    // Başarılı
    console.log("Enlem:", position.coords.latitude);
    console.log("Boylam:", position.coords.longitude);
    console.log("Doğruluk (metre):", position.coords.accuracy);
    console.log("Yükseklik:", position.coords.altitude);       // null olabilir
    console.log("Hız:", position.coords.speed);                 // null olabilir
  },
  (error) => {
    // Hata
    switch (error.code) {
      case error.PERMISSION_DENIED:
        console.error("Kullanıcı konum iznini reddetti");
        break;
      case error.POSITION_UNAVAILABLE:
        console.error("Konum bilgisi mevcut değil");
        break;
      case error.TIMEOUT:
        console.error("Konum isteği zaman aşımına uğradı");
        break;
    }
  },
  {
    enableHighAccuracy: true, // GPS kullan (daha yavaş ama hassas)
    timeout: 10000,           // 10 saniye zaman aşımı
    maximumAge: 60000,        // 1 dakika eski konum kabul edilir
  }
);

Sürekli Konum İzleme

// Konum değiştikçe güncelle — navigasyon uygulamalarında
const watchId = navigator.geolocation.watchPosition(
  (position) => {
    updateMapMarker(position.coords.latitude, position.coords.longitude);
  },
  (error) => {
    console.error("Konum hatası:", error.message);
  },
  {
    enableHighAccuracy: true,
    maximumAge: 0,  // Her zaman taze konum
  }
);

// İzlemeyi durdur
// navigator.geolocation.clearWatch(watchId);

Mesafe Hesaplama (Haversine)

// İki nokta arasındaki mesafeyi hesapla (km)
function calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 6371; // Dünya yarıçapı (km)
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);

  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

function toRad(deg) {
  return deg * (Math.PI / 180);
}

// Kullanım
const istanbul = { lat: 41.0082, lon: 28.9784 };
const ankara = { lat: 39.9334, lon: 32.8597 };
const dist = calculateDistance(istanbul.lat, istanbul.lon, ankara.lat, ankara.lon);
console.log(`Mesafe: ${dist.toFixed(1)} km`); // ~351.5 km

⚠️ Dikkat: Geolocation API HTTPS gerektirir (localhost hariç). HTTP üzerinde çalışmaz. Ayrıca kullanıcı izni gerekir — izin verilmezse graceful degradation yap.


Notification API: Masaüstü Bildirimleri

Web uygulamalarından masaüstü bildirimi gönderebilirsin — mesajlaşma uygulamaları, e-posta bildirimleri, zamanlayıcılar gibi senaryolarda.

// İzin iste (bir kez)
async function requestNotificationPermission() {
  if (!("Notification" in window)) {
    console.log("Bu tarayıcı bildirimleri desteklemiyor");
    return false;
  }

  if (Notification.permission === "granted") {
    return true; // Zaten izin verilmiş
  }

  if (Notification.permission !== "denied") {
    const permission = await Notification.requestPermission();
    return permission === "granted";
  }

  return false; // Daha önce reddedilmiş
}

// Bildirim gönder
async function sendNotification(title, body, icon) {
  const hasPermission = await requestNotificationPermission();
  if (!hasPermission) return;

  const notification = new Notification(title, {
    body,
    icon: icon || "/favicon.png",
    badge: "/badge.png",
    tag: "my-notification",  // Aynı tag'lı bildirim güncellenir
    requireInteraction: false, // Otomatik kapansın mı
  });

  // Tıklama olayı
  notification.onclick = (event) => {
    event.preventDefault();
    window.focus(); // Uygulamayı ön plana getir
    notification.close();
  };

  // Otomatik kapat
  setTimeout(() => notification.close(), 5000);
}

// Kullanım
sendNotification(
  "Yeni Mesaj",
  "Ali: Merhaba, nasılsın?",
  "/avatars/ali.jpg"
);

Bildirim Stratejisi

// Akıllı bildirim yöneticisi — spam önleme
class NotificationManager {
  #lastNotification = 0;
  #minInterval = 5000; // En az 5 saniye arayla

  async notify(title, options = {}) {
    const now = Date.now();
    if (now - this.#lastNotification < this.#minInterval) {
      console.log("Bildirim çok sık, atlanıyor");
      return;
    }

    // Sayfa odakta ise bildirim gönderme
    if (document.hasFocus()) {
      console.log("Sayfa zaten aktif, bildirim gerekmiyor");
      return;
    }

    const hasPermission = await requestNotificationPermission();
    if (!hasPermission) return;

    this.#lastNotification = now;
    return new Notification(title, options);
  }
}

const notifier = new NotificationManager();

Clipboard API: Pano Erişimi

Modern Clipboard API, metin, HTML ve hatta dosyaları pano'ya kopyalama ve yapıştırma imkanı sunar:

// Metin kopyala — modern async API
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    console.log("Kopyalandı!");
    return true;
  } catch (error) {
    console.error("Kopyalama başarısız:", error);
    return false;
  }
}

// Metni panodan oku
async function pasteFromClipboard() {
  try {
    const text = await navigator.clipboard.readText();
    console.log("Yapıştırılan:", text);
    return text;
  } catch (error) {
    console.error("Yapıştırma başarısız:", error);
    return null;
  }
}

// "Kopyala" butonu
document.getElementById("copy-btn").addEventListener("click", async () => {
  const code = document.getElementById("code-block").textContent;
  const success = await copyToClipboard(code);

  if (success) {
    // Butonu geçici olarak "Kopyalandı!" yap
    const btn = document.getElementById("copy-btn");
    const original = btn.textContent;
    btn.textContent = "✅ Kopyalandı!";
    setTimeout(() => btn.textContent = original, 2000);
  }
});

Zengin İçerik Kopyalama

// HTML ve düz metin olarak kopyala
async function copyRichText(html, plainText) {
  try {
    const blob = new Blob([html], { type: "text/html" });
    const textBlob = new Blob([plainText], { type: "text/plain" });

    const clipboardItem = new ClipboardItem({
      "text/html": blob,
      "text/plain": textBlob,
    });

    await navigator.clipboard.write([clipboardItem]);
    console.log("Zengin içerik kopyalandı!");
  } catch (error) {
    console.error("Kopyalama hatası:", error);
  }
}

// Kullanım: tablo verisi kopyala
copyRichText(
  "<table><tr><td>İsim</td><td>Yaş</td></tr></table>",
  "İsim\tYaş"
);

⚠️ Dikkat: Clipboard API güvenli bağlam (HTTPS veya localhost) ve çoğu durumda kullanıcı etkileşimi (tıklama) gerektirir. Arka planda sessizce pano'ya erişemezsin — bu bir güvenlik önlemi.


Gerçek Dünya Örneği: Akıllı Dashboard Widget

Birden fazla API'yi birleştiren bir dashboard widget'ı:

class DashboardWidget {
  constructor(container) {
    this.container = container;
    this.chart = null;

    this.setupObservers();
  }

  setupObservers() {
    // 1. Element ekrana girince veri yükle (lazy load)
    this.visibilityObserver = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          this.loadData();
          this.visibilityObserver.unobserve(this.container);
        }
      },
      { threshold: 0.1 }
    );
    this.visibilityObserver.observe(this.container);

    // 2. Boyut değişince grafiği yeniden çiz
    this.resizeObserver = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      if (this.chart) {
        this.chart.resize(width, height - 40); // Header çıkar
      }
    });
    this.resizeObserver.observe(this.container);

    // 3. Container'a veri attribute'u eklenirse güncelle
    this.mutationObserver = new MutationObserver((mutations) => {
      mutations.forEach(m => {
        if (m.attributeName === "data-refresh") {
          this.loadData();
        }
      });
    });
    this.mutationObserver.observe(this.container, {
      attributes: true,
      attributeFilter: ["data-refresh"],
    });
  }

  async loadData() {
    const endpoint = this.container.dataset.endpoint;
    const data = await fetch(endpoint).then(r => r.json());
    this.render(data);
  }

  render(data) {
    // Grafik çizim mantığı...
    this.chart = { resize: (w, h) => console.log(`Resize: ${w}x${h}`) };
    this.container.innerHTML = `<div class="chart">${data.length} veri noktası</div>`;
  }

  destroy() {
    this.visibilityObserver.disconnect();
    this.resizeObserver.disconnect();
    this.mutationObserver.disconnect();
  }
}

Özet

  • IntersectionObserver: Element ekranda mı? Lazy loading, infinite scroll, scroll animasyonları. rootMargin ve threshold ile ince ayar.

  • ResizeObserver: Element boyutu değişti mi? Grafik yeniden çizimi, responsive widget'lar. Window resize değil, element resize.

  • MutationObserver: DOM değişti mi? Element ekleme/silme, attribute değişikliği, metin değişikliği. Canlı editörler, tema algılama.

  • Geolocation: Kullanıcı konumu (HTTPS + izin gerekli). getCurrentPosition tek seferlik, watchPosition sürekli izleme.

  • Notification: Masaüstü bildirimi (izin gerekli). tag ile güncelleme, document.hasFocus() ile gereksiz bildirimi engelle.

  • Clipboard API: Kopyala/yapıştır (HTTPS + kullanıcı etkileşimi). writeText basit metin, write zengin içerik.

  • Tüm Observer'larda disconnect() çağırmayı unutma — memory leak'e yol açar.