AJAX

Działanie stron opiera się o protokół HTTP (stąd początek adresu zaczyna się od http://). Strona wysyła do serwera żądanie, ten je odbiera, przetwarza, po czym wysyła odpowiedź poprzedzoną kodem odpowiedzi. Niektóre kody odpowiedzi są nam znane - np kod 404, oznaczający brak danego adresu, lub np 401 oznaczający brak autoryzacji.
W klasycznych stronach aby zawartość pobrana z serwera mogła być wyświetlona, musimy przeładować całą stronę. Korzystając z AJAX możemy przeładowywać części strony. A że robimy to zazwyczaj asynchronicznie, użytkownik w tym czasie dalej może korzystać z naszej strony.
Przykładami stron wykorzystującymi technologię AJAX są np. gmail.com, tvgry.pl, youtube.com.

Aby sensownie testować działanie skryptów z tego rozdziału, zainstaluj sobie jakiś lokalny serwer. Swego czasu napisałem artykuł jak to zrobić. Zapraszam do lektury (autoreklama mode off)

Stworzenie obiektu XMLHttpRequest

Głównym zadaniem AJAX jest otwarcie połączenia z serwerem. Aby to zrobić używamy obiektu XMLHttpRequest. Taka komunikacja z serwerem jest realizowana na 2 różne sposoby. Starsze wersje IE (<7) używają do tego celu bibliotek ActiveX, reszta przeglądarek wykorzystują do tego celu obiekt XMLHttpRequest. Na szczęście oba typy połączenia dysponują podobnymi metodami, więc wystarczy namówić IE do tego, by widział swój sposób tak samo jak inne przeglądarki :).

Aby stworzyć obiekt XMLHttpRequest, skorzystajmy z poniższego skryptu. Jeżeli dana przeglądarka nie obsługuje obiektu XMLHttpRequest, wtedy tworzymy nowy obiekt-wrapper o takiej samej nazwie. Dzięki temu w przyszłości będziemy mogli wywoływać ten obiekt tak samo dla obu typów przeglądarek:


//IE nie posiada XMLHttpRequest, dlatego stworzymy obiekt-wrapper, dzięki temu dla obu typów przeglądarek będziemy mogli używać tego samego wywołania
if (typeof XMLHttpRequest == "undefined") {
    XMLHttpRequest = function() {
        //IE wykorzystuje biblioteki ActiveX do tworzenia obiektu XMLHttpRequest
        return new ActiveXObject(
            //IE5 używa innego obektu XMLHTTP niż IE6 i wyższe
            navigator.userAgent.indexOf("MSIE 5") >=0 ? "Microsoft.XMLHTTP" : "Msxml2.XMLHTTP"
        );
    }
}

//a teraz już dla wszystkich tak samo
var xml = new XMLHttpRequest();

Od tej pory możemy mamy dostęp do metod i właściwości tego obiektu:

Metody:

  • open("metoda", "url", async, "user", "password") - otwiera połączenie do serwera. Zazwyczaj korzystamy z 3 pierwszych atrybutów - metody (GET/POST), adresu pliku na serwerze, oraz boolowskiej zmiennej określającej czy dane połączenie ma być asynchroniczne. Ze względów bezpieczeństwa, możesz otwierać połączenie tylko ze swoją domeną.
  • send("content") - wysyła żądanie do serwera
  • abort() - zatrzymuje żądanie
  • setRequestHeader() - wysyła konkretny nagłówek do serwera
  • getResponseHeader("nazwa_nagłówka") - pobiera konkretny nagłówek http
  • getAllResponseHeaders() - pobiera wszystkie wysłane nagłówki http jako string

Właściwości:

  • onreadystatechange - zdarzenie odpalane w chwili zmiany stanu danego połączenia
  • readyState - zawiera aktualny status połączenia (0: połączenie nie nawiązane, 1: połączenie nawiązane, 2: żądanie odebrane, 3: przetwarzanie, 4: dane zwrócone i gotowe do użycia)
  • responseText - zawiera zwrócone dane w formie tekstu
  • responseXML - zawiera zwrócone dane w formie drzewa XML (jeżeli zwrócone dane są prawidłowym dokumentem XML)
  • status - zwraca status połączenia np 404 - gdy strona nie istnieje, itp)
  • statusText - zwraca status połączenia w formie tekstowej - np 404 zwróci Not Found

Powyższe metody i właściwości omówimy poniżej. Przejdźmy kolejno przez kroki nawiązania połączenia i odbioru odpowiedzi.

Nawiązujemy połączenie

Napiszmy prosty skrypt realizujący połączenie typu GET.


//korzystamy z wcześniej napisanego wrappera
var xml = new XMLHttpRequest();
xml.open("GET", "/some/url.cgi?postPerDate=20101203", true);
xml.send();

Na początku tworzymy nasz obiekt połączeń. Następnie otwieramy połączenie metodą open(). Przyjmuje ona 3 argumenty. Pierwszy z nich mówi o typie połączenia. Najpopularniejszymi są GET lub POST, chociaż można zestawiać także inne typy np. HEAD (dzięki temu typowi możesz pobrać nagłówek dokumentu na serwerze i sprawdzić np datę jego modyfikacji). Drugim parametrem jest adres skryptu serwera (wraz z danymi po znaku ? w przypadku GET). Ostatni parametr typu boolaean określa typ połączenia - czy ma być asynchroniczny, czy synchroniczny. Asynchroniczny oznacza, że strona może dalej pracować, gdy takie połączenie jest realizowane.

W forma połączenia GET, dane przekazywane są po znaku "?" czyli jako query string. Dlatego właśnie wymagana jest serializacja danych. Query string ma ograniczoną długość do kilku KB (zależnie od przeglądarki), tak więc ilość danych przesyłana tą metodą jest ograniczona.

Drugą formą połączenia jest POST. Dzięki tej formie jesteśmy w stanie przesłać do serwera dowolną liczbę danych:


var xml = new XMLHttpRequest();
xml.open("POST", "/some/url.cgi", true);

// Ustawiamy nagłówek, tak by serwer wiedział jak przetwarzać przesyłane dane
xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

//wysyłamy dane na serwer
xml.send( serialize( data ) );

Za pomocą POST jesteśmy w stanie przesyłać każdy typ danych. Dlatego jako super bohaterowie musimy poinformować serwer, jakie dane chcemy do niego wysłać. Służy do tego metoda setRequestHeader(). W połączeniach GET nie musimy tego robić, gdyż wszystkie serwery domyślnie wiedzą jak sobie radzić z danymi znajdującymi się za znakiem "?".

Zauważ jaki nagłówek ustawiliśmy. urlencoded to format, w jaki zamieniliśmy nasze dane za pomocą naszej funkcji serialize. Możesz równie dobrze ustawić nagłówek text/xml i wysyłać xml, czy nawet obiekty javascript (json). Przykład wysyłania takich nie zserializowanych danych może wyglądać tak:


var xml = new XMLHttpRequest();
xml.open("POST", "/some/url.cgi", true);

// Ustawiamy nagłówek, tak by serwer wiedział jak przetwarzać przesyłane dane
xml.setRequestHeader("Content-Type", "text/xml");

//wysyłamy dane w formacie xml na serwer
xml.send( "<items><item id='one'/><item id='two'/></items>" );

Sprawdzamy stan połączenia

Po wysłaniu danych na serwer, są one przez niego przetwarzane, po czym zostają nam zwrócone. Aby odebrać odpowiedź, reagujemy na zdarzenie onreadystatechange, które zostaje odpalone wraz ze zmianą statusu połączenia. Gdy to zdarzenie zostanie odpalone, musimy sprawdzić aktualny stan połączenia za pomocą właściwości readyState, która może przyjąć następujące wartości:

  • 0: połączenie nie nawiązane,
  • 1: połączenie nawiązane,
  • 2: żądanie odebrane,
  • 3: przetwarzanie,
  • 4: dane zwrócone i gotowe do użycia.

Nas interesuje najbardziej ostatnia wartość (4).

Jeżeli readyState równa się 4, oznacza to, że zwrócone dane są gotowe do użycia. Możemy uzyskać do nich dostęp za pomocą metod responseText i responseXML.


var xml = new XMLHttpRequest();

xml.open("GET", "skrypt.php", true);
//jeżeli stan dokumentu został zmieniony
xml.onreadystatechange = function(){
    //4 = dokument został w pełni przesłany i jest gotowy do użycia
    if ( xml.readyState == 4 ) {
        // xml.responseXML zawiera zwrócony dokument xml
        // xml.responseText zawiera zwrócony tekst
        // (if no XML document was provided)    
        //czyścimy obiekt, dla zwolnienia pamięci
        xml = null;
    }
};
xml.send();

Jeżeli z serwera został zwrócony poprawny dokument XML, wtedy będzie on istniał w właściwości responseXML jako drzewo DOM, po którym możemy się przemieszczać tak samo jak po drzewie DOM naszej strony. Właściwość responseText zawiera w formacie tekstu każdą zwróconą odpowiedź.

Testujemy stan połączenia

Połączenie zostało zakończone. Musimy teraz przetestować czy nasze dane zostały załadowane poprawnie.

Aby sprawdzić połączenie, sprawdzamy kod statusu odpowiedzi. Popularnym kodem jest np kod błędu 404, który mówi nam, że dana strona nie istnieje. Jeżeli właściwość status będzie równa 404, oznaczać to będzie że plik do którego chcieliśmy się odwołać nie istnieje.


if (http_request.status == 404) {
    console.warn('strona nie istnieje')
}

Nas tak naprawdę interesują kody z przedziału 200 - 300, które oznaczają prawidłowe zwrócenie danych. Poza wspomnianymi kodami, istnieje też kod 304, który ma dla nas znaczenie, gdyż oznacza że zwrócone dane są identyczne jak te z przeglądarki cache. Nie powinniśmy tego traktować jako błąd. Niestety przeglądarka Safari w tym przypadku zwraca pusty ciąg, co jest błędne, więc musimy to naprawić.


if (
    //Każdy status z przedziału 200-300 jest ok
    ( xml.status >= 200 && xml.status < 300 ) ||
    //zwrócone dane są takie same jak w przeglądarce
    (xml.status == 304) ||
    // Safari zwraca pusty ciąg znaków jeżeli zwrócone dane nie są zmodyfikowane - to też jest ok
    (navigator.userAgent.indexOf("Safari") >= 0 && typeof xml.status == "undefined")) 
{ 
    alert(xml.responseText);
}

Próbujemy wykryć status połączenia sprawdzając jego kod statusu. Jeżeli wykrycie się nie uda, połączenie się też nie udało. Jest to bardzo istotny krok w nawiązywaniu połączenia AJAXem, gdyż musimy mieć pewność, że zwracane dane są prawidłowe, a nie są np nagłówkiem mówiącym o nie istniejącej stronie.

Zwrócone dane

Po nawiązaniu połączenia, pozytywnym sprawdzeniu odpowiedzi, wreszcie możemy przystąpić do "zabawy" na zwróconych danych. Jak wspomniałem wcześniej, zwrócone dane zawierają się równocześnie w 2 właściwościach: responseText i responseXML. Pierwsza z nich zawiera dowolne dane w formie zwykłego tekstu. Druga zawiera prawidłowy dokument XML, który jest przerobiony na drzewo DOM. Dzięki temu korzystając z metod DOM możemy po nim się poruszać tak samo jak po drzewie naszej strony.

Zbierając poznane informacje możemy napisać prosty kod połączenia:


var xml = new XMLHttpRequest();

xml.open("GET", "skrypt.php", true);
xml.onreadystatechange = function() {
    if ( xml.readyState == 4 && (r.status >= 200 && xml.status < 300 || xml.status == 304 || navigator.userAgent.indexOf("Safari") >= 0 && typeof r.status == "undefined")) {
        if (xml.responseText=="ok") {
            var ok = document.createTextNode('***OK***');
            document.getElementsByTagName('body')[0].appendChild(ok);
        }
        xml = null;
    }
};
xml.send();

Skrypt PHP, do którego odwołuje się powyższy skrypt JS nie jest zbyt skomplikowany :)


<?
    echo "ok";
?>

Pamiętać musimy, że jeżeli chcemy zwracać dokument XML, wtedy musi on być poprawnym dokumentem XML, w przeciwnym przypadku właściwość responseXML będzie zawierała null. Nie jest niestety to kurs o tworzeniu XML (może innym razem :]), podam więc przykład prostego dokumentu XML:


<xml version="1.0" ?>';
<uberGames>
  <game>
    <title>Syndicate</title>
    <note>10/10</note>
    <gameType>strategia</gameType>
 </game>
  <game>
    <title>Cannon Fodder</title>
    <note>9/10</note>
    <gameType>strategia</gameType>
 </game>
 <game>
    <title>Alien Breed</title>
    <note>10/10</note>
    <gameType>strzelanka</gameType>
 </game>
</uberGames>

Wypisując taki dokument w PHP jako odpowiedź z serwera, pamiętajmy, że musimy go poprzedzić nagłówkiem text/xml:

header('Content-Type: text/xml');

Gotowa implementacja

Implementowanie AJAXA tak jak to zostało pokazane powyżej jest bardzo niepraktyczne. Przy pojedynczym połączeniu spełni swoją rolę, co jednak gdy będziemy chcieli AJAX wykorzystać w kilku miejscach na stronie?

Napiszmy funkcję, która nam to ułatwi:


function ajax( options ) {
    options = {
        type: options.type || "POST",
        url: options.url || "",
        onComplete: options.onComplete || function(){},
        onError: options.onError || function(){},
        onSuccess: options.onSuccess || function(){},
        dataType: options.dataType || "text"
    };

    var xml = new XMLHttpRequest();
    xml.open(options.type, options.url, true);
    
    xml.onreadystatechange = function(){
        if ( xml.readyState == 4) {
            if ( httpSuccess( xml ) ) {
                var returnData = (options.dataType=="xml")? xml.responseXML : xml.responseText
                options.onSuccess( returnData );
            } else {
                options.onError();
            }
            options.onComplete();
            xml = null;
        }
    };
    
    xml.send();
    
    function httpSuccess(r) {
        try {
            return ( r.status >= 200 && r.status < 300 || r.status == 304 || navigator.userAgent.indexOf("Safari") >= 0 && typeof r.status == "undefined")
        } catch(e) {
            return false;
        }        
    }
}

W zasadzie jest to zebranie poznanych wcześniej informacji w jedną funkcję. Ciekawą techniką jest utworzenie dodatkowego obiektu z wartościami domyślnymi wywoływanej funkcji.

Przykład prostego użycia naszej funkcji wygląda tak:


ajax( {
    type: "GET",
    url: "skrypt.php",
    onError: function(msg) {
        console.warn(msg)
    },
    onSuccess: function(msg) {
        console.log(msg);
    }
});

Bardziej praktyczny przykład pobierający z serwera dokument XML i wstawiający zawartość jego węzłów do listy:


document.getElementById('fillList').addEventListener('click', function() {
    //tworzymy loading
    var loading = document.createElement('span');
        loading.appendChild(document.createTextNode('loading'));
        loading.className = 'loading';
        this.parentNode.insertBefore(loading,this);
    
    //odpalamy naszą funkcję z przykładowymi parametrami
    ajax( {
        type: "GET",
        url: "skrypt.php?start=2&iloscTytulow=5",
        dataType: "xml",        
        onError: function(msg) {
            //jak error, to wypisujemy w firebugu
            console.warn(msg)
        },
        onSuccess: function(msg) {
            //usuwamy "loading"
            loading.parentNode.removeChild(loading);
            
            //czyscimy liste - by kolejne odwołania się nie sumowały :)
               var lista = document.getElementById('lista');
            if (lista.hasChildNodes() && lista.childNodes.length) {
                while (lista.firstChild) {
                lista.removeChild(lista.firstChild)
                }
            }    
            
            //robiąc pętlę po drzewie otrzymanego dokumentu XML, wyświetlamy tytuły jako li w naszej liście
            var tytuly = msg.getElementsByTagName('tytul');
            for (i=0; i<tytuly.length; i++) {
                var tytul = tytuly[i].firstChild.nodeValue;
                var li = document.createElement('li');
                    li.appendChild(document.createTextNode(tytul));
                    lista.appendChild(li);                        
            }
        }            
    });
});

Skrypt PHP, do którego się odwołujemy przedstawiłem poniżej. W naszym skrypcie wstawiłem funkcję sleep, by zasymulować opóźnienie.


<?
//nasz skrypt jest mocno ograniczony - to tylko przykład
$titles = Array("Kubuś Puchatek","Kaczor Donald","Harry Potter","Conan","Świat Dysku","Alchemik","Cordian","Wiedźmin","DragonLance","Warhammer");
$min = (int)$_GET['start']-1;
$max = (int)$_GET['start']+(int)$_GET['iloscTytulow'];

sleep(5);

header('Content-Type: text/xml');
echo '<?xml version="1.0" ?>';
echo '<root>';    
for ($i=$min; $i<$max; $i++) {
    echo '<tytul>'.$titles[$i].'</tytul>';    
}
echo "</root>";
?>

    po otrzymaniu wyniku sprawdź w debugerze (F12 w przeglądarce), co zostało zwrócone

    Serializacja danych

    Częstokroć podczas wysyłania danych na serwer będziemy musieli je zamienić na odpowiedni format nadający się do przesłania. Taka zamiana nazywa się serializacją danych. W małych skryptach możemy takie dane wklepywać z palca (co robiliśmy powyżej np po znaku ? w adresach). Czasami jednak warto użyć do tego automatu. Napiszmy funkcję, która będzie serializowała przekazywane dane:

    
    //Funkcja może przyjąć 2 różne typy danych
    // - tablicę-zbiór inputów.
    // - Obiekt zawierający pary klucz/wartość
    function serialize(a) {        
        var s = [];
        // Jeżeli została przekazana tablica, zakładamy że zawierainputy z formy
        if ( a.constructor == Array ) {
            // serializuj inputy
            for ( var i = 0; i < a.length; i++ ) {
                s.push( a[i].name + "=" + encodeURIComponent( a[i].value ) );
            }
        // inaczej są to pary klucz/wartość
        } else {
            for ( var j in a ) {
                s.push( j + "=" + encodeURIComponent( a[j] ) );
            }
        }
        //Zawracamy wynik - serailizowana tablica połączona znakiem &
        return s.join("&");
    }
    

    Od tej pory łatwo możemy serializować nasze dane:

    
    var f = document.getElementById('formularz');
    var serializowane = serialize(f.getElementsByTagName('input'));