Ajax-Technologie reichert dröge Webapplikationen mit dynamischen Elementen an, damit der Benutzer sie als desktopartig empfindet. Ein Perl-Skript zeigt eine einfache Implementierung eines dynamischen Textschnipselverwalters.
Abbildung 1: Der Textschnipselverwalter im Browser. |
Mit Gmail fing es fast unbemerkt an, spätestens
die Maps
-Seite traf alle Webentwickler wie
ein Paukenschlag: 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. Auf einmal war
kein lästiger Client-Server-Rundtrip und anschließendes
Nachladen der gerade dargestellten Seite mehr
notwendig, um Zustandsänderungen einer Applikation
anzuzeigen. Heutzutage schießen die Ajax-
Applikationen auf dem Web wie Pilze aus dem Boden. Der
Beta-Release der Yahoo! Webmail sieht einer Desktop-
Applikation schon verblüffend ähnlich, nur wer genau
hinsieht, merkt, dass eigentlich der gute alte
Mozilla Firefox 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 (also ohne dass die Darstellung der Seite oder die Benutzerführung leidet) mit dem Webserver Daten austauschen kann. Diese schmuggelt es dann dynamisch ins HTML der Seite ein, sodass dort nur kleine Änderungen passieren, ohne dass der Browser die Seite komplett neu aufbauen muss.
Abbildung 1 zeigt eine Beispielapplikation, die oft genutzte Textschnipsel
für Emails unter benutzerdefinierten Namen auf dem Webserver verwaltet
und zum Cut-and-Paste in einem Textfeld darstellt.
Mit Add new topic
lässt sich ein neuer Name definieren, Update
spielt dem Server den Text im Textfenster zu und Remove
löscht
den angewählten Eintrag vom Server und eliminiert ihn aus der
angezeigten Liste.
Die Darstellung wird nur ein einziges Mal in den Browser geladen.
Die unterstrichenen Links sehen zwar wie Hyperlinks aus und laden
zum Klicken ein, laden aber kein neues Dokument in den Browser.
Lediglich der jeweils im OnClick
-Handler
angegebenen JavaScript-Code wird ausgeführt.
Das Ganze wird von dem Perl-Modul CGI::Ajax
von Brent Pedersen
schön verpackt. Es definiert ein Client-Server-Protokoll (in JavaScript
und Perl), mit dem der JavaScript-Code Server-Funktionen per Funktionsnamen
und Parameterlisten anspricht.
Ein JavaScript-Objekt vom Typ XMLHttpRequest
(oder auf IE ein ActiveXObject("Microsoft.XMLHTTP")
) sorgt dafür,
dass der Browser dem CGI-Skript auf der Serverseite einen
GET-Request zuspielt, der vorher ausgehandelte Perl-Funktionen anspricht,
die wiederum Werte zurückschicken. Diese schnappt der JavaScript-Code
auf und frischt die Felder der GUI auf.
Ein mit
CGI::Ajax->new( 'display' => \&display);
im CGI-Skript definiertes Objekt vom Typ CGI::Ajax
legt fest, dass
sich im später ausgesandten HTML eine JavaScript-Sektion befindet,
die die Funktion display()
definiert. Auf der Perl-Seite auf
dem Server wird ebenfalls eine Funktion display()
definiert,
die der JavaScript-Handler später per HTTP-Request ansprechen wird.
Ein in HTML definierter Radiobutton mit dem OnClick-Handler
OnClick="display(['Faule Ausrede'], ['tarea', 'statusdiv'])"
ruft die JavaScript-Funktion display()
auf und übergibt ihr die
zwei angegebenen Arrays. Der erste Array enthält das id
-Attribut
des HTML-Elements der Schnipselüberschrift,
deren String der Server-Funktion übergeben wird.
Der zweite Array führt die id
-Attribute der HTML-Tags, die der Handler
nach Abschluss des Requests mit den Rückgabewerten
der Serverfunktion auffrischen wird.
Abbildung 2: Kommunikation zwischen Browser, JavaScript Engine und Web Server |
Im vorliegenden Fall führt der Radiobutton mit der id
``Faule Ausrede''
auch das Attribut VALUE="Faule Ausrede"
, und demensprechend erhält die
Perl-Funktion display()
auf dem Server (in Listing snip
in Zeile 10)
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 serverseitigen Handler zum
Anspringen der Perl-Funktionen bereitstellt.
Allerdings macht snip
Client-seitig mehr als nur Textfelder aufzufrischen.
Falls der Benutzer eine der Überschriften mit einem Mausklick auf
Remove
löscht, verschwindet nicht nur der Radiobutton mit der
Überschrift, sondern auch die allererste Überschrift in der verbleibenden Liste
wird selektiert und deren Textschnipsel vom Server geladen. Dergleichen
kann CGI::Ajax
noch nicht, aber mit zusätzlichen JavaScript-Funktionen
lässt sich das bewerkstelligen.
Dafür ist CGI::Ajax
aber denkbar einfach zu bedienen: Wie Listing
snip
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.
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);
Das Perlskript snip
bezieht das auszusendende HTML aus dem
Template snip.tmpl
, das in Abbildung 2 zu sehen ist. Das
Template-Toolkit lädt die Vorlage und
bietet eine Reihe von Konstrukten, mit denen
man einfach for
-Schleifen oder Bedingungen ins HTML einbetten
kann. Bewusst verzichtet es auf eine komplette Programmiersprache,
um die Vermischung von Applikationslogik und Darstellung zu verhindern.
So iteriert es mit [% FOREACH topic = topics %]
über den vorher
via snip
hereingereichten Array @topics mit den Schnipselüberschriften
und gibt für jede einen Radiobutton in einer Tabelle aus.
[% topic %]
liefert jedes Mal den Wert der Template-Variablen
topic
zurück. Das
Attribut id
jedes Radiobuttons wird jeweils auf den Textstring
der Überschrift gesetzt, der OnClick
-Handler auf die vorher
beschriebene Funktion display()
, die sowohl im clientseitigen
JavaScript- als auch im Perl-Universum auf dem Server existiert.
select_first()
selektiert den ersten Eintrag in der Liste
der Radiobuttons und fordert vom Server den Textschnipsel zu der
entsprechenden Überschrift an, um ihn im großen Textfeld anzuzeigen.
Hierzu sucht es zunächst nach dem HTML-Tag mit der ID form
(das HTML-Formular in snip.tmpl
) und analysiert dann den
Rückgabewert.
Ist die Überschriftenliste hingegen
leer, selektiert select_first()
natürlich
nichts und kommuniziert auch nicht mit dem Server.
Es ist wichtig, dass die in snip
verwendeten Hyperlinks vom
Tagtyp <A>
in ihren OnClick
-Handlern als letzte
Aktion return false
stehen haben. 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
.
Abbildung 3: HTML-Template, das snip mit dem Template-Toolkit verarbeitet |
Das Template lädt zu Anfang die JavaScript-Bibliothek snip.js
,
die einige Funktionen bereitstellt, damit die 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.
topic_remove()
löscht eine Überschrift aus der Liste
von Radiobuttons und schickt einen Request an den Server,
um das Textschnipsel dort ebenfalls 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
verwirrt und liefert dennoch einen Wert zurück. Um diesen Bug
zu umschiffen, prüft id_selected()
das Ergebnis von
id_selected_first_pass()
nochmals nach, und liefert undefined
zurück, falls Firefox beim Schummeln erwischt wurde.
Sind zwei oder mehr Einträge
in der Liste von Radiobuttons, liefert form.r.length
die Länge
der Liste. Ist nur ein Eintrag präsent, liefert form.r.length
einen undefinierten Wert. Ist die Liste leer, ist form.r
undefiniert.
Das checked
-Flag des ersten Eintrag wird dann entweder über das
einzigen Elements mit dem NAME
-Attribut r
(form.r.checked
) oder dem i-ten Element der Liste
(form.r[i].checked
) angesprochen.
Anschließend ruf select_first()
noch die Funktion display()
auf,
damit vom Server der Text zur ersten Überschrift eingeholt wird.
Das Skript dient nur der Illustration und müsste für den professionellen Gebrauch noch verfeinert werden. Mit anderen Browsern als Firefox treten Kompatibilitätsprobleme und es gibt professionelle Toolkits (und bald Microsofts ``Atlas''), die sich darum kümmern, dass auch Oma Meumes IE 4.0 eine akzeptable (wenn auch abweichende) Darstellung bekommt. Auch die Fehlerbehandlung wurde weggelassen. Es ist gar nicht so einfach, einzuprogrammieren, was passiert, wenn ein unsichtbarer HTTP-Request schief geht. Hierzu muss man in die Niederungen des JavaScript-Codes hinabsteigen und entsprechende Rückmeldungen für den Benutzer definieren. Der Teufel liegt bei Ajax wie so oft im Detail.
001 // snip.js Mike Schilli, m@perlmeister.com, 2005 002 003 // ################################################## 004 function topic_add(topic) { 005 // ################################################## 006 var itemTable = document.getElementById("topics"); 007 var newRow = document.createElement("TR"); 008 var newCol1 = document.createElement("TD"); 009 var newCol2 = document.createElement("TD"); 010 var input = document.createElement("INPUT"); 011 012 if(topic.length == 0) { 013 alert("No topic name specified."); 014 return false; 015 } 016 017 input.name = "r"; 018 input.type = "radio"; 019 input.id = topic; 020 input.value = topic; 021 input.onclick = function() { 022 display([topic], ['tarea', 'statusdiv']); 023 }; 024 input.checked = 1; 025 newCol1.appendChild(input); 026 027 var textnode = document.createTextNode(topic); 028 newCol2.appendChild(textnode); 029 030 itemTable.appendChild(newRow); 031 newRow.appendChild(newCol1); 032 newRow.appendChild(newCol2); 033 034 document.getElementById('tarea').value = ""; 035 document.getElementById('new_topic').value = ""; 036 037 return false; 038 } 039 040 // ################################################## 041 function topic_update() { 042 // ################################################## 043 if(!id_selected()) { 044 alert("Create a new topic first"); 045 return; 046 } 047 update_me( [ id_selected(), 'tarea' ], 048 'statusdiv'); 049 } 050 051 // ################################################## 052 function topic_remove() { 053 // ################################################## 054 var sel = id_selected(); 055 056 if(!sel) { alert("No topic available"); 057 return; 058 } 059 060 remove_me([sel], 'statusdiv'); 061 062 var node = document.getElementById(sel); 063 var row = node.parentNode.parentNode; 064 row.parentNode.removeChild(row); 065 select_first(); 066 } 067 068 // ################################################## 069 function select_first() { 070 // ################################################## 071 var form = document.getElementById("form"); 072 if(! form.r) { return; } 073 if(! form.r.length) { 074 form.r.checked = 1; 075 if(! document.getElementById(id_selected()) ) { 076 document.getElementById('tarea').value = ""; 077 return; 078 } 079 display([id_selected()], ['tarea', 'statusdiv']); 080 } 081 082 for(var i = 0; i < form.r.length; i++) { 083 form.r[i].checked = 1; 084 break; 085 } 086 display([id_selected()], ['tarea', 'statusdiv']); 087 } 088 089 // ################################################## 090 function id_selected() { 091 // ################################################## 092 sel = id_selected_first_pass(); 093 094 if(! document.getElementById(sel) ) { 095 document.getElementById('tarea').value = ""; 096 return; 097 } 098 return sel; 099 } 100 101 // ################################################## 102 function id_selected_first_pass() { 103 // ################################################## 104 var form = document.getElementById("form"); 105 if(! form.r) { return 0; } 106 if(! form.r.length) { return form.r.id; } 107 108 for(var i = 0; i < form.r.length; i++) { 109 if(form.r[i].checked) { 110 return form.r[i].id; 111 } 112 } 113 alert("Selected ID is unknown"); 114 return 0; 115 }
Das Skript benötigt die Module Class::Accessor
, CGI::Ajax
und
Template
vom CPAN. Danach kommen das Skript snip
(ausführbar!)
und das Template snip.tmpl
ins Verzeichnis cgi-bin
des Webservers und das
JavaScript-Skript snip.js
direkt unter die Dokumentenwurzel
(meist htdocs
).
Falls das zum Cut-and-Paste der Textschnipsel
verwendete Terminal
kein UTF-8 versteht, sollte Zeile 59 in snip
auskommentiert werden,
damit der Webserver stattdessen
charset=iso-8859-1
im CGI-Header sendet. Und
wer den Dokumenten-Cache nicht unter /tmp
anlegen will,
legt das gewünschte Verzeichnis mit
my $cache = Cache::FileCache->new({ cache_root => "/pfad"})
in Zeile 7 fest. Anschließend lädt ein auf
http://server/cgi-bin/snip
gerichteter Browser die Oberfläche und
führt, falls JavaScript aktiviert ist, die dort angebotenen
Schnipselfunktionen aus, kommuniziert mit dem Server und hält dort den
Schnipselspeicher aktuell.
Michael Schilliarbeitet 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. |