Działanie stron opiera się o protokuł HTTP (stąd początek adtresu 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 lub strona Fantomasa doman.art.pl :].
Aby sensownie testować działanie skryptów z tego rozdziału, zainstaluj sobie jakiś lokalny serwer - np VertrigoServ.
Instalacja tego serwera jest identyczna jak instalacje innych programów, więc nie będę jej tutaj opisywał. Po zainstalowaniu i włączeniu serwera, obok zegara pojawi się ikonka z plusem. Klikasz na nią prawym przyciskiem myszy, po czym zerkasz na 2 najbardziej interesujące cię opcje: strona lokalna i folder www. Resztę testujesz sam :)
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:
Właściwości:
Powyższe metody i właściwości omówimy poniżej. Przejdźmy kolejno przez kroki nawiązania połączenia i odbioru odpowiedzi.
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?wpisyNadzien=28102010", true);
xml.send();
Na początku tworzymy nasz obiekt połączeń. Natępnie otwieramy połącznie 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ć. Jak widzimy powyżej, 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ć pzresył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>" );
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:
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)
//czyscimy 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ź.
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);
}
Żadna magia. 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 isntniejącej stronie.
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łącznia:
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" ?>';
<perelki>
<gra>
<tytul>Syndicate</tytul>
<ocena>10/10</ocena>
<typ>strategia</typ>
</gra>
<gra>
<tytul>Cannon Fodder</tytul>
<ocena>9/10</ocena>
<typ>strategia</typ>
</gra>
<gra>
<tytul>Alien Breed</tytul>
<ocena>10/10</ocena>
<typ>strzelanka</typ>
</gra>
</perelki>
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');
Implementowanie AJAXA tak jak to zostało pokazane powyżej jest bardzo niepraktyczne. Przy pojedyńczym 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 funckji.
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 serweru dokument XML i wstawiający zawartość jego węzłów do listy:
document.getElementById('fill_list').onclick = 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ć opuź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>";
?>
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'));