Fetch, DOM manipuláció, eseménykezelés, Web Storage, időzítők és további böngésző API-k – a kliens oldali fejlesztés eszköztára.
A fetch() a modern böngészők beépített HTTP kliens API-ja, amely felváltotta a régebbi XMLHttpRequest-et. A legnagyobb előnye, hogy Promise-alapú – az 1. fejezetben tanult async/await mintával természetesen használható. A fetch() globális függvény, tehát bárhol meghívható JavaScript kódból.
A Fetch API kétlépcsős: először a válaszfejléceket kapjuk meg (Response objektum), majd egy második aszinkron lépésben a válasz törzsét (body) kell kiolvassuk. Ez azért hasznos, mert a fejlécekből (pl. státuszkód) már eldönthetjük, hogy egyáltalán érdemes-e a törzsöt feldolgozni.
async function getUsers() { // 1. lépés: a fetch() elindítja a kérést, és Promise-t ad vissza. // Az await megvárja, amíg a FEJLÉCEK megérkeznek. var response = await fetch("/api/users"); // 2. lépés: a response.ok ellenőrzi, hogy a státuszkód 200-299 közé esik-e. // FONTOS: a fetch() NEM dob hibát 404-nél vagy 500-nál! if (!response.ok) { throw new Error("HTTP hiba: " + response.status); } // 3. lépés: a válasz törzsének kiolvasása JSON-ként. // Ez is aszinkron művelet (a törzs streamelve érkezik). var data = await response.json(); return data; }
fetch() Promise-e csak hálózati hibánál (nincs internet, DNS hiba, szerver nem elérhető) lesz rejected. Ha a szerver válaszol – akár 404-gyel, akár 500-zal –, a Promise FULFILLED lesz! Ezért MINDIG ellenőrizzük a response.ok tulajdonságot (vagy a response.status értékét).
| Tulajdonság / Metódus | Leírás | Példa |
|---|---|---|
response.ok | true, ha a státuszkód 200–299 | if (!response.ok) throw ... |
response.status | HTTP státuszkód (szám) | 200, 404, 500 |
response.statusText | Státusz szöveg | "OK", "Not Found" |
response.headers | Válaszfejlécek (Headers objektum) | response.headers.get("Content-Type") |
response.json() | Törzs kiolvasása JSON-ként | A leggyakoribb API válasz |
response.text() | Törzs kiolvasása nyers szövegként | HTML, XML, CSV válasz |
response.blob() | Törzs kiolvasása binárisként | Kép, PDF letöltés |
Az alapértelmezett metódus GET. Más metódusokhoz a fetch() második paramétereként egy konfigurációs objektumot adunk meg, amelyben meghatározzuk a metódust, a fejléceket és a kérés törzsét (body).
// POST kérés – új erőforrás létrehozása // A Content-Type fejléccel jelezzük, hogy JSON-t küldünk. // A body-t JSON.stringify()-jal alakítjuk szöveggé. async function createUser(userData) { var response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(userData), }); if (!response.ok) throw new Error("HTTP " + response.status); return await response.json(); // a szerver visszaadja a létrehozott user-t } // PUT kérés – meglévő erőforrás teljes frissítése async function updateUser(id, userData) { var response = await fetch("/api/users/" + id, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(userData), }); if (!response.ok) throw new Error("HTTP " + response.status); return await response.json(); } // DELETE kérés – erőforrás törlése // A DELETE-nek általában nincs body-ja. async function deleteUser(id) { var response = await fetch("/api/users/" + id, { method: "DELETE", }); return response.ok; // true ha sikeres (200–299) }
Előfordul, hogy egy kérést meg akarunk szakítani – például ha a felhasználó elnavigál az oldalról, vagy ha időkorlátot (timeout) szeretnénk beállítani. Az AbortController API-val jelzést (signal) küldhetünk a fetch-nek, hogy álljon le.
var controller = new AbortController(); // 5 mp timeout: ha a szerver nem válaszol, megszakítjuk var timeoutId = setTimeout(function() { controller.abort(); }, 5000); try { var response = await fetch("/api/slow-endpoint", { signal: controller.signal, // a fetch figyeli ezt a jelzést }); clearTimeout(timeoutId); // sikeres válasz => töröljük a timeout-ot var data = await response.json(); } catch (err) { if (err.name === "AbortError") { console.log("A kérés megszakítva (timeout vagy felhasználó)."); } else { console.error("Hálózati hiba:", err); } }
abort() hívással az összeset leállíthatjuk.
Nagyobb alkalmazásokban nem akarjuk minden egyes fetch hívásnál megismételni a hibaellenőrzést, a fejlécek beállítását és a JSON feldolgozást. Ehelyett készítünk egy újrafelhasználható wrapper objektumot:
var api = { baseUrl: "/api", // Az alap request metódus, amely minden közös logikát tartalmaz request: async function(endpoint, options) { var url = this.baseUrl + endpoint; var defaults = { headers: { "Content-Type": "application/json" }, }; var config = Object.assign({}, defaults, options); var response = await fetch(url, config); if (!response.ok) { // Megpróbáljuk kiolvasni a szerver hibaüzenetét var errorBody = await response.text().catch(function() { return ""; }); throw new Error("HTTP " + response.status + ": " + errorBody); } return await response.json(); }, // Kényelmi metódusok get: function(endpoint) { return this.request(endpoint); }, post: function(endpoint, data) { return this.request(endpoint, { method: "POST", body: JSON.stringify(data), }); }, put: function(endpoint, data) { return this.request(endpoint, { method: "PUT", body: JSON.stringify(data), }); }, del: function(endpoint) { return this.request(endpoint, { method: "DELETE" }); }, }; // Használat – tiszta és tömör: var users = await api.get("/users"); var newUser = await api.post("/users", { name: "Anna", age: 28 }); await api.put("/users/42", { name: "Anna", age: 29 }); await api.del("/users/42");
request metódust.
A REST (Representational State Transfer) egy architektúrális stílus, amely szabályokat ad arra, hogyan kommunikáljanak a kliens és a szerver alkalmazások HTTP-n keresztül. A REST nem protokoll és nem szabvány – inkább konvenciók és elvek gyűjteménye, amelyet a legtöbb modern web API követ.
A REST lényege: az erőforrásokat (felhasználók, termékek, rendelések) URL-ekkel azonosítjuk, és a HTTP metódusokkal (GET, POST, PUT, DELETE) végzünk rajtuk műveleteket. A kommunikáció állapotmentes (stateless) – minden kérésnek tartalmaznia kell az összes szükséges információt, a szerver nem emlékszik a korábbi kérésekre.
A REST API-ban minden URL egy erőforrást (resource) jelöl. Az URL-ek konvenció szerint főnevek (nem igék!), és többes számúak:
| URL | Erőforrás | Leírás |
|---|---|---|
/api/users | Felhasználók gyűjtemény | Az összes felhasználó |
/api/users/42 | Egyetlen felhasználó | A 42-es ID-jú felhasználó |
/api/users/42/orders | Beágyazott erőforrás | A 42-es user rendelései |
/api/products?category=electronics | Szűrt gyűjtemény | Elektronikai termékek |
GET /api/getUsersPOST /api/createUserPOST /api/deleteUser/42
GET /api/users — listázásPOST /api/users — létrehozásGET /api/users/42 — egy elem lekérésePUT /api/users/42 — teljes frissítésPATCH /api/users/42 — részleges frissítésDELETE /api/users/42 — törlés
A REST API-ban a HTTP metódusok felelnek meg a CRUD (Create, Read, Update, Delete) műveleteknek. Fontos fogalom az idempotencia: egy művelet idempotens, ha többször végrehajtva ugyanazt az eredményt adja.
| HTTP metódus | CRUD | Idempotens? | Body? | Példa |
|---|---|---|---|---|
GET | Read | Igen | Nem | Felhasználók listázása |
POST | Create | Nem | Igen | Új felhasználó regisztráció |
PUT | Update (teljes) | Igen | Igen | Profil összes mezőjének frissítése |
PATCH | Update (részleges) | Nem | Igen | Csak az e-mail cím módosítása |
DELETE | Delete | Igen | Nem | Felhasználó törlése |
A szerver a válasz státuszkódjával jelzi a művelet eredményét. A kódok százas csoportokba tartoznak: 2xx = siker, 4xx = kliens hiba, 5xx = szerver hiba.
| Kód | Jelentés | Mikor kapjuk? |
|---|---|---|
200 OK | Sikeres kérés | GET, PUT, PATCH sikeres válasz |
201 Created | Erőforrás létrehozva | Sikeres POST (új elem létrejött) |
204 No Content | Sikeres, nincs válasz body | Sikeres DELETE |
400 Bad Request | Hibás kérés | Validációs hiba, hiányzó mezők |
401 Unauthorized | Nincs hitelesítés | Hiányzó vagy lejárt token |
403 Forbidden | Nincs jogosultság | Hitelesített, de nincs joga hozzá |
404 Not Found | Nem található | Nem létező erőforrás (/users/99999) |
409 Conflict | Ütközés | Duplikált e-mail vagy felhasználónév |
500 Internal Error | Szerverhiba | Váratlan szerver oldali hiba |
Az alábbi példa bemutatja, hogyan valósítjuk meg a CRUD műveleteket a 2.1-ben tanult Fetch API-val. Figyeljük meg, hogy a HTTP metódus és az URL együtt határozza meg a műveletet:
// ── CREATE: új felhasználó létrehozása ── async function createUser(userData) { var response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(userData), }); if (response.status === 201) { return await response.json(); // A szerver visszaadja az új user-t (id-vel) } if (response.status === 400) { var errors = await response.json(); throw new Error("Validáció: " + errors.message); } throw new Error("HTTP " + response.status); } // ── READ: összes felhasználó lekérdezése (lapozással) ── async function getUsers(page, limit) { var url = "/api/users?page=" + page + "&limit=" + limit; var response = await fetch(url); if (!response.ok) throw new Error("HTTP " + response.status); return await response.json(); // Válasz: { data: [...], total: 150, page: 1, limit: 20 } } // ── READ: egyetlen felhasználó ID alapján ── async function getUser(id) { var response = await fetch("/api/users/" + id); if (response.status === 404) return null; // Nem létezik if (!response.ok) throw new Error("HTTP " + response.status); return await response.json(); } // ── UPDATE: felhasználó teljes frissítése ── async function updateUser(id, userData) { var response = await fetch("/api/users/" + id, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(userData), }); if (!response.ok) throw new Error("HTTP " + response.status); return await response.json(); } // ── DELETE: felhasználó törlése ── async function deleteUser(id) { var response = await fetch("/api/users/" + id, { method: "DELETE", }); if (response.status === 404) return false; // Már nem létezik if (!response.ok) throw new Error("HTTP " + response.status); return true; }
A legtöbb REST API JSON formátumban kommunikál. Érdemes egységes válaszstruktúrát használni – így a kliens oldali kód mindig tudja, mit várjon:
Sikeres válasz – egyetlen elem (GET /api/users/42 → 200 OK)
{
"id": 42,
"name": "János",
"email": "janos@example.com",
"createdAt": "2025-01-15T10:30:00Z"
}
Sikeres válasz – lista lapozással (GET /api/users?page=2&limit=10 → 200 OK)
{
"data": [
{ "id": 11, "name": "Anna", "email": "anna@example.com" },
{ "id": 12, "name": "Béla", "email": "bela@example.com" }
],
"total": 150,
"page": 2,
"limit": 10,
"totalPages": 15
}
Hiba válasz (POST /api/users → 400 Bad Request)
400 { "error": "Validation failed", "message": "Hiányos adatok.", "details": [ { "field": "email", "message": "Kötelező mező" }, { "field": "name", "message": "Minimum 2 karakter" } ] }
A 2.1-es API wrapper-t kibővíthetjük, hogy a különböző státuszkódokra eltérően reagáljon. A 4xx hibák a kliens hibái (javíthatók), az 5xx hibák a szerver hibái (nem javíthatók kliensről):
async function handleResponse(response) { // Siker (200–299) if (response.ok) { if (response.status === 204) return null; // No Content (pl. DELETE) return await response.json(); } // Megpróbáljuk kiolvasni a szerver hibaüzenetét var errorData; try { errorData = await response.json(); } catch (e) { errorData = { message: "Ismeretlen hiba" }; } if (response.status === 401) { window.location.href = "/login"; // Lejárt token => bejelentkezés return; } if (response.status === 403) { showError("Nincs jogosultságod ehhez a művelethez."); return; } if (response.status === 400 || response.status === 422) { if (errorData.details) { showValidationErrors(errorData.details); } return; } throw new Error(errorData.message || "HTTP " + response.status); }
A DOM (Document Object Model) a HTML dokumentum fa-struktúrájú reprezentációja a memóriában. Amikor a böngésző betölti a HTML-t, felépíti a DOM fát, amelyet a JavaScript szabadon olvashat és módosíthat. Minden HTML elem egy csomópont (node) a fában – a <html> a gyökér, és minden más elem ennek leszármazottja.
A DOM manipuláció az a folyamat, amellyel JavaScript-ből módosítjuk az oldal tartalmát, megjelenését vagy szerkezetét – mindezt az oldal újratöltése nélkül.
Mielőtt módosíthatnánk egy elemet, először meg kell találnunk a DOM fában. A két legfontosabb metódus a querySelector (egy elem) és a querySelectorAll (több elem). Mindkettő CSS szelektorokkal dolgozik, tehát ugyanazokat a szelektorokat használhatjuk, mint a CSS-ben.
| Metódus | Visszatérés | Ha nincs találat | Mikor használjuk? |
|---|---|---|---|
querySelector(sel) | Első egyező elem | null | Egyetlen elem (szinte mindig ez kell) |
querySelectorAll(sel) | NodeList (összes egyező) | Üres NodeList | Több elem |
getElementById(id) | Elem id alapján | null | Régi kód, ma querySelector elég |
closest(sel) | Legközelebbi ős (felfelé) | null | Eseménydelegálásnál (lásd 2.3) |
// Egyetlen elem kiválasztása var title = document.querySelector("h1"); // elem típus szerint var btn = document.querySelector("#submit-btn"); // id szerint var card = document.querySelector(".card.active"); // összetett CSS szelektor // FIGYELEM: ha nem létezik az elem, null-t kapunk! var elem = document.querySelector(".nonexistent"); console.log(elem); // null // Több elem kiválasztása var items = document.querySelectorAll(".list-item"); console.log(items.length); // pl. 5 // NodeList bejárása forEach-csel items.forEach(function(item, index) { console.log(index + ": " + item.textContent); });
querySelectorAll() statikus NodeList-et ad vissza (pillanatfelvétel). A régebbi getElementsByClassName() élő HTMLCollection-t ad – ha a DOM változik, a lista automatikusan frissül. A modern fejlesztésben a querySelectorAll() az ajánlott.
Az elem tartalmát kétféleképpen módosíthatjuk, de a biztonsági következmények alapvetően különböznek:
✘ XSS veszély! // innerHTML ÉRTELMEZI a HTML-t => ha felhasználói adat kerül bele, // a böngésző végrehajtja a benne lévő scripteket! var userInput = '<img src=x onerror="alert(document.cookie)">'; elem.innerHTML = "<p>Üdv, " + userInput + "!</p>"; // Az onerror lefut => XSS támadás! (Lásd 3.1 fejezet)
✔ Biztonságos // textContent NEM értelmez HTML-t – nyers szövegként kezeli var userInput = '<img src=x onerror="alert(1)">'; elem.textContent = "Üdv, " + userInput + "!"; // Kimenet: "Üdv, <img src=x onerror="alert(1)">!" (nyers szöveg)
Az elemek HTML attribútumait olvashatjuk és módosíthatjuk. Különösen hasznos a data-* attribútumok rendszere, amellyel egyedi adatokat csatolhatunk az elemekhez:
// Hagyományos attribútum-kezelés var img = document.querySelector("img"); img.setAttribute("src", "/images/photo.jpg"); img.setAttribute("alt", "Profilkép"); var src = img.getAttribute("src"); // data-* attribútumok – egyedi adatok HTML-ben // HTML: <div class="card" data-user-id="42" data-role="admin"> var card = document.querySelector(".card"); var userId = card.dataset.userId; // "42" (data-user-id => dataset.userId) var role = card.dataset.role; // "admin" // A kötőjeles nevek camelCase-re alakulnak: // data-user-id => dataset.userId // data-is-active => dataset.isActive
Az elemek CSS osztályait a classList API-val kezeljük. Ez sokkal kényelmesebb és biztonságosabb, mint a régi className string manipuláció:
var card = document.querySelector(".card"); card.classList.add("active"); // Hozzáadás card.classList.add("highlight", "big"); // Többet egyszerre card.classList.remove("hidden"); // Eltávolítás card.classList.toggle("expanded"); // Kapcsoló: ha van, leveszi; ha nincs, hozzáadja var isActive = card.classList.contains("active"); // Lekérdezés: true/false // Gyakori minta: aktív elem váltás document.querySelectorAll(".tab").forEach(function(tab) { tab.classList.remove("active"); // Mindről levesszük }); clickedTab.classList.add("active"); // Csak a kattintottra tesszük
Új HTML elemeket programozottan hozhatunk létre és szúrhatunk be a DOM-ba. Ez biztonságosabb, mint az innerHTML, és jobban kézben tartható:
// 1. Elem létrehozása (még NEM jelenik meg az oldalon!) var li = document.createElement("li"); // 2. Tartalom és attribútumok beállítása li.textContent = "Új tennivaló"; li.classList.add("todo-item"); li.dataset.id = "123"; // 3. Beszúrás a DOM-ba (itt jelenik meg az oldalon) var list = document.querySelector("ul"); list.append(li); // Lista VÉGÉRE list.prepend(li); // Lista ELEJÉRE // Beszúrás egy adott elem elé/mögé var ref = document.querySelector(".reference"); ref.before(li); // A referencia ELÉ ref.after(li); // A referencia MÖGÉ // Törlés li.remove();
Ha sok elemet kell egyszerre beszúrni, a DocumentFragment egy „láthatatlan konténer", amely nem része a DOM-nak. Az elemeket ebbe gyűjtjük, majd egyetlen művelettel szúrjuk be – így a böngésző csak EGYSZER rajzolja újra az oldalt (reflow), nem minden egyes elemnél:
// 100 listaelem hatékony beszúrása var fragment = document.createDocumentFragment(); for (var i = 0; i < 100; i++) { var li = document.createElement("li"); li.textContent = "Elem " + (i + 1); fragment.append(li); // A fragment-be gyűjtjük (NEM a DOM-ba!) } document.querySelector("ul").append(fragment); // Egyetlen DOM művelet => egyetlen reflow => gyors!
Az események a felhasználó és a böngésző interakcióit jelzik: kattintás, billentyűleütés, űrlap beküldés, scroll stb. A JavaScript-ben listener (figyelő) függvényeket regisztrálunk, amelyek az esemény bekövetkeztekor automatikusan lefutnak. Ez az aszinkron, eseményvezérelt programozás alapja.
Az addEventListener() metódussal regisztrálunk egy listenert egy adott eseménytípusra. Fontos: ha később el akarjuk távolítani a listenert, nevesített függvényt kell használnunk (nem anonim függvényt), mert a removeEventListener()-nek ugyanazt a függvény-referenciát kell kapnia.
var btn = document.querySelector("#my-btn"); // Nevesített függvény => eltávolítható! function handleClick(event) { console.log("Kattintás történt!"); console.log("Cél elem:", event.target); } // Regisztráció btn.addEventListener("click", handleClick); // Eltávolítás (ugyanaz a függvény-referencia kell!) btn.removeEventListener("click", handleClick);
Az addEventListener harmadik paramétereként opciókat adhatunk meg:
btn.addEventListener("click", handleClick, { once: true, // Egyszer fut, aztán automatikusan eltávolítódik. // Hasznos: "Megrendelés" gomb – ne lehessen duplán kattintani. capture: false, // Melyik fázisban kapjuk el? false = bubbling (alapért.) // true = capturing fázis (kívülről befelé) passive: true, // Jelezzük, hogy NEM fogjuk meghívni a preventDefault()-ot. // A böngésző optimalizálhatja a scrolling-ot. // Scroll és touch eseményeknél ajánlott. });
Amikor egy esemény bekövetkezik, a böngésző létrehoz egy Event objektumot, amely részletes információt tartalmaz az eseményről. Ezt a listener függvény első paramétereként kapja meg:
| Tulajdonság / Metódus | Leírás | Használat |
|---|---|---|
event.target | Az elem, amelyen az esemény ténylegesen történt | Delegálásnál a belső elem azonosítása |
event.currentTarget | Az elem, amelyre a listener regisztrálva van | Mindig a „saját" elemünk |
event.type | Az esemény típusa (string) | "click", "submit", "keydown" stb. |
event.preventDefault() | Alapértelmezett viselkedés tiltása | Form submit, link navigáció megakadályozása |
event.stopPropagation() | Esemény terjedésének leállítása | Ritkán szükséges – óvatosan! |
// preventDefault() – űrlap beküldés megakadályozása var form = document.querySelector("#my-form"); form.addEventListener("submit", function(event) { event.preventDefault(); // Az oldal NEM töltődik újra! var formData = new FormData(form); var data = Object.fromEntries(formData); console.log("Űrlap adatok:", data); // Küldés Fetch-csel az oldal újratöltése helyett fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); });
Az események a DOM fában terjednek. Ez három fázisban történik:
Alapértelmezetten a bubbling fázisban kapjuk el az eseményeket. Ez azt jelenti, hogy ha egy gombra kattintunk, az eseményt a gomb szülője, nagyszülője stb. is „meghallja".
// HTML: <div class="outer"><button class="inner">Katt</button></div> document.querySelector(".outer").addEventListener("click", function() { console.log("outer kattintás"); }); document.querySelector(".inner").addEventListener("click", function() { console.log("inner kattintás"); }); // A .inner gombra kattintva a kimenet (bubbling sorrend): // "inner kattintás" (target fázis) // "outer kattintás" (bubbling: felfelé terjedt a szülőhöz)
A bubbling-ot kihasználva egyetlen listenert helyezhetünk a szülő elemre, amely az összes gyermekelem eseményét elkapja. Ez két nagy előnnyel jár:
✔ Delegálás // Egyetlen listener a listán, amely kezeli az összes törlés gombot var todoList = document.querySelector(".todo-list"); todoList.addEventListener("click", function(event) { // A closest() megkeresi a legközelebbi őst (vagy önmagát), // amely illeszkedik a szelektorra. Ha nem talál: null. var deleteBtn = event.target.closest("[data-action='delete']"); if (!deleteBtn) return; // Nem törlés gombra kattintottak // A gombtól felfelé keressük a listaelemet var item = deleteBtn.closest(".todo-item"); if (!item) return; var id = item.dataset.id; item.remove(); console.log("Törölve: " + id); }); // Ez működik DINAMIKUSAN hozzáadott elemekre is! // Ha programozottan hozzáadunk egy új .todo-item-et, // a törlés gombja automatikusan működik, mert a szülő figyel.
event.target a LEGBELSŐ elem, amelyre kattintottunk. Ha a gomb belsejében egy <span> ikon van, a target az ikon lesz, nem a gomb! A closest() felfelé keresi a megadott szelektort, így biztosan megtalálja a gombot (vagy annak szülőjét).
Egyes események nagyon sűrűn tüzelnek – az input esemény minden billentyűleütésnél, a scroll akár másodpercenként 60-szor. Ha ezekre drága műveletet (pl. API hívás, DOM átrendezés) kötünk, az alkalmazás belassulhat. A megoldás a hívások korlátozása:
| Minta | Működés | Analógia | Használat |
|---|---|---|---|
debounce | Csak a LEGUTOLSÓ hívás után X ms-sel fut | Lift: megvárja, amíg mindenki beszállt | Keresőmező, űrlap validáció |
throttle | Maximum X ms-enként egyszer fut | Csap: egyenletesen csöpög | Scroll, resize, egérmozgatás |
// Debounce implementáció // A függvény csak akkor fut le, ha delayMs ideje nem hívták meg újra. // Minden egyes hívás "újraindítja az órát". function debounce(fn, delayMs) { var timerId; return function() { var context = this; var args = arguments; clearTimeout(timerId); // Töröljük az előző időzítőt timerId = setTimeout(function() { fn.apply(context, args); // delayMs elteltével lefut }, delayMs); }; } // Használat: keresőmező – 300ms-ot várunk a gépelés befejezése után var searchInput = document.querySelector("#search"); searchInput.addEventListener("input", debounce(function(event) { console.log("Keresés indítása: " + event.target.value); // Itt hívhatnánk: api.get("/search?q=" + event.target.value) }, 300));
// Throttle implementáció // A függvény legfeljebb limitMs-enként egyszer fut le. function throttle(fn, limitMs) { var waiting = false; return function() { if (waiting) return; // Még nem telt le az idő fn.apply(this, arguments); waiting = true; setTimeout(function() { waiting = false; // limitMs után újra engedélyez }, limitMs); }; } // Használat: scroll esemény – legfeljebb 100ms-enként window.addEventListener("scroll", throttle(function() { console.log("Scroll pozíció: " + window.scrollY); }, 100));
A Web Storage API lehetővé teszi, hogy a böngészőben kulcs-érték párokat tároljunk. Kétféle tároló érhető el, amelyek hasonlóan működnek, de eltérő élettartammal rendelkeznek:
| Szempont | localStorage | sessionStorage |
|---|---|---|
| Élettartam | Tartós – böngésző bezárás után is megmarad | Munkamenet végéig – a fül bezárásakor törlődik |
| Megosztás | Ugyanazon origin minden füle/ablaka látja | Fülenként/ablakonként külön |
| Méretkorlát | ~5 MB (origin-enként) | ~5 MB (origin-enként) |
| Tipikus használat | Téma, nyelv, kosár, beállítások | Wizard lépései, ideiglenes szűrők |
Mindkét tároló szinkron – azonnal visszaadja az eredményt (nem kell await). Fontos korlátozás: csak stringeket tárolhatnak. Ha objektumot vagy tömböt akarunk menteni, JSON.stringify()-jal alakítjuk szöveggé, és visszaolvasáskor JSON.parse()-szal.
// ÍRÁS – kulcs-érték pár mentése localStorage.setItem("theme", "dark"); localStorage.setItem("lang", "hu"); // OLVASÁS – ha nem létezik a kulcs, null-t kapunk var theme = localStorage.getItem("theme"); // "dark" var foo = localStorage.getItem("foo"); // null (nem létezik) // TÖRLÉS – egy kulcs eltávolítása localStorage.removeItem("theme"); // AZ ÖSSZES TÖRLÉSE – figyelem, ez mindent kitöröl! localStorage.clear(); // Kulcsok száma console.log(localStorage.length); // pl. 2
A Storage csak stringeket kezel. Ha objektumot próbálunk közvetlenül elmenteni, a böngésző a .toString() metódust hívja meg rajta, aminek eredménye "[object Object]" – ami nyilvánvalóan nem az, amit szeretnénk:
✘ Hibás – adatvesztés! // Objektum közvetlenül => "[object Object]"! localStorage.setItem("user", { name: "János", age: 25 }); var result = localStorage.getItem("user"); console.log(result); // "[object Object]" – az adatok ELVESZTEK!
✔ Helyes – JSON // Mentés: objektum => JSON string var user = { name: "János", age: 25, hobbies: ["futás", "olvasás"] }; localStorage.setItem("user", JSON.stringify(user)); // Visszaolvasás: JSON string => objektum var stored = JSON.parse(localStorage.getItem("user")); console.log(stored.name); // "János" console.log(stored.hobbies[0]); // "futás"
Érdemes egy wrapper függvényeket készíteni, amelyek kezelik a JSON konverziót, a null ellenőrzést és a tárhelyhiány (QuotaExceededError) hibát:
var storage = { // Értékmentés – JSON-ként tárolja set: function(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.error("Storage írási hiba:", e.message); // QuotaExceededError: a tárhely megtelt (~5 MB) } }, // Értékolvasás – ha nem létezik, a defaultValue-t adja get: function(key, defaultValue) { try { var item = localStorage.getItem(key); return item !== null ? JSON.parse(item) : defaultValue; } catch (e) { // JSON.parse hiba (sérült adat) return defaultValue; } }, remove: function(key) { localStorage.removeItem(key); }, }; // Használat: storage.set("settings", { theme: "dark", fontSize: 16 }); var settings = storage.get("settings", { theme: "light", fontSize: 14 }); console.log(settings.theme); // "dark"
A setTimeout() egy adott idő elteltével egyszer futtatja le a megadott függvényt. Fontos: a megadott idő minimális várakozás, nem garancia – ha a főszál foglalt, a callback csak később fut le (lásd 1.1 Event Loop).
// 2 mp (2000 ms) múlva fut le – EGYSZER var timerId = setTimeout(function() { console.log("Ez 2 másodperc múlva jelenik meg."); }, 2000); // Törlés – ha a timeout még nem futott le, megakadályozhatjuk clearTimeout(timerId); // Gyakorlati példa: értesítés automatikus eltűntetése function showNotification(message, durationMs) { var el = document.querySelector(".notification"); el.textContent = message; el.classList.add("visible"); setTimeout(function() { el.classList.remove("visible"); }, durationMs); } showNotification("Mentés sikeres!", 3000);
A setInterval() a megadott időközönként ismételten futtatja a callback-et. Nagyon fontos az interval ID mentése és a megfelelő leállítás, különben memóriaszivárgást okozhatunk!
// Visszaszámláló var remaining = 10; var display = document.querySelector("#countdown"); var intervalId = setInterval(function() { remaining--; display.textContent = remaining + " mp"; if (remaining <= 0) { clearInterval(intervalId); // FONTOS: leállítjuk! display.textContent = "Lejárt!"; } }, 1000);
Ha a callback aszinkron (pl. fetch hívás), és a végrehajtási idő hosszabb, mint az intervallum, a hívások „halmozódnak" – az előző még fut, amikor az újabb indul. A megoldás: rekurzív setTimeout.
✘ Halmozódik! // Ha a fetch 3 mp-ig tart, de az intervallum 1 mp // => 3 kérés indul, mielőtt az első befejeződik! setInterval(async function() { var data = await fetch("/api/status").then(function(r) { return r.json(); }); updateDashboard(data); }, 1000);
✔ Rekurzív setTimeout // Csak a BEFEJEZÉS UTÁN ütemez újat => nincs halmozódás async function pollStatus() { try { var data = await fetch("/api/status").then(function(r) { return r.json(); }); updateDashboard(data); } catch (err) { console.error("Polling hiba:", err); } setTimeout(pollStatus, 1000); // Csak most ütemezzük az újat } pollStatus(); // Indítás
A requestAnimationFrame() (rAF) a böngésző renderelési ciklusához igazodik – a callback közvetlenül a következő képkocka rajzolása ELŐTT fut le. Ez tökéletes szinkronizációt biztosít az animációkhoz:
| setInterval(fn, 16) | requestAnimationFrame(fn) | |
|---|---|---|
| Pontosság | ~16ms, de csúszhat | Pontosan a képkocka előtt |
| Háttér fül | Továbbra is fut (CPU!) | Automatikusan szünetel |
| FPS | Fix (pl. 60) | Alkalmazkodik (60/120/144 Hz) |
| Batching | Nem optimalizált | A böngésző összevonja a DOM olvasásokkal |
// Időalapú animáció – azonos sebesség MINDEN monitoron // A delta time segítségével nem számít, hogy 60 vagy 144 Hz a kijelző var box = document.querySelector(".animated-box"); var position = 0; var lastTime = null; var SPEED = 200; // 200 pixel / másodperc function animate(timestamp) { // Az első híváskor nincs még lastTime if (lastTime === null) lastTime = timestamp; // Delta time: az előző képkocka óta eltelt idő (mp-ben) var deltaTime = (timestamp - lastTime) / 1000; lastTime = timestamp; // Pozíció frissítése: sebesség * idő = út position += SPEED * deltaTime; box.style.transform = "translateX(" + position + "px)"; // Amíg nem ért 500px-re, folytatjuk if (position < 500) { requestAnimationFrame(animate); } } requestAnimationFrame(animate); // Indítás
requestAnimationFrame-et komplex, programozott animációkhoz használjuk, ahol a CSS nem elég (pl. canvas rajzolás, fizika szimulációk, játékok).
A Geolocation API lehetővé teszi a felhasználó földrajzi pozíciójának lekérdezését. A böngésző engedélyt kér a felhasználótól, és csak HTTPS-en működik (biztonsági okokból).
// Egyszeri pozíció lekérdezés navigator.geolocation.getCurrentPosition( // Sikeres callback function(pos) { console.log("Szélesség: " + pos.coords.latitude); console.log("Hosszúság: " + pos.coords.longitude); console.log("Pontosság: " + pos.coords.accuracy + " m"); }, // Hiba callback function(err) { if (err.code === 1) console.log("Felhasználó megtagadta az engedélyt"); if (err.code === 2) console.log("Pozíció nem elérhető"); if (err.code === 3) console.log("Időtúllépés"); }, // Opciók { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 } ); // Folyamatos követés (pl. navigáció) var watchId = navigator.geolocation.watchPosition(function(pos) { updateMap(pos.coords.latitude, pos.coords.longitude); }); // Leállítás navigator.geolocation.clearWatch(watchId);
A Clipboard API aszinkron hozzáférést biztosít a rendszer vágólapjához. Engedélyt igényelhet a böngészőtől.
// Szöveg másolása a vágólapra async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); showToast("Vágólapra másolva!"); } catch (err) { console.error("Másolás sikertelen:", err); } } // "Másolás" gomb implementálása document.querySelector("#copy-btn").addEventListener("click", function() { var codeBlock = document.querySelector("pre code"); copyToClipboard(codeBlock.textContent); });
Az IntersectionObserver hatékonyan figyeli, hogy egy elem mikor kerül a viewport-ba (láthatóvá válik). Két klasszikus felhasználása: lazy loading (képek késleltetett betöltése) és infinite scroll (végtelen görgetés).
// Lazy loading: a képeket csak akkor töltjük be, amikor közel kerülnek // HTML: <img class="lazy" data-src="/images/photo.jpg" alt="Fotó"> var lazyObserver = new IntersectionObserver( // Callback: minden figyelt elemre meghívódik, ha változik a láthatóság function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) { var img = entry.target; img.src = img.dataset.src; // data-src => src (betöltés indul) img.classList.remove("lazy"); // Opcionális: stílus váltás lazyObserver.unobserve(img); // Leállítjuk a figyelést } }); }, // Opciók: 200px-szel ELŐBB betöltjük (a viewport szélétől) { rootMargin: "200px" } ); // Minden lazy képet figyelni kezdünk document.querySelectorAll("img.lazy").forEach(function(img) { lazyObserver.observe(img); });
<img loading="lazy"> attribútum is elég – nem kell JavaScript. Az IntersectionObserver-t összetettebb logikához (animált belépés, infinite scroll, analytics tracking) használjuk.
A FormData automatikusan kiolvassa az űrlap mezőit, és kényelmesen küldhető Fetch-csel. Két fő küldési mód:
var form = document.querySelector("#registration-form"); form.addEventListener("submit", async function(event) { event.preventDefault(); var formData = new FormData(form); // Automatikusan kiolvassa a mezőket // 1. mód: JSON küldés (ha nincs fájl) var jsonData = Object.fromEntries(formData); await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(jsonData), }); // 2. mód: FormData küldés (fájlfeltöltésnél) // FONTOS: NE állítsuk be a Content-Type fejlécet! // A böngésző automatikusan beállítja a multipart/form-data-t // a megfelelő boundary-vel. await fetch("/api/upload", { method: "POST", body: formData, // Közvetlenül a FormData objektumot adjuk }); });
A History API lehetővé teszi az URL módosítását az oldal újratöltése nélkül – ez az SPA (Single Page Application) routing alapja. Az URL API pedig az URL-ek strukturált elemzésére és módosítására szolgál.
// URL módosítása újratöltés nélkül history.pushState( { page: "products", id: 42 }, // state objektum (popstate-ben kapjuk) "", // title (a legtöbb böngésző ignorálja) "/products/42" // az új URL (megjelenik a címsorban) ); // Vissza/Előre gomb kezelése window.addEventListener("popstate", function(event) { if (event.state) { console.log("Navigáció: " + event.state.page); renderPage(event.state); } }); // URL elemzése az URL API-val var url = new URL("https://example.com/search?q=javascript&page=2#results"); console.log(url.pathname); // "/search" console.log(url.searchParams.get("q")); // "javascript" console.log(url.searchParams.get("page"));// "2" console.log(url.hash); // "#results" // URL építése url.searchParams.set("page", "3"); console.log(url.toString()); // "https://example.com/search?q=javascript&page=3#results"