AJAX
Kommunikation zwischen Client und Server soll nicht nur über die Eingabe von URLs in die Adresszeile des Browsers erfolgen, sondern auch aus dem laufenden Clientprogramm heraus initiiert werden können. Dafür wird das Konzept AJAX (Asynchronous JavaScript and XML) verwendet. Das Konzept besagt, dass von JavaScript-Programmen aus HTTP-Requests asynchron abgesetzt werden. Asynchron bedeutet hierbei, dass der Client nicht blockiert, bis der Server die Anfrage verarbeitet und die Antwort zurückgesendet hat, sondern dass ein Eventhandler registriert wird, der von der Laufzeitumgebung aufgerufen wird, sobald die Antwort des Servers vorliegt.
Entgegen der Namensbezeichnung sind wir nicht auf XML als Datenstruktur für die transportierten Daten beschränkt.
Echoserver
Als einfaches Beispiel dient ein Echoserver, der den Querystring der gesendeten Anfrage unverändert an den Client zurückschickt (Datei Web_Quellcode_FP/vorlesung/ajax/http_echo.js):
Der Echoserver lässt sich testen, indem im Browser ein URL aufgerufen wird, der einen Querystring enthält. Der Querystring ist eine Sammlung von durch & getrennten Schlüssel/Wert-Paaren. Er wird am Ende des URLs, beginnend mit einem ? angegeben:
http://localhost:3000/?key1=hallo&key2=welt
Die Antwort des oben angegebenen Beispielservers enthält den Querystring des Requests, der im Browser ausgegeben wird:
?key1=hallo&key2=welt
XMLHttpRequest
Der eigentliche Zweck von AJAX besteht nicht vorrangig darin, ganze HTML-Seiten abzurufen (das kann der Browser auch ohne AJAX), sondern einzelne Datenschnipsel aus dem laufenden Programm heraus an den Server zu senden oder von diesem abzurufen. Dazu dient das XMLHttpRequest-Objekt des Browsers.
Im Folgenden werden Einzelaspekte aus der Datei Web_Quellcode_FP/vorlesung/ajax/send_http.html diskutiert. Für korrektes Funktionieren der Beispiele muss jQuery eingebunden sein.
Zuerst wird ein XMLHttpRequest-Objekt erzeugt:
var xhttp = new XMLHttpRequest();
Bei diesem Objekt registrieren wir den Eventhandler, der darauf reagiert, dass sich der Zustand des Requests (NICHT der HTTP-Status) ändert. Der Name des Eventhandlers ist onreadystatechange. Im folgenden Beispiel wird die Antwort des Servers ( xhttp.responseText) in ein div der Seite geschrieben:
// Eventhandler: wird immer aufgerufen, sobald der ReadyState des Requests sich ändert xhttp.onreadystatechange = function() { // Wir interessieren uns nur für ReadyState 4 (Anfrage beendet und Antwort ist bereit) // und HTTP-Status OK if (xhttp.readyState === 4 && xhttp.status === 200) { // Schreibe die Antwort in das HTML-Dokument (DOM) $('#antwort'.html) = xhttp.responseText; } };
Nachdem der Eventhandler registriert wurde, wird die eigentliche Anfrage an den Server abgesetzt. Die Zufallszahl im Querystring dient nur zur visuellen Überprüfung, ob sich etwas geändert hat:
// Anfrage an den Server vorbereiten ... xhttp.open("GET", "http://localhost:3000/?key1=hallo&key2=welt&rnd=" + Math.random()); // ... und absenden xhttp.send();
Kommunikation mittels JSON
Zur Kommunikation zwischen Client und Server eignet sich JSON sehr gut, da in den meisten Programmiersprachen APIs zur Kodierung und Dekodierung von JSON enthalten sind. Beispielsweise könnte ein JavaScript-Client auch ohne besonderen Transformationsaufwand JSON-Objekte an einen PHP-Server senden und von dort empfangen.
Zwischen JavaScript-Client und -Server stellt sich die Frage nach dem Format ohnehin normalerweise nicht, da JSON direkt in Objekte zurücktransformiert werden kann. Der Echoserver kann an die Verarbeitung von JSON angepasst werden:
// URL parsen var reqdetails = url.parse(req.url, true); console.log(reqdetails); // JSON aus dem Querystring lesen var json = reqdetails.query.obj; // in Objekt umwandeln var o = JSON.parse(json); // ausgeben console.log(o); // HTTP Header schreiben // Cross-Site-Scripting erlauben! res.writeHead(200, { "content-type": "application/json", "Access-Control-Allow-Origin": "null" }); // eigentliche Ausgabe schreiben // Querystring echoen res.write(JSON.stringify(o)); // abschließen res.end();
Der Content-Type der Antwort wurde auf application/json gesetzt. Dadurch kann der Browser ggf. die erhaltenen Daten vorverarbeiten.
Im Objekt reqdetails, das durch Parsen des Objekts req.url gewonnen wurde, legt Node.js das Attribut query an. Dieses ist ein Objekt, das alle Komponenten des Querystrings als Attribute enthält. Es kann direkt mit den Attributnamen auf die Werte zugegriffen werden, wenn die Namen der Schlüssel des Querystrings bekannt sind. Im Fall oben ist das obj.
Dieser Name wird in unserem Beispiel dann clientseitig als Schlüssel im Querystring verwendet, womit die Verbindung zum Objekt reqdetails.query hergestellt ist. Als Wert wird dann entsprechend der JSON-String eingesetzt:
function sendRequest() { // Dummy-Objekt var o = { key1: "hallo", key2: "welt", rnd: Math.random() }; // Anfrage an den Server vorbereiten ... xhttp.open("GET", "http://localhost:3000/?obj=" + JSON.stringify(o)); // ... und absenden xhttp.send(); }
Als Antwort haben wir ebenfalls JSON erhalten. Dieses wird aus dem responseText geparst und dann attributweise in das div mit der ID antwort geschrieben. Hier ist zu beachten, dass das jQuery-Objekt des Div bereits vor dem Betreten der Schleife gespeichert wird, damit nicht bei jedem Schleifendurchlauf ein laufzeitintensives Query gegen das DOM ausgeführt wird:
xhttp.onreadystatechange = function() { // Wir interessieren uns nur für State 4 (Anfrage beendet und Antwort ist bereit) // und HTTP-Status OK (200) if (xhttp.readyState === 4 && xhttp.status === 200) { // Schreibe die Antwort in das HTML-Dokument (DOM) // JSON parsen var obj = JSON.parse(xhttp.responseText); // jQuery-Objekt des div var div = $('#antwort'); // Attribute des erhaltenen Objekts durchlaufen ... for(var key in obj){ // ... und ausgeben div.append(key + ": " + obj[key] + "<br>"); } } };
Weitere HTTP-Request Typen
Zur Realisierung von REST-Interfaces sind neben GET die HTTP-Verben POST, PUT und DELETE von Bedeutung. Bei der Verwendung von POST und PUT ist die Nutzlast nicht im URL sondern im Request-Body enthalten. Im Request-Body können nahezu beliebige Datenmengen transportiert werden. Die Verwendung des Request-Body erfordert Anpassungen am Code des Clients. Die Nutzdaten werden nicht mehr an den Request-URL angehängt, sondern als Parameter an die Funktion send des XMLHttpRequest-Objekts übergeben (Datei Web_Quellcode_FP/vorlesung/ajax/send_http_post.html):
function sendRequest() { // Dummy-Objekt var o = { key1: "hallo", key2: "welt", rnd: Math.random() }; // POST-Anfrage an den Server vorbereiten ... xhttp.open("POST", "http://localhost:3000/"); // ... und mit der Nutzlast im Request-Body absenden xhttp.send(JSON.stringify(o)); }
Serverseitig sind umfassendere Änderungen vorzunehmen. Ab einer bestimmten Größe (die oft nicht vorhersagbar ist), werden POST-Requests in Teilen, sogenannten Chunks, übertragen. Viele Frameworks und Clients verwenden standardmäßig Chunkgrößen von 2, 4 oder 8 KB.
Da der Server beim Warten auf Chunks nicht blockieren soll, ist es erforderlich, einen Eventhandler zu registrieren, der beim Eintreffen eines Chunks ausgeführt wird. Das Request-Objekt von Node.js hat die Funktion on zum Registrieren von Eventhandlern, welcher mit dem ersten Parameter das zu überwachende Ereignis mitgeteilt wird. Im Fall des Eintreffen eines Chunks wird das Event data als Zeichenkette angegeben. Der zweite Parameter ist der eigentliche Eventhandler. Im data-Eventhandler werden die ankommenden Chunks aneinandergekettet:
var data = ""; req.on('data', function(chunk) { data = data + chunk.toString(); });
Sobald alle Chunks eingetroffen sind, wird dies durch das Ereignis end signalisiert. Im end-Eventhandler wird dann die eigentliche Antwort generiert und an den Client zuückgeliefert. Die vollständige Implementierung des POST-Requests (Datei Web_Quellcode_FP/vorlesung/ajax/http_echo_post.js):
var data = ""; // POST muss gesondert behandelt werden, da die Daten in Chunks kommen können if (req.method === "POST") { console.log("[200] " + req.method + " to " + req.url); // Das Request-Objekt ist da, sobald die Header vollständig sind, die Daten // sind aber evtl. noch nicht da --> Eventhandler // Es kommen Daten: Eventhandler für Chunks req.on('data', function(chunk) { data = data + chunk.toString(); }); // Der Request ist beendet: Eventhandler sendet die Antwort an den Client req.on('end', function() { // empty 200 OK response for now res.writeHead(200, "OK", { "Content-Type": "application/json", "Access-Control-Allow-Origin": "null" }); // Die erhaltenen Daten zerlegen und anzeigen var o = JSON.parse(data); for(var key in o){ console.log(key + ": " + o[key]); } // zurückschicken res.write(JSON.stringify(o)); res.end(); }); } else { // Kein POST-Request // ... }
Diese Konstruktion wird leicht unübersichtlich. Daher empfiehklt es sich, Codeteile in eigene Funktionen (z. B. handleGet, handlePostusw.) auszulagern.
HTTP-Requests mit jQuery absetzen
Neben der Manipulation des DOM gestattet jQuery einen komfortablen Umgang mit dem XMLHttpRequest-Objekt. Insbesondere kapselt es das Attribut onreadystatechange und erlaubt die Definition von vereinfachten Eventhandlern für verschiedene Requesttypen (GET, POST, Weitere).
Durch die Verwendung von jQuery zum clientseitigen Absetzen des POST-Requests verienfacht sich die Implementierung des Clients deutlich (Datei Web_Quellcode_FP/vorlesung/ajax/send_http_post_jquery.html). jQuery profitiert dabei von der serverseitigen Einstellung des Content-Type auf application/json. Dadurch wird die Antwort direkt geparst und als Objekt im Parameter result des Eventhandlers abgelegt: