Hogyan fejlesszünk AJAX alkalmazást bevezető

Előre le kell szögezne, hogy az íráshoz, annyira semmi közöm sincs, hogy még mindegyik részét se értem. :) Ettől függetlenül nagyon precíz, szájbarágós, és kódrészletekkel, kódmagyarázatokkal felépített, konkrét példamegoldáson alapuló leírás, rrd tollából. Az írás annyira egyedülálló magyar nyelven, hogy semmikép se szeretném ha elveszne, ezért inkább én is learchiválom, de az eredeti itt található meg. Jó olvasgatást és fejlesztést.

Kóstoljunk bele az AJAX-ba!

Zsong az internet az AJAX-tól. Gondolom mindenkiben felmerült már a kérdés, hogy mi is ez, és kell-e nekem. Mivel magyarul alig-alig lehet még a témáról leírást találni gondoltam itt az idõ bepótolni a hiányt.

Figyelem! Azok számára akik az AJAX-ot csak takarítószer formájában ismerik javaslom, hogy olvassanak tovább mielõtt belekóstolnak!

Az alapok

A szó

Maga a szó az Asynchronous JavaScript and XML (Aszinkron Javascript és XML) rövidítése, ami egy szép, tökéletesen semmitmondó és hibás megfogalmazás. Hibás abból a szempontból, hogy XML nélkül is vígan muzsikál a dolog.

Akkor meg mi?

Én inkább úgy fogalmaznék, hogy az AJAX, a Javascript, a CSS, a HTML, a DOM és egy szerver oldali nyelv (pl a PHP) együttes használatának egy speciális kombinációját valósítja meg, úgy hogy segítségével egyesíteni lehet a szerver és kliens oldali programozási előnyöket.

Az AJAX ugyanis lehetőséget biztosít számunkra, hogy az oldal bizonyos részei a többitől teljesen függetlenül újratöltődjenek / cserélődjenek.

Mit kell hozzá tudnom ha használni akarom?

  • be kell tudnunk kapcsolni a számítógépet a nagy POWER feliratú gomb segítségével :)
  • ismernünk kell a HTML nyelvet
  • tudnunk kell használni az XMLHttpRequest Javascript objektumot, hogy adatokat tudjunk küldeni a szervernek és fogadni attól
  • szükséges egy szerver oldali programozási nyelv ismerete, mivel az AJAX alkalmazásunk folyamatosan a szerverrel fog kommunikálni, és attól fog mindenféle adatokat lekérni, így nem árt ha valahogy rá tudjuk venni a szervert hogy válaszoljon és azt amit kell
  • ismernünk kell a DOM-ot
  • használnunk kell tudni a JavaScript DOM manipuláló eljárásait
  • ismernünk kell a CSS-t, hogy a Javascript DOM manipuláció során a megfelelő CSS beállításokat tudjuk használni
  • kifinomultabb alkalmazásoknál ismernünk kell az xml nyelvet, mivel a szerver jellemzően xml válaszokat fog adni a kéréseinkre

Hogyan működik az egész, és mi a különbség a normál HTTP kommunikációhoz képest?

A hagyományos felfogás szerint ha egy oldalon rákattintunk egy linkre, kitöltünk egy user beléptető formot, akkor a böngésző elküldi a form adatokat (vagy lekéri a linkel oldalt) a szervernek és a szerver a teljes oldalt visszaküldi a böngészőnek ami újratölti azt teljes egészében még akkor is ha ténylegesen az oldalnak csak egy kicsi része változott meg. (Példánknál maradva egy usernév és jelszó beírására alkalmas form helyett mondjuk megjelenik egy ‘5 hozzászólásra van még lehetőséged’ felirat, és az oldal többi része teljesen változatlan.) Ugye az azonos tartalmak cache-elését és a helyi lemezről való betöltését a böngészők megoldják, de általában csak a css fileokat, a képeket és a html-ből belinkelt javascript fileokat cachelik, a tartalmat nem.

Ezzel két gondunk van:

  • a user oldaláról: az oldal lassabban fog betöltődni hiszen legjobb esetben is a teljes szöveget újra kell tölteni, a DOM-t a 0-ról újra kell építeni
  • az üzemeltető oldaláról: ugyanannak a kapcsolódó usernak ismételten ki kell küldeni ugyanazt a tartalmat ami növeli a szerver felhasznált sávszélességét.

Ezzel szemben az AJAX-os alkalmazásunk kizárólag a megváltozott tartalmat fogja a szervertől lekérni, és az oldalnak csak ezt a részét fogja újragenerálni, anélkül, hogy a user munkáját félbeszakítaná.

AJAX alkalmazások építési elvei

  1. a szerver adatot szolgáltat nem tartalmat
  2. a böngésző egy alkalmazást szolgál ki nem tartalmat
  3. a felhasználó hosszan, sokat és folyamatosan interaktálhat az alkalmazással

Lássuk a medvét!

Most már, hogy teljesen :) tisztában vagyunk a szükséges elméleti háttérrel lássunk egy konkrét példát. Az egyszerűség kedvéért nem xml adatcserével, hanem sima adatcserével fogjuk a feladatot megoldani.

A feladat

Adott egy oldal és azt szeretnénk, hogy az egyik dobozban a hírkereső friss rovatának 5 legfrisebb híre jelenjen meg, és percenként automatikusan frissüljön. A kész alkalmazás munka közben megtekinthető itt.

Az oldalunk html része

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
  <title>AJAX mintafeladat</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link rel="stylesheet" type="text/css" href="hirkereso.css" />
  <script type="text/javascript" src="./hirkereso.js"></script>
</head>
<body>
<div id="lap">
  <div id="fejlec">
    <h1>AJAX minta alkamazás</h1>
  </div>
  <div id="kozep">
    <div id="balsav">
      <div id="menu">
        <h3>Legfrissebb hírek a hírkeresőn</h3>
        <div id="ujhirek">
        </div>
      </div>
    </div>
    <div id="tartalom">
      <h2>
        Az oldal percenként automatikusan le fogja kérni a hírkereső oldal 5 legfrissebb cikkét.
      </h2>
    </div>
  </div>
</div>
</body>
</html>

A character-set értékét érdemes utf-8-ra állítanunk, mert az AJAX kommunikáció többnyire ezen történik. A html felépítésében nincs semmi érdekes azon felül, hogy indulásként definiálunk egy üres ujhirek id-jű div-et, ami majd az érkező adatokat fogja tartalmazni

A szerver oldali script

A szerver oldali scriptünk be fogja olvasni a hirkereso.hu-t, az előre megadott (reguláris) mintára illeszkedő sort kiírja, és ha ezt 5-ször megtette akkor kilép. A script simán a print / echo függvényt használja a kiíratásra, mivel majd a Javascript gondoskodik arról, hogy az így kiíratott oldal tartalma megjelenjen az oldalunkon.

Az itt bemutatott script PHP nyelven íródott de bármely szerveroldali nyelven készülhetett volna, mivel az AJAX alkalmazásunk bármelyikkel tud kommunikálni, legyen az Java, ASP vagy bármi egyéb.

Előszőr beállítjuk a headereket amelyek a helyes karakterkódolásban és a cache-elés elkerülésében segítenek. A böngészők cache-je egy okos kis találmány, de AJAX alkalmazásunkban mindig gondoskodni kell róla, hogy a böngésző ne használja a cache-t, mivel a folyamatos szerverrel történő kommunikáció a fontos.

<?php
//hírkereső lekérése
header("Content-Encoding: utf-8");  

//csak GET-nél használható, IE kibukik máskülönben rajta
if($_GET) header("Content-Type: text/html; charset=utf-8");

//ne cacheljenek a browserek
header('Expires: Wed, 23 Dec 1980 00:30:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');

A hirkereso forrását áttanulmányozva azt láttam, hogy a friss bejegyzések a <div class=”hirszovegSoremeles”> stringgel kezdődnek, ezt adjuk meg keresési mintának. Maximalizáljuk a találatok számát 5-re, nullázzuk a találatok számolására szolgáló változót és megnyitjuk a hirkereso oldalt.

print '<ul>';
$keresminta = '<div class="hirszovegSoremeles">';
$max = 5;
$i = 0;
$fp = fopen("http://hirkereso.hu/", "r");
  if (!$fp){
    print 'A hírkereső oldala nem elérhető!';
    }

Ezután szépen végigballagunk a file-on soronként és megvizsgáljuk, hogy illeszkedik-e a keresési mintánkra. Ha igen kiveszünk minden formázási utasítást (class, style, font), és esetlegesen bezavaró html elemet (div, span). ezzel azt biztosítjuk, hogy a találatokban a linkek bennmaradnak, de semmilyen más formázási utasítást nem tartunk meg.

  else{
    $megvan = false;
    while(!feof($fp) && !$megvan){
      $sor = fgets($fp);
      //keressük meg az első bejegyzést (sort) ami illik a $keresminta mintára
      if(strpos($sor,$keresminta) !== false){
        //növeljük a számlálót
        $i++;
        if($i == $max) $megvan = true;

        //pucolgassuk a felesleget
        $sor = preg_replace("/style=\"[^\"]*\"/",'',$sor);
        $sor = preg_replace("/class=\"[^\"]*\"/",'',$sor);
        $sor = preg_replace("/<\/*div[^>]*>/",'',$sor);
        $sor = preg_replace("/<\/*span[^>]*>/",'',$sor);
        $sor = preg_replace("/<\/*font[^>]*>/",'',$sor);

Végül a találatot konvertálnunk kell UTF-8-ra, mivel a hírkereső ISO-8859-2-vel dolgozik.

        print '<li>'.iconv("ISO-8859-2", "UTF-8", $sor).'</li>';
        }
      }
    fclose($fp);
    }
print '</ul>';
?>

Lényegében ennyi, mivel ez a kis scriptecske pontosan azt fogja csinálni amit mi akarunk, vagyis a legfrisebb 5 hírt a megfelelő formában és karakterkódolással kiírja. (Persze éles rendszeren némi hibakezelést nem árt még hozzáadni, mivel a hírkereső bármikor megváltoztathatja az oldal felépítését, vagy átnevezheti a szóban forgó divet hirszovegSoremeles-ről valami másra.)

Lényeg a lényeg ha feltöltjük a PHP filet a szerverünkre mondjuk hirkereso.php néven akkor annak közvetlen meghívásával ellenőrizhetjük, hogy az AJAX alkalmazásunk szerver oldali része működik.

A javascript file-unk (hirkereso.js)

Először is arról kell gondoskodnunk, hogy miközben a szerverrel való adatcsere folyik a user valahogy lássa, hogy valami történik a háttérben, különben nem fogja érteni, hogy miért tűnik el a lista amit olvasgat(na). Ehhez definiálunk egy globális változót, ami esetünkben egy animált gif lesz. Így fog majd kinézni:

var loading = '<img src="./loading.gif" />';

Aztán létrehozunk egy új objektum osztályt, és megadunk pár állapotjelző állandót.

var ajax = new Object();

//állapotjelző állandók
ajax.ALLAPOT_UNINITIALIZED = 0;
ajax.ALLAPOT_LOADING = 1;
ajax.ALLAPOT_LOADED = 2;
ajax.ALLAPOT_INTERACTIVE = 3;
ajax.ALLAPOT_COMPLETE = 4;

Jöhet az objektum konstruktora, ami az objektumhíváskor megadott változókat elérhetővé teszi a többi tagfüggvény részére, és meghívja az objektum betolt függényét. Az url, method, parameterek és contentType paraméterek magához a szerverrel való kommunikációhoz kellnek majd, az onload és onerror paraméterek szabályozzák, hogy mely függvények hívódjanak meg ha az objektumban a kérés sikeresen lefutott, illetve ha nem, végül a celDiv paraméter azt határozza meg, hogy melyik div-be kerüljenek a szervertől érkező adatok.

//a konstruktor
ajax.adatBetolto = function(url,onload,onerror,method,parameterek,contentType, celDiv){
  this.lekeres = null;
  this.onerror = (onerror) ? onerror : this.alapHiba;
  this.celDiv = (celDiv) ? celDiv;
  this.betolt(url,method,parameterek,contentType);
  }

Az objektum betölt függvénye. A prototype-pal memóriát spórolunk, mert így a függvény csak egyszer jön létre és minden példányhoz csatolódik, nem minden ojjektumpéldányban külön-külön jön létre. Gondoskodunk arról, hogy ha nincs megadva a method (POST / GET) akkor használjuk alapértelmezettként a GET-et, illetve ha POST van megadva akkor megadjuk hozzá a megfelelő content type headert.

ajax.adatBetolto.prototype.betolt = function(url,method,parameterek,contentType){
  method = (method) ? method : "GET";
  if (!contentType && method == "POST"){
    contentType = 'application/x-www-form-urlencoded';
    }

Mivel az Internet Expóker másképpen kezeli az XMLHttpRequest objektumot (legalábbis a 6-os verzióig, a 7-esben már natívan kezeli) a window.XMLHttpRequest objektum létezésének vizsgálatával megállapítjuk, hogy normál XMLHttpRequest objektumot, vagy ActiveXObject objektumot kell létrehoznunk. Akár így, akár úgy a lekeres objektumunk elvileg létrejön.

  //XMLHttpRequest ojjektum létrehozása minden böngésző számára
  if (window.XMLHttpRequest){
    this.lekeres = new XMLHttpRequest();
    }
  //a user saját szerencsétlenségére IE-t használ
  else if (window.ActiveXObject){
    this.lekeres = new ActiveXObject("Microsoft.XMLHTTP");
    }

Ha nem csak elvileg hanem ténylegesen létrejött a lekeres nevű objektumunk, akkor itt az idő megtennünk amit meg kell. Indítsuk el az adatforgalmat a szerver felé.

	
  if (this.lekeres){
    try{
      var loader = this;

Amikor kiküldünk egy kérést a szerver felé akkor az több állapoton fog keresztülmenni, a fent meghatározott állapot állandóknak megfelelően. Definiáljuk az XMLHttpRequest objektum onreadystatechange tulajdonságát, ami egy event listener (hogy mondjuk ezt magyarul? eseményfigyelő?). A mi esetünkben az objektum readyState tulajdonságának változásainak alkamával meg fog hívódni az allapot nevű tagfüggvény.

      this.lekeres.onreadystatechange = function(){
        ajax.adatBetolto.allapot.call(loader);
        }

Ok. Ha már mindent előkészítettünk akkor küldjük ki a kérést a szervernek. Ehhez az objektum open függvényét fogjuk meghívni. Az open a következő paramétereket várja:

  • method: milyen módon küldjük az adatokat (GET / POST)
  • url: mi a meghívandó file elérési útja (csak ugyanazon a szerveren lehet, mint ahonnan a javascript fileunk letöltődött)
  • async: aszinkron adatátvitel esetén true-ra kell állítani. Ez az AJAX lényege ezért itt hardcode-olva true-n van.
  • username, password: http hitelesítés esetén megadandó adatok
      this.lekeres.open(method,url,true);

Ha fennt definiáltuk, akkor itt beállítjuk a megfelelő content type headert.

      if (contentType){
        this.lekeres.setRequestHeader('Content-Type', contentType);
        }

A kommunikáció kliens oldali utolsó lépéseként pedig ténylegesen elküldjük a kérést a szervernek. Ha külön paraméterekkel hívtuk meg az objektumot, akkor itt elküldjük a paramétereket is.

      this.lekeres.send(parameterek);
      }

A teljes fenti kommunikációs blokkot egy try-on belül helyeztük el, hogy hiba esetén el tudjuk az itteni catch blokkal kapni a hibaüzenetet és meg tudjuk hívni a hibakezelő taggfüggvényt.

    catch (err){
      this.onerror.call(this);
      }
    }
  }

Lássuk az allapot tagfüggvényt. Létrehozzuk a lekeres, a httpStatusz és a ready nevű helyi változókat a rövidebb hivatkozások érdekében.

ajax.adatBetolto.allapot = function(){
  var lekeres = this.lekeres;
  var ready = lekeres.readyState;
  try{
    var httpStatusz = lekeres.status;

Ugye a most létrehozott ready változónk a szerver válasza szerint váltogatni fogja az értékét ahogy az oldal betöltésének állapota változik. Az IE némileg itt is másképpen viselkedik mint a böngészőprogramok, de ha csak a végső stádiumot (azaz a befejezett betöltődést) akarjuk kezelni, akkor nem lesz ezzel gondunk. Érdemes kipróbálni, hogy mi történik, ha az if elé beszúrunk egy alert(httpStatusz); sort. Ha megfigyeljük az IE szerint nem ugyanazokon az állapotokon megy keresztül a várt csomag mint a többiek szerint.

Na, szóval azt fogjuk vizsgálni, hogy ha a csomag állapota complete, azaz befejezett, akkor milyen válasz kódot küldött ki a szerver.

    if (ready == ajax.ALLAPOT_COMPLETE){

Ha 200-at akkor a szerver oldalon minden rendben van, a szerver válaszolt, megküldte számunkra a várt adatot, esetünkben a legfrisebb 5 cikket.

      if (httpStatusz == 200 || httpStatusz == 0){  //ha OK a státusz vagy nem foglalt
         this.onload.call(this);
          }

Ha nem, akkor valami gubanc van, meg kell hívnunk a hibakezelő függvényt. Akkor jutunk ide, ha szerver nem 200-as OK kódot küldött vissza hanem a leggyakrabban előforduló következők egyikét:

304 Not Modified
A böngészőnk azt állítja, hogy van egy cache-elt változata az adatokból és a szerver azt válaszolja, hogy azóta nem módosult az oldal. Ez elvileg ugye nem lehet, mert fenn gondoskodtunk a cache kikerüléséről.
401 Unauthorized
Ezt a választ kapjuk, ha kérésünkhöz http autorizáció szükséges, de valami nem stimmelt. Ebben az esetben érdemes megnézni az open függvényünk user és password részét.
403 Forbidden
A válasz megjelenítése nem engedélyezett.
404 Not Found
A keresett lap nem található. Esetleg rosszul adtuk meg a meghívandó szerver oldali file nevét vagy helyét.
500 Internal Server Error
Szerverhiba.
503 Service Unavailable
A szerver nagyon leterhelt, nem tud válaszolni.
      else{  //ha hiba érkezett
        this.onerror.call(this);
        }
      }
    }

Ha a try blokkunk hibát dobott akkor azért ezt még elkapjuk.

  catch(e){}
  }

Definiálunk egy hibakezelő tagfüggvényt is, úgy, hogy a hibaüzenetet oda írja be ahová egyébként a szervertől jövő adatokat várjuk. Nem túl elegáns megoldás, de tanulásként pont megfelelő. Kiíratjuk a readyState, a status változókat és a szerver válaszának header-jeit.

ajax.adatBetolto.prototype.alapHiba = function(){
  this.celDiv.innerHTML = "Adatlekérési hiba!" + 
  "<br />Állapot:" + this.lekeres.readyState + 
  "<br />HTTP Státusz: " + this.lekeres.status + 
  "<br />Fejlécek: " + this.lekeres.getAllResponseHeaders();
  }
//--------------ajax ojjektum definíció vége-----------------------

Itt a vége az objektumunk definíciójának. A fenti objektum nem csak a példában, de bármely más AJAX alkalmazásban is használható.

Lássuk akkor a két függvényünket ami munkába állítja a fenti objektumot. Ha a lapunk betöltődött a böngészőbe hívódjon meg a hirleker függvény.

window.onload = hirleker;

Ha valaki még emlékszik a html fileunkra, akkor talán rémlik neki, hogy volt egy ujhirek nevezetű div-ünk, amit azért hoztunk létre, hogy majd ebbe kerüljenek bele a szervertől érkező adatok. Akik nem emlékeznek azok tekerjenek egy kicsit feljebb, hogy lássák, hogy tényleg így van.

Az ujhirek id-jű divünk a JavaScript program számára az aDiv objektumként lesz elérhető, és rögtön használjuk az innerHTML tulajdonságot, hogy a div-ben megjelenjen a kis szerverrel való kommunikációt szimbolizáló animált gif.

function hirleker(){
  aDiv = document.getElementById('ujhirek');
  aDiv.innerHTML = loading;

Aztán létrehozzuk az ajax osztályunk azAdat nevű objektumpéldányát. A paraméterek (url,onload,onerror,method,parameterek,contentType, celDiv) sorban:

  • hirkereso.php: így hívják a fennt bemutatott szerver oldali scriptet ami felé adatokat / kérést küldünk és amitől választ / adatokat várunk.
  • celbaIr: Ez a függvény fog meghívódni ha az objektumunk sikerrel futott
  • null: szerepel az onerror paraméterként, vagyis az objektum az alapértelmezett hibakezelő függvényt fogja hibák esetén használni. Éles alkalmazás esetén ez nem túl elegáns megoldás.
  • GET: GET :)
  • null: nem küldünk paramétereket (Ha a php fileunk várna paramétereket, mondjuk mást csinálna ha úgy hívnánk meg, hogy hirkereso.php?oldal=hirkereso mint a hirkereso.php?oldal=index akkor itt a parameterek paramétert meg kellne adnunk ebben a formában: oldal=index&nap=ma)
  • null: nem határozunk meg content type-ot
  • aDiv: ide íródnak majd bele a szervertől érkező adatok
  var azAdat = new ajax.adatBetolto('hirkereso.php', celbaIr, null, 'GET', null, null, aDiv);

A setTimeout függvény hívásával pedig arról gondoskodunk, hogy 60*1000 ms-unként, azaz 1 percenként hívja meg újra a hirleker függvényt.

  var ujrahivo = setTimeout(hirleker,60000);
  }

A celbaIr függvény hívódik meg ha az AJAX visszakapta az adatokat a szervertől (onload). Az objektumhoz csatolódik így használható a this kulcsszó. Nem is csinál semmi ördőngősséget, csupán a kis loading.gif-et kicseréli a szervertől kapott adatokra, melyeket az objektumunk responseText tulajdonsága tartalmaz.

function celbaIr(){
  var aDiv = document.getElementById(this.celDiv.id);
  aDiv.innerHTML = this.lekeres.responseText;
  }

Ennyi az egész, a teljes forrásfileok letölthetők innen.

Hogyan tovább?

A fenti példánk egyszerű adatokat vár a szervertől. Ha valamivel bonyolultabb adatstruktúrára van szükségünk, akkor az adatokat a szerverről xml formátumban kell kiküldenünk, és a JavaScript függvényünknek az XML formátumot kell feldolgoznia. Ehhez némileg módosítanunk kell a programjainkat, de ez már egy másik történet lesz.

Ha AJAX-os alkalmazások építésére adjuk a fejünket mindenféleképpen érdemes telepítenünk a Firefox Firebug kiterjesztését. Ezzel nyomon követhetjük a szerverrel való kommunikációt, a JavaScript által generált html oldal forrását böngészhetjük, és számtalan más hasznos szolgáltatást vehetünk igénybe.

Ez a leírás azoknak szól akiket a „Hogy is működik?” kérdés foglalkoztat. Aki csupán használni akarja az AJAX lehetőségeit annak javaslom a prototype keretrendszert, ami saját AJAX objektummal dolgozik.

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük