Die Ajax-Technologie reichert dröge Webapplikationen mit dynamischen Elementen an. Das gelingt bereits so gut, dass Benutzer sie auf dem Desktop kaum noch von anderen Programmen unterscheiden. Ein kleines Perl-Skript demonstriert die Technik.
Spätestens die Google-Maps-Seite wirkte wie ein Paukenschlag auf alle Webentwickler: Auf einmal war es möglich, den dargestellten Ausschnitt einer Landkarte dynamisch zu verschieben, ganz so, als liefe da im Browser keine Webapplikation, sondern ein lokales GUI. Plötzlich war kein lästiger Client-Server-Roundtrip inklusive Nachladen der gerade dargestellten Seite mehr notwendig, um Zustandsänderungen einer Applikation anzuzeigen.
Nun schießen die Ajax-Applikationen im Web wie Pilze aus dem Boden. Die Beta-Release von Yahoo-Webmail beispielsweise sieht einer Desktop-Applikation schon verblüffend ähnlich, nur wer genau hinsieht, merkt, dass eigentlich der gute alte Webbrowser läuft.
Ajax (Asynchronous Javascript and XML) baut auf Dynamic HTML und Client-seitigem Javascript auf. Das ursprünglich von Microsoft erfundene und lange unter dem Radar geflogene Objekt »XMLHttpRequest« macht es möglich, dass ein von einer Webseite heruntergeladenes Javascript-Skript asynchron mit dem Webserver Daten austauscht. Die schmuggelt es dann dynamisch ins HTML der Seite ein, sodass dort nur kleine Änderungen erfolgen und der Browser die Seite nicht komplett neu aufbauen muss.
Schnipselwerk
Abbildung 1 zeigt eine Beispielapplikation, die oft genutzte Textschnipsel für E-Mails auf dem Webserver verwaltet und zum Cut&Paste in einem Textfeld anbietet. Mit »Add new topic« lässt sich ein neuer Textbaustein eingeben, »Update« spielt dem Server den passenden Text zu und »Remove« löscht den angewählten Eintrag vom Server.
Die Darstellung lädt sich nur ein einziges Mal in den Browser. Die unterstrichenen Links sehen zwar wie Hyperlinks aus und laden zum Klicken ein, holen aber kein neues Dokument in den Browser. Lediglich der jeweils im »OnClick«-Handler angegebene Javascript-Code wird ausgeführt.
Das Perl-Modul CGI::Ajax von Brent Pedersen macht es verhältnismäßig einfach, diesen Mechanismus zu implementieren. Es definiert ein Client-Server-Protokoll (in Javascript und Perl), mit dem der Javascript-Code des Clients auf dem Server Funktionen über deren Namen und Parameterlisten anspricht.
Ein Javascript-Objekt vom Typ »XMLHttpRequest« (»ActiveXObject(“Microsoft.XMLHTTP”)« im IE) sorgt dafür, dass der Browser dem CGI-Skript auf der Server-Seite einen GET-Request zuspielt. Dieser Request aktiviert eine vorher ausgehandelte Perl-Funktion, die wiederum Werte zurückschickt. Die schnappt sich der Javascript-Code und frischt die Felder des Browser-GUI auf.
Faule Ausrede
Ein mit »CGI::Ajax->new(\’display\’ => $amp;display);« im CGI-Skript definiertes Objekt vom Typ »CGI::Ajax« bewirkt, dass sich im später ausgesandten HTML eine Javascript-Sektion findet, die die Funktion »display()« definiert. Auf dem Server ist ebenfalls eine Perl-Funktion »display()« definiert, die der Javascript-Handler später per HTTP-Request ansprechen wird.
Ein in HTML definierter Radiobutton mit dem On-Click-Handler »OnClick=”display([\’Faule Ausrede\’],[\’tarea\’, \’statusdiv\’])”« ruft die Javascript-Funktion »display()« auf und übergibt ihr die zwei angegebenen Arrays. Das erste enthält das »id«-Attribut des HTML-Elements Schnipselüberschrift, dessen Text die Serverfunktion übergeben bekommt. Das zweite Array speichert die »id«-Attribute der HTML-Tags, die der Handler nach Abschluss des Requests mit den Rückgabewerten der Serverfunktion auffrischen wird.

Abbildung 2: Dank der Kommunikation zwischen Browser, Javascript-Engine und Webserver wird die Webseite portionsweise dynamisch aktualisiert.
Im vorliegenden Fall führt der Radiobutton mit der »id« »Faule Ausrede« auch das Attribut »VALUE=”Faule Ausrede”«, entsprechend erhält die Perl-Funktion »display()« auf dem Server (Zeile 11 in Listing 1) diesen Textstring als ersten Parameter überreicht. »display()« macht nichts weiter, als das zu »Faule Ausrede« passende Textschnipsel aus dem Cache zu holen und zusammen mit einer Statusmeldung zurück an den Browser zu senden. Dort springt der Javascript-Eventhandler wieder ein und frischt das große Textfeld (»id=\’tarea\’«) und das Statusfeld ganz unten (»id=\’statusdiv\’«) mit den von »display()« zurückgegebenen Strings auf. All dies wird sauber von »CGI::Ajax« abstrahiert, das den dazu notwendigen Javascript-Code zum Browser sendet und die Server-seitigen Handler zum Anspringen der Perl-Funktionen bereitstellt.
Allerdings macht »snip« Client-seitig mehr als nur Textfelder auffrischen. Falls der Benutzer eine der Überschriften mit einem Mausklick auf »Remove« löscht, verschwindet nicht nur der Radiobutton mit der Überschrift, sondern das Skript selektiert auch die erste Überschrift in der verbleibenden Liste und lädt deren Textschnipsel vom Server. Solche Aufgaben beherrscht CGI::Ajax noch nicht, aber mit zusätzlichen Javascript-Funktionen lässt sich das bewerkstelligen.
Einfache Bedienung
Dafür ist CGI::Ajax aber denkbar einfach zu bedienen: Wie Listing 1 zeigt, sind lediglich Funktionen für die verschiedenen Client-Aktionen (Remove,Update,Display) zu definieren und eine Funktion »show_html« bereitzustellen, die beim ersten Laden das HTML der Client-Applikation zurückliefert.
|
Listing 1: |
|---|
01 #!/usr/bin/perl -w
02 use strict;
03 use CGI;
04 use CGI::Ajax;
05 use Cache::FileCache;
06 use Template;
07
08 my $cache = Cache::FileCache->new();
09
10 ###########################################
11 sub display {
12 ###########################################
13 my($topic) = @_;
14
15 return $cache->get($topic),
16 "Retrieved $topic";
17 }
18
19 ###########################################
20 sub remove_me {
21 ###########################################
22 my($topic) = @_;
23
24 $cache->remove($topic);
25 return "Deleted $topic";
26 }
27
28 ###########################################
29 sub update_me {
30 ###########################################
31 my($topic, $text) = @_;
32
33 $cache->set($topic, $text);
34
35 my $disptext = $text;
36 $disptext = substr($text, 0, 60) .
37 "..." if length $text > 60;
38 return "Topic '$topic' updated " .
39 "with '$disptext'";
40 }
41
42 ###########################################
43 sub show_html {
44 ###########################################
45 my $template = Template->new();
46
47 my @keys = sort $cache->get_keys();
48
49 $template->process("snip.tmpl",
50 { topics => @keys },
51 my $result) or die $template->error();
52
53 return $result;
54 }
55
56 ###########################################
57 # main
58 ###########################################
59 my $cgi = CGI->new();
60 $cgi->charset("utf-8");
61
62 my $pjx = CGI::Ajax->new(
63 'display' => &display,
64 'update_me' => &update_me,
65 'remove_me' => &remove_me
66 );
67 print $pjx->build_html($cgi, &show_html);
|
HTML und Perl getrennt
Das Perlskript »snip« bezieht das auszusendende HTML aus dem Template »snip.tmpl« in Abbildung 3. Das Template-Toolkit lädt die Vorlage und bietet Konstrukte, mit denen man einfach »for«-Schleifen oder Bedingungen ins HTML einbetten kann. Es will bewusst keine komplette Programmiersprache sein, um zu verhindern, dass sich Applikationslogik und Darstellung vermischen. So iteriert es beispielsweise mit »[% FOREACH topic = topics %]« über das vorher via »snip« hereingereichten Array »@topics« mit den Schnipselüberschriften und gibt für jede einen Radiobutton in einer Tabelle aus. »[% topic %]« liefert immer den Wert der Template-Variablen »topic« zurück.
Das Attribut »id« jedes Radiobuttons wird auf den Textstring der Überschrift gesetzt und der »OnClick«-Handler auf die vorher beschriebene Funktion »display()«, die sowohl im Client-seitigen Javascript- als auch im Perl-Universum auf dem Server existiert.
Die Javascript-Funktion »select_first()« in Listing 2 selektiert den ersten Eintrag in der Liste der Radiobuttons und fordert vom Server das Textschnipsel zu der entsprechenden Überschrift an, um es im großen Textfeld anzuzeigen. Hierzu sucht die Funktion mit »getElementById« nach dem HTML-Tag mit der ID »form«, also dem HTML-Formular in »snip.tmpl«, und analysiert dann den Rückgabewert. Ist die Überschriftenliste allerdings leer, selektiert »select_first()« natürlich nichts und kommuniziert auch nicht mit dem Server.
Das Template lädt anfangs die Javascript-Bibliothek »snip.js«, die einige Funktionen bereitstellt, damit das GUI rund läuft. Die Javascript-Funktion »topic_add()« in »snip.js« nimmt den String einer Überschrift entgegen und fügt am Ende der Radiobutton-Tabelle einen neuen Eintrag mit diesem Namen ein. Hierzu sucht es zunächst die Radiobutton-Tabelle, erzeugt zwei neue Tabellenspalten, eine neue Tabellenreihe, einen neuen Radiobutton und hängt diese Einträge dann in die Tabelle ein.
|
Listing 2: |
|---|
001 // ########################################
002 function topic_add(topic) {
003 // ########################################
004 var itemTable = document.getElementById("topics");
005 var newRow = document.createElement("TR");
006 var newCol1 = document.createElement("TD");
007 var newCol2 = document.createElement("TD");
008 var input = document.createElement("INPUT");
009
010 if(topic.length == 0) {
011 alert("No topic name specified.");
012 return false;
013 }
014
015 input.name = "r";
016 input.type = "radio";
017 input.id = topic;
018 input.value = topic;
019 input.onclick = function() {
020 display([topic], ['tarea', 'statusdiv']);
021 };
022 input.checked = 1;
023 newCol1.appendChild(input);
024
025 var textnode = document.createTextNode(topic);
026 newCol2.appendChild(textnode);
027
028 itemTable.appendChild(newRow);
029 newRow.appendChild(newCol1);
030 newRow.appendChild(newCol2);
031
032 document.getElementById('tarea').value = "";
033 document.getElementById('new_topic').value = "";
034
035 return false;
036 }
037
038 // ########################################
039 function topic_update() {
040 // ########################################
041 if(!id_selected()) {
042 alert("Create a new topic first");
043 return;
044 }
045 update_me( [ id_selected(), 'tarea' ],
046 'statusdiv');
047 }
048
049 // ########################################
050 function topic_remove() {
051 // ########################################
052 var sel = id_selected();
053
054 if(!sel) { alert("No topic available");
055 return;
056 }
057
058 remove_me([sel], 'statusdiv');
059
060 var node = document.getElementById(sel);
061 var row = node.parentNode.parentNode;
062 row.parentNode.removeChild(row);
063 select_first();
064 }
065
066 // ########################################
067 function select_first() {
068 // ########################################
069 var form = document.getElementById("form");
070 if(! form.r) { return; }
071 if(! form.r.length) {
072 form.r.checked = 1;
073 if(! document.getElementById(id_selected()) ) {
074 document.getElementById('tarea').value = "";
075 return;
076 }
077 display([id_selected()], ['tarea', 'statusdiv']);
078 }
079
080 for(var i = 0; i < form.r.length; i++) {
081 form.r[i].checked = 1;
082 break;
083 }
084 display([id_selected()], ['tarea', 'statusdiv']);
085 }
086
087 // ########################################
088 function id_selected() {
089 // ########################################
090 sel = id_selected_first_pass();
091
092 if(! document.getElementById(sel) ) {
093 document.getElementById('tarea').value = "";
094 return;
095 }
096 return sel;
097 }
098
099 // ########################################
100 function id_selected_first_pass() {
101 // ########################################
102 var form = document.getElementById("form");
103 if(! form.r) { return 0; }
104 if(! form.r.length) { return form.r.id; }
105
106 for(var i = 0; i < form.r.length; i++) {
107 if(form.r[i].checked) {
108 return form.r[i].id;
109 }
110 }
111 alert("Selected ID is unknown");
112 return 0;
113 }
|
Halbtote Links
Es ist wichtig, dass die in »snip« verwendeten Hyperlinks vom Tag-Typ »<A>« in ihren On-Click-Handlern als letzte Aktion »return false« vorsehen. So führt der Browser bei einem Klick auf den Link lediglich den zugeordneten Javascript-Code aus und folgt nicht dem angegebenen Bogus-Attribut »HREF«.
»topic_remove()« löscht eine Überschrift aus der Liste der Radiobuttons und schickt einen Request an den Server, um das Textschnipsel auch dort zu löschen. »id_selected()« gibt das »id«-Attribut des selektierten Radiobuttons aus, also den String der Überschrift, die der Benutzer zu sehen wünscht. Ist die Liste jedoch leer, zeigt sich Firefox manchmal etwas verwirrt und liefert dennoch einen Wert zurück. Um diesen Bug zu umgehen, prüft »id_selected()« (Zeile 88) das Ergebnis von »id_selected_first_pass()« nochmals nach und liefert »undefined« zurück, falls Firefox beim Schummeln erwischt wurde.
Enthält die Liste der Radiobuttons zwei oder mehr Einträge, zeigt »form.r.length« die Länge der Liste an. Ist nur ein Eintrag vorhanden, liefert »form.r.length« einen undefinierten Wert. Ist die Liste leer, ist »form.r« undefiniert. Das »checked«-Flag des ersten Eintrags spricht dann entweder über das einzige Element mit dem »NAME«-Attribut »r« (»form.r.checked«) oder das i-te Element der Liste (»form.r[i].checked«) an. Anschließend ruft »select_first()« noch die Funktion »display()« auf, die den Text passend zur ersten Überschrift einholt.
Mit Haken und Ösen
Das hier vorgestellte Skript dient nur der Illustration, es ließe sich für professionelle Zwecke noch weiter verfeinern. Mit anderen Browsern als Firefox treten allerdings Kompatibilitätsprobleme auf. Es gibt aber bereits professionelle Toolkits (und bald auch Microsofts »Atlas«), die sich darum kümmern, dass auch Oma Meumes Internet Explorer 4.0 eine akzeptable (wenn auch abweichende) Darstellung erreicht.
Auf die Fehlerbehandlung hat der Autor des Skripts bewusst verzichtet. Denn es ist gar nicht so einfach, in Programmform zu bringen, was passieren soll, wenn ein unsichtbarer HTTP-Request schief geht. Hierzu muss der Programmierer tief in die Niederungen des Javascript-Code hinabsteigen und dort entsprechende Rückmeldungen für den Benutzer hinterlegen. Der Teufel liegt eben wie so oft auch bei Ajax im Detail.
Installation
Das Skript benötigt die Module Class::Accessor, CGI::Ajax und Template vom CPAN. Danach kopiert der Anwender das Skript »snip« (ausführbar) und das Template »snip.tmpl« in das Verzeichnis »cgi-bin« und das Javascript-Skript »snip.js« direkt ins Dokumenten-Wurzelverzeichnis des Webservers (meist ist das »htdocs«).
Falls das zum Cut&Paste der Textschnipsel verwendete Terminal kein UTF-8 versteht, gehört die Zeile 60 in »snip« auskommentiert, damit der Webserver stattdessen »charset=iso-8859-1« im CGI-Header sendet. Wer zudem den Dokumenten-Cache nicht unter »/tmp« anlegen will, bestimmt das gewünschte Verzeichnis mit »my $cache = Cache::FileCache->new({cache_root => “/pfad”})« in Zeile 8.
Anschließend lädt ein auf »http://server/cgi-bin/snip« gerichteter Browser die Oberfläche und führt, falls Javascript aktiviert ist, die angebotenen Schnipselfunktionen aus. Dabei kommuniziert er mit dem Server und hält dort den Schnipselspeicher aktuell. (jcb)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel:[ftp://www.linux-magazin.de/pub/listings/magazin/2005/12/Perl] |
|
Der Autor |
|---|
|
Michael Schilli arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen. Seine Homepage: [http://perlmeister.com] |









