Aus Linux-Magazin 12/2005

Mehr Dynamik für Webseiten dank Ajax

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 1: So assistiert der Textschnipselverwalter im Browser dem Benutzer.

Abbildung 1: So assistiert der Textschnipselverwalter im Browser dem Benutzer.

Abbildung 2: Dank der Kommunikation zwischen Browser, Javascript-Engine und Webserver wird die Webseite portionsweise dynamisch aktualisiert.

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:
»snip«

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:
»snip.js«

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.

Abbildung 3: Ein Template, das »snip« mit dem Template-Toolkit verarbeitet.

Abbildung 3: Ein Template, das »snip« mit dem Template-Toolkit verarbeitet.

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]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben