Az utolsó fejezetben a tananyag kétféle megoldását összekötő technológiáról, a weboldalak részleges frissítéséért felelős AJAX-ról lesz szó, amelyben egyszerre jelenik meg a kliensoldali JavaScript kód és a szerveroldali PHP kód. Először bemutatásra kerül az AJAX technológia koncepciója, az ezt biztosító XMLHttpRequest objektum tulajdonságai, majd ennek segítségével történő feladatok megoldása és általánosítása.
Tananyagunk utolsó fejezetében egy olyan technológiáról esik szó, mely összefogja a kliens- és szerveroldali dinamikus programozásról tanultakat. Az AJAX technológiával lehetővé válik a szerverrel való kapcsolattartás a teljes oldal újratöltése nélkül, ezáltal sokkal folyamatosabb, nagyobb fokú élményt adva a felhasználóknak weboldalak használata közben. Éppen ez a tulajdonsága tette olyan népszerűvé ezt a technológiát a 2000-es évek közepén, és nyitotta meg a kapukat a modern és korszerű webes alkalmazások felé.
Az eddigi fejezetekben már többször volt szó egy weboldal (általában valamilyen webes tartalom) kiszolgálásának lépéseiről. A folyamat a HTTP protokoll szabályai szerint zajlik. Ebben a folyamatban mindig a kliens kezdeményezi a kapcsolatot egy HTTP kérés elküldésével. A kliens az esetek legnagyobb részében egy böngésző, és ezen belül a kérés tipikusan hivatkozásra kattintva vagy űrlapot elküldve, esetleg a címet közvetlenül a böngésző címsorába írva indul el. A szerver a kérést feldolgozva a kért tartalmat egy HTTP válasz keretén belül küldi vissza a kliensnek, amely egy idő után újra elölről kezdi ezt a folyamatot.
Az oldalkiszolgálás ilyen formáját a lenti ábra szemlélteti. Ezen jól látható, hogy az aktuális működés, feldolgozás vagy a kliensen, vagy a szerveren van, ezek váltogatják egymást időben egy picit eltolva (míg a kérés eljut a szerverig, és onnan vissza).
A hagyományos oldalkiszolgálásnak több következménye, hátránya is lehet:
A fenti problémák nagy része abból fakad, hogy a kérés elküldése és a válasz megérkezése között a böngészőbeli alkalmazás használhatatlan. Ez okozza a szaggatottság érzését. Ugyanakkor a szerverrel való kommunikáció elkerülhetetlen, mert bizonyos adatok és tartalmak csak onnan érhetőek el.
A megoldást erre a problémára az nyújtaná, ha úgy lehetne a szerverrel felvenni a kapcsolatot, hogy az oldal teljes újratöltését elkerüljük. Egy betöltött oldal esetében tehát valamilyen módon a háttérben kellene a HTTP kommunikációt lefolytatni.
A probléma nem új keletű, és már az 1990-es évek második felében többféle megoldást dolgoztak ki rá. Ezeknek az összefoglaló neve az angol remote scripting volt.
Ezeket a technológiákat már szórványosan használták a 90-es évek vége felé, de éppen ez volt az az időszak, amikor a webes fejlesztések elsősorban a szerveroldalra és az interoperabilitásra összpontosultak, így szélesebb körben, népszerűbb alkalmazásokban elvétve használták.
Ahogy azonban a tananyag elején található történeti áttekintésben is láttuk, a 2000-es évek közepére egyre több embernek lett széles sávú internetkapcsolata, egyre több alkalmazás költözik át a webre, egyre több ember használja ezeket, és ezzel együtt egyre nagyobb igény mutatkozik az igényes, felhasználóbarát, gyors webes alkalmazásokra. Az innovatívabb vállalatok látják ezt az igényt, és megjelennek a remote scripting technológiát alkalmazó modern webalkalmazások (Flickr, Google Maps, Google Docs), amelyek olyan élményt nyújtanak a böngészőben, mint asztali párjaik.
Ugyanekkor nevet kap az XMLHttpRequestre épülő technológia. 2005-ben Jesse James Garret egy blogbejegyzésében vázolja az AJAX koncepcióját, amely az oldalak részleges frissítésének problémakörét megoldó, már létező technológiák gyűjtőneve. A név az Aszinkron JavaScript és XML elnevezés rövidítéséből származik, és az alábbi kiforrott, szabványos technológiákra épül:
Az AJAX lelkét az XMLHttpRequest objektum jelenti, mely segítségével a háttérben aszinkron módon, azaz a felhasználói felület működtetésével párhuzamosan lehet a szerver felé kéréseket indítani és az onnan érkező válaszokat feldolgozni. Az oldalkiszolgálás folyamata a következő:
Az AJAX-szal működtetett oldal a következő tulajdonságokkal bír:
Egy AJAX-os hívás során a kliens ugyanúgy HTTP protokollon keresztül lép kapcsolatba a szerverrel, mint a hagyományos oldalkiszolgálás esetén, az egyetlen különbség az, hogy AJAX esetén nem a böngésző kezdeményezi a kérést, hanem mindezt JavaScript programmal vezéreljük. A kapcsolattartásért az XMLHttpRequest objektum a felelős, így AJAX-os oldalak készítéséhez először is ezt az objektumot kell megismernünk.
Az XMLHttpRequest objektum manapság már minden böngészőben egyformán elérhető, de majd csak a HTML5 életbelépésével együtt válik szabványossá. Mint minden JavaScript objektumot, őt is a tulajdonságain és metódusain keresztül ismerhetünk meg.
A legegyszerűbb feladatként egy gomb megnyomására kérjük le a szerverről az aktuális időt.
A felhasználói felület ugyancsak spártai lesz: egy gombot rakunk fel a kérés kezdeményezésére, a választ pedig egy egyszerű div elemben várjuk.
A hagyományos megoldásban egy űrlapon keresztül küldjük el a kérést a szervernek. Az ott lefutó PHP állomány újra legenerálja az űrlapot, és az aktuális időt is. Az űrlap megint csak önmagára mutat. Ellenőrzésképpen a $_GET és $_POST tömb is kiírásra kerül.
<?php print_r($_GET); print_r($_POST); $ido = date('Y.m.d. G:i:s'); ?> <!doctype html> <html> <head> <meta charset="utf-8"> <title>AJAX példa</title> </head> <body> <form action="pingphp.php" method="get"> <input type="submit" id="gomb" value="Ping"> </form> <hr> <div id="output"> <?php echo $ido; ?> </div> </body> </html>
A hagyományos megoldással ellentétben az AJAX-os alkalmazásban kerülni szeretnénk azokat az elemeket, amelyek a böngészők automatikusan az oldal újratöltésére kényszerítik. Így űrlap helyett egyszerű gombot teszünk fel az oldalra, amelyre kattintva JavaScripttel kezdeményezzük a kérést. A HTML szerkezet a továbbiakban tehát ekként változik (ping.html):
<!doctype html> <html> <head> <meta charset="utf-8"> <title>AJAX példa</title> <script type="text/javascript" src="ping.js"></script> </head> <body> <input type="button" id="gomb" value="Ping"> <hr> <div id="output"></div> </body> </html>
Szerveroldalon pedig a következő PHP szkriptet hívjuk (ping.php):
<?php print_r($_GET); print_r($_POST); echo date('Y.m.d. G:i:s'); ?>
Első megoldásunkban a gombra kattintva ún. szinkron kérést intézünk a szerver felé. Ehhez a gomb lenyomásakor létrehozunk egy új XMLHttpRequest objektumot (new XMLHttpRequest()), beállítjuk a küldési paramétereket az open() parancs segítségével külön ügyelve, hogy a harmadik paraméterben a szinkron módot jelezzük egy hamis értékkel, majd a send paranccsal elküldjük a kérést. A szinkron kérés azt jelenti, hogy a JavaScript kód futása akkor folytatódik, ha a válasz megérkezett. Ekkor pedig nincsen más hátra, mint a válaszszövegben érkező értéket (responseText) megjelenítsük a div elemben. A ping.js ennek megfelelően így alakul:
//Segédfüggvények function $(id) { return document.getElementById(id); } //Oldal betöltésekor lefutó függvény function init() { $('gomb').onclick = ping; } window.addEventListener('load', init, false); //A gomb lenyomásakor lefutó függvény function ping() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'ping.php', false); xhr.send(null); $('output').innerHTML = xhr.responseText; }
A megoldás fő hátrány a szinkronitásból fakad. Ha ugyanis a kérés kiszolgálása sokáig tart, akkor JavaScript kód sokáig nem fut tovább, és ez az egész felületet használhatatlanná teszi erre az időre. Ez pedig a hagyományos oldalkiszolgálásnál tapasztalt szaggatottságot eredményezi, amit éppen elkerülni próbálunk.
A folyamatos élményt éppen az biztosítja, hogy a kérés a felület használatával párhuzamosan fut. Ezt hívjuk aszinkron futásnak, amikor a kérés és a felület működtetése között nincsen folyamatos egymásra utaltság.
Ilyen kérést az open metódus harmadik paraméterének igazra állításával lehet indítani. A kérés teljesüléséről, pontosabban a kérés állapotváltozásairól az XMLHttpRequest objektum a readystatechange eseményen keresztül értesíti a környezetét. Erre kell a programunknak feliratkoznia, ha a kérés végére kíváncsi. Ezt a readyState tulajdonság 4-es értéke, és a 200-as HTTP státuszkód jelzi.
var xhr; function ping() { xhr = new XMLHttpRequest(); xhr.open('GET', 'ping.php', true); xhr.addEventListener('readystatechange', pingKezelo, false); xhr.send(null); } function pingKezelo() { if (xhr.readyState == 4 && xhr.status == 200) { $('output').innerHTML = xhr.responseText; } }
Mivel az aszinkron kezelés miatt két függvénybe esik szét a programlogika, és mindkettőben szükség van az xhr objektumra, ezért ezt globális változónak adtuk meg. A globális változókkal azonban több probléma is adódik:
A fenti problémákat egyszerűen elkerülhetjük úgy, ha az eseménykezelő függvénynek átadjuk a kérést kezdeményező XMLHttpRequest objektumot egy névtelen függvény segítségével.
function ping() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'ping.php', true); xhr.addEventListener('readystatechange', function () { pingKezelo(xhr); }, false); xhr.send(null); } function pingKezelo(xhr) { if (xhr.readyState == 4 && xhr.status == 200) { $('output').innerHTML = xhr.responseText; } }
A pingKezelo függvényben keveredik az XMLHttpRequest objektum állapotváltozásának a vizsgálata a válasz feldolgozásának logikájával. Emeljük az előbbi részt a névtelen eseménykezelő függvényünkbe.
function ping() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'ping.php', true); xhr.addEventListener('readystatechange', function () { if (xhr.readyState == 4 && xhr.status == 200) { pingKezelo(xhr); } }, false); xhr.send(null); } function pingKezelo(xhr) { $('output').innerHTML = xhr.responseText; }
Mivel az XMLHttpRequest objektum a HTTP protokollon keresztül intézi a szerverrel a kapcsolatot, ezért paramétereket ugyanúgy kell meghatározni, mint ahogy azt a HTTP-vel foglalkozó fejezetünkben bemutattuk. GET paraméterek szerveroldali szkriptnek való átadásához a kérésszöveget az URL-hez kell csatolni. Példánkban az idő mellett a $_GET és $_POST tömbök is megjelennek a válaszban. Ezeken keresztül tudjuk ellenőrizni, hogy a felküldött adat megérkezett-e.
function ping() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'ping.php?alma=piros', true); xhr.addEventListener('readystatechange', function () { if (xhr.readyState == 4 && xhr.status == 200) { pingKezelo(xhr); } }, false); xhr.send(null); } function pingKezelo(xhr) { $('output').innerHTML = xhr.responseText; }
POST adatok küldéséhez a kérésszövegnek a HTTP kérés üzenettörzsében kell megjelennie. Ezt úgy érhetjük el, hogy a send metódusnak paraméterként adjuk át a kérésszöveget. POST adatok küldéséhez HTTP fejlécben jelezni kell, hogy a szöveges adatok URL kódolással kerülnek elküldésre (application/x-www-form-urlencoded, ugyanilyen kódolással küldi fel az űrlap is az elemeket).
function ping() { var xhr = new XMLHttpRequest(); xhr.open('POST', 'ping.php?alma=piros', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.addEventListener('readystatechange', function () { if (xhr.readyState == 4 && xhr.status == 200) { pingKezelo(xhr); } }, false); xhr.send('korte=sarga'); } function pingKezelo(xhr) { $('output').innerHTML = xhr.responseText; }
Manapság szinte kivétel nélkül minden korszerű böngésző támogatja az XMLHttpRequest objektum konstruktorfüggvénnyel történő előállítását (new XMLHttpRequest()). Régebbi böngészőkben, főleg régebbi Internet Explorer verziókban ezt az objektumot másképpen kellett előállítani. Ahhoz, hogy kódunk ne függjön a böngészők típusától, érdemes az XMLHttpRequest objektum előállítását egy külön segédfüggvényben végrehajtani.
function ujXHR() { var xhr = null; try { xhr = new XMLHttpRequest(); } catch(e) { try { xhr = new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) { try { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) { xhr = null; }}} return xhr; } function ping() { var xhr = ujXHR(); //... }
Sikertelen kérést érdemes a kódunkban valamilyen módon kezelni. Az alábbi példában nem 200-as hibakód esetén a konzolon jelezzük a hibát.
function ping() { var xhr = ujXHR(); xhr.open('POST', 'ping.php?alma=piros', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.addEventListener('readystatechange', function () { if (xhr.readyState == 4) { if (xhr.status == 200) { pingKezelo(xhr); } else { console.log('Hiba'); } } }, false); xhr.send('korte=sarga'); }
Egy AJAX hívásnál alkalmazott lépések nagyjából hasonlóak a fenti ping függvényben megismert folyamathoz feladattól függetlenül. A következő paraméterek térnek el kérésenként:
Érdemes tehát a fenti lépéseket kiemelni egy segédfüggvénybe, és a feladatfüggő adatokat paraméterként megjeleníteni benne. Mivel viszonylag sok paraméterünk van, és ezek közül nem mindegyiket kell minden esetben megadni, az ajax() segédfüggvényünk ezeket a paramétereket egységbe foglaló objektumot vár egyetlen paraméterként. A függvény elején megnézzük, érkezett-e megfelelő paraméter, és ha nem, akkor a megfelelő alapértelmezett értékkel töltjük fel. A másik említésre érdemes dolog, hogy a siker függvény az xhr objektum mellett paraméterként a választ is megkapja.
function ajax(opts) { var mod = opts.mod || 'GET', url = opts.url || '', getadat = opts.getadat || '', postadat = opts.postadat || '', siker = opts.siker || function(), hiba = opts.hiba || function(); mod = mod.toUpperCase(); url = url+'?'+getadat; var xhr = ujXHR(); xhr.open(mod, url, true); if (mod === 'POST') { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); } xhr.addEventListener('readystatechange', function () { if (xhr.readyState == 4) { if (xhr.status == 200) { siker(xhr, xhr.responseText); } else { hiba(xhr); } } }, false); xhr.send(mod == 'POST' ? postadat : null); return xhr; }
Az ajax segédfüggvénnyel példafeladatunk megoldása paraméterezési kérdéssé egyszerűsödik:
function ping() { ajax({ mod: 'post', url: 'ping.php', getadat: 'alma=piros', postadat: 'korte=sarga', siker: pingKezelo }); } function pingKezelo(xhr, text) { $('output').innerHTML = text; }
Vagy a pingKezelo függvényt rögtön a siker paraméternek adva:
function ping() { ajax({ mod: 'post', url: 'ping.php', getadat: 'alma=piros', postadat: 'korte=sarga', siker: function (xhr, text) { $('output').innerHTML = text; } }); }
AJAX kérésekben adatot szerveroldali bemeneti paraméterként megadni a következőképpen lehet:
Az érkező válasz sokféle formátumban érkezhet:
A válasz az előállítás módjától függően lehet:
A kliens szempontjából mindegy, hogy a tartalom hogyan áll elő, ő a HTTP válasz végeredményét dolgozza fel.
Az alábbiakban a különböző lehetséges válaszformátumokat és azok feldolgozását nézzük végig egy példán keresztül. A válaszokat az egyszerűség kedvéért statikus tartalomként adjuk meg, a fejezet végén pedig megnézzük, hogyan állíthatóak elő ezek dinamikusan PHP segítségével.
Példafeladat: Adott gyümölcsök listája, jelenítsük meg felsorolásként.
A HTML kód:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>AJAX példa</title> <script type="text/javascript" src="ajax.js"></script> <script type="text/javascript" src="format.js"></script> </head> <body> <input type="button" id="btnText" value="Text"> <input type="button" id="btnJSON" value="JSON"> <input type="button" id="btnHTML" value="HTML"> <input type="button" id="btnXML" value="XML"> <input type="button" id="btnScript" value="Szkript"> <hr> <div id="output"></div> </body> </html>
A szöveges válasz feldolgozása a válasz formátumától függ, igazából bármi lehet. Emiatt a feldolgozási logika sokszor egyedi és bonyolult. Viszonylag ritkán használják.
Példánkban a válasz formátuma:
A feldolgozó szkript:
function lista(t) { return '<ul><li>' + t.join('</li><li>') + '</li></ul>'; } function text() { ajax({ url: 'gyumolcs.txt', siker: function (xhr, text) { console.log(text); var t = text.split(','); $('output').innerHTML = (new Date()).toLocaleString() + lista(t); } }); }
A JSON egy nagyon elterjedt, általános adatleíró formátum. Nagyon gyakran használják kliensoldali szkriptekben, mert JavaScripttel könnyen feldolgozható (JavaScript lévén maga a formátum is). JSON válasz értelmezése vagy a JavaScript eval függvényével, vagy az utóbbi időben egyre szélesebb körben támogatott JSON.parse metódussal lehet. Az értelmezés végén kapott adatszerkezetet struktúrájának megfelelően kell feldolgozni.
Példánkban a JSON válasz:
[ "alma", "körte", "szilva", "barack", "eper", "málna", "szeder" ]
A feldolgozó szkript (lista függvényt ld. feljebb):
function json() { ajax({ url: 'gyumolcs.json', siker: function (xhr, text) { var json = eval(text); // vagy var json = JSON.parse(text); console.log(json); $('output').innerHTML = (new Date()).toLocaleString() + lista(json); } }); }
A HTML szabványos formátumként nagyon elterjedt. Nagyon egyszerű a feldolgozása, hiszen tipikusan a választ egy másik elembe kell helyezni.
Példánkban a HTML válasz:
<ul> <li>alma</li> <li>körte</li> <li>szilva</li> <li>barack</li> <li>eper</li> <li>málna</li> <li>szeder</li> </ul>
A feldolgozó szkript:
function html() { ajax({ url: 'gyumolcs.html', siker: function (xhr, text) { var html = text; console.log(html); $('output').innerHTML = (new Date()).toLocaleString() + html; } }); }
Egy szabványos szöveges adatleírási formátumról van szó, amely nagyon elterjedt, de inkább vállalati alkalmazások adatleíró és kommunikációs formátumaként. Webes alkalmazásokban ritkábban használják kliensoldalon, valószínűleg erőforrásigényes feldolgozása miatt. Az AJAX betűszó X betűje jelzi, hogy eredetileg ezt szánták a fő válaszformátumnak. Az XMLHttpRequest objektum fel van készítve XML dokumentum fogadására, responseXML metódusa ennek az értelmezett DOM fáját tartalmazza (ld. XML DOM).
Példánk XML válasza:
<?xml version="1.0" encoding="UTF-8"?> <gyumolcsok> <gyumolcs>alma</gyumolcs> <gyumolcs>körte</gyumolcs> <gyumolcs>szilva</gyumolcs> <gyumolcs>barack</gyumolcs> <gyumolcs>eper</gyumolcs> <gyumolcs>málna</gyumolcs> <gyumolcs>szeder</gyumolcs> </gyumolcsok>
Feldolgozása a korábbról ismert DOM műveletekkel lehetséges:
function xml() { ajax({ url: 'gyumolcs.xml', siker: function (xhr, text) { var xmldom = xhr.responseXML; console.log(xmldom); var gyumolcsok = xmldom.getElementsByTagName('gyumolcs'); var t = []; for (var i = 0; i < gyumolcsok.length; i++) { t.push(gyumolcsok[i].firstChild.nodeValue); }; $('output').innerHTML = (new Date()).toLocaleString() + lista(t); } }); }
Válaszként érkezhet JavaScript kód is, amely jól illeszkedik a kliensoldali környezetbe, feldolgozása is egyszerű. A szövegként érkezett kódot többféleképpen értelmeztethetjük. Ebben a példában egyszerűen az eval függvényt használjuk erre a célra.
Példa válaszunk:
function getGyumolcsok() { return [ "alma", "körte", "szilva", "barack", "eper", "málna", "szeder" ]; }
A feldolgozó szkript:
function script() { ajax({ url: 'gyumolcs.js', siker: function (xhr, text) { console.log(text); eval(text); var t = getGyumolcsok(); $('output').innerHTML = (new Date()).toLocaleString() + lista(t); } }); }
Az előző esetekben nem volt fontos a feldolgozó szkript számára, hogy a válasz statikusként volt megadva vagy program állította elő. Természetesen a fenti formátumok bármelyikét elő lehet állítani szkript segítségével.
PHP oldalon (gyumolcs.php) a gyümölcslista tárolására tömböt érdemes használni. Ebből az egyik legegyszerűbb mód a JSON formátum előállítása:
<?php $gyumolcsok = array( "alma", "körte", "szilva", "barack", "eper", "málna", "szeder", ); echo json_encode($gyumolcsok); ?>
AJAX hívások tesztelését, nyomon követését a JavaScript konzolok megfelelő eszközeivel lehet megtenni. Szinte mindegyik böngésző konzoljában van olyan eszköz, amely figyeli és a konzolra kiírja az XMLHttpRequest objektumon keresztül végzett hívásokat. Itt megtekinthetők:
Természetesen AJAX hívásokat tartalmazó kódunkban érdemes a hívás körül megfelelő console.log utasításokkal a paramétereket és az érkezett választ kiíratni.
Biztonsági okokból nem engedélyezett különböző domainek között AJAX kommunikáció. Ez alól kivételt az ún. JSONP (JSON with Padding) képez, amely script elem dinamikus beszúrásával idegen tartalom futtatását teszi lehetővé.
Az AJAX hívásokat továbbra is a kliens kezdeményezi, azaz szerver push-ra lehetőség itt sincsen. (Ehhez ld. a megfelelő HTML5-ös technológiákat.)
Mivel az AJAX-os oldalak másképp működnek, mint a hagyományos oldalak, amelyekhez a felhasználók hozzászoktak, így ilyen oldalak tervezését különösen meg kell fontolni, és megfelelő visszajelzéseket kell adni a felhasználónak a háttérben zajló műveletekről. Mindig a használhatóságot kell szem előtt tartani.
A tananyag az ELTE - PPKE informatika tananyagfejlesztési projekt (TÁMOP-4.1.2.A/1-11/1-2011-0052) keretében valósult meg.
A tananyag elkészítéséhez az ELTESCORM keretrendszert használtuk.