Der Interaktionsdesigner – PHP, jQuery und CSS

20. Februar 2009 (20:26 Uhr)

jQuery UI Progressbar und PHP verbinden

Der neuste Release Candidate vom jQuery Userinterface 1.6 ist genial! Vor allem das neue Element, die Progressbar, hat es mir angetan. Die Bedienung ist sehr einfach, einem Element wird die Progressbar zugewiesen und die Bar dehnt sich in den gegebenen Proportionen automatisch aus.

Herrlich und sehr einfach mit einer Schleife in JavaScript zu verändern. Aber wie um alles in der Welt kriegt man die Bar dazu den aktuellen Status einer langwierigen PHP Operation wider zu spiegeln?

Ich habe eine ganze Weile herumprobiert und das folgende ist dabei heraus gekommen. Hier ist die Demo und im Beitrag folgt die Beschreibung.

Vorüberlegungen

Was man in dieser Situation braucht ist eine Push-Funktion. Die Webseite soll eine Verbindung zu einem serverseitigen Script aufbauen und halten. Wenn sich bei dem Script etwas tut, dann kriegt die Seite bescheid und kann darauf reagieren. Opera unterstützt diese Funktion bereits, und sagt dazu Event Streaming to Web Browsers, aber leider zum Glück entwickeln wir Applikationen für alle Browser.

Es gibt eine PHP Klasse, die Progressbar von David Bongard, welche eine eigene Progressbar auf die Seite bringt. Die ist wirklich gut und funktioniert zuverlässig, aber hat zwei entscheidene Nachteile: Erstens sitzt der User immer vor einer unfertigen Seite (bis die Progressbar fertig geladen ist) und zweitens kann man damit nicht ohne weiteres die Progressbar des UI benutzen.

Erster Gedanke

Ich gucke mir die Funktionen von Davids Progressbar ab und greife per Ajax auf die dynamisch generierte Datei zu. In der steht dann, je nach Fortschritt, immer etwas anderes drin.
Nun wird die Ausgabe aber über flush() erzwungen, welches zwar eine Ausgabe erzeugt aber nicht dem Browser sagt, dass die Seite fertig geladen wurde.  Diese fehlende Information bringt jQuery dazu solange zu warten, bis alle Schritte ausgeführt wurden, also die Datei vollständig geladen ist und dann erst den Callback auszuführen. Funktioniert nicht, bzw. springt von Null auf Hundert.

Zweiter Gedanke

Im PHP Script muss eine neue Datei erzeugt werden, welche nur den aktuellen Fortschritt als Inhalt hat. Diese kann per jQuery abgefragt werden und das Ergebnis auf der Progressbar dargestellt werden.
Los gehts!

Das PHP Script

Das PHP Script sieht natürlich bei jedem anders aus. In der Demo ist es einfach nur eine Schleife mit einer Sleep-Anweisung.

  ob_start();
  $filename = 'remote-bar-'.$_GET['fn'].'.temp';
  for($i = 1; $i <= 10; $i++) {
  [tab]echo $i.'0<br>';
  [tab]ob_flush(); flush();
  [tab]$fh = fopen($filename, 'w');
  [tab]fwrite($fh, $i."0");
  [tab]fclose($fh);
  [tab]sleep(1);
  }
  
  unlink('remote-bar-'.$_GET['fn'].'.temp');
Ich habe den Ausgabechache benutzt und eine Ausgabe erzeugt, was eigentlich sinnlos ist, aber zum Testen ist es praktisch. Die wesentlichen Funktionen sind die Filehandler: es wird eine Datei geöffnet, der Inhalt mit dem aktuellen Schritt überschrieben und wieder geschlossen. Anschließend wartet das Script für eine Sekunde und führt das Ganze dann wieder von vorne aus.

Der Parameter fn wird gleich per JavaScript gesetzt, damit nicht alle Benutzer in die gleiche Datei schreiben.

jQuery

Kurz angemerkt sei, dass im HTML ein Container mit der ID bar vorhanden sein muss, logisch oder? Der Rest ist mehr oder weniger dem aktuellen Layout geschuldet.

var res = 0;
var fn = Math.random(0, 999)+"";
fn = fn.substr(5, 10);
Es geht los mit der Generierung einer zufälligen Zahl für den Dateinamen. Weiter gehts mit dem Script für das Laden der Seite.
jQuery(function($) {
  $("#bar").progressbar({value: 0});
  $.ajax({
  [tab]url: "remote.php",
  [tab]type: "get",
  [tab]data: {fn: fn},
  [tab]success: function() {
  [tab]  $("#watcher").html("Fertig!");
  [tab]}
  });
  get_remote();
});
Wenn das DOM geladen und der Browser bereit ist jQuery(function($) { dann wird die Progressbar mit einem Startwert von 0 initalisiert $("#bar").progressbar({value: 0});. Anschließend (oder in einer "richtigen" Anwendung erst nach einem ausgelösten Eventhandler) wird das PHP Script aufgerufen. Das startet die Schleife, erstellt eine neue Datei und ist erst fertig,wenn der komplette Vorgang ausgeführt wurde.
Anschließend wird die Funktion get_remote() aufgerufen, welche den aktuellen Status prüft.
function get_remote() {
  $.ajax({
  [tab]url: "remote-bar-"+fn+".temp",
  [tab]dataType: "text",
  [tab]ifModified: true,
  [tab]success: function(response) {
  [tab]  $("#bar").progressbar('option', 'value', response);
  [tab]  $("#watcher").html(response);
  [tab]  if(response < 100)
  [tab]  [tab]setTimeout("get_remote()", 100);
  [tab]}
  });
}
Diese Funktion besteht aus einer einzigen Ajaxabfrage. Die Datei remote-bar-"+fn+".temp wird aufgerufen, erwartet wird ein Text als antwort dataType: "text", aber nur wenn sich der Inhalt verändert hat ifModified: true.
Wenn ein Rückgabewert besteht success: function(response) wird die folgende Funktion ausgeführt (in der Variable response steht der aktuelle Fortschritt): Der Progressbar wird der ausgelesene Wert zugewiesen, in das Element mit der ID watcher wird der Wert eingefügt und wenn es noch keine 100 (Prozent) erreicht hat, dann wird in 100 Millisekunden erneut die Funktion aufgerufen.

Das wars.

Fazit

In der Demo kann man sich das Resultat ansehen und ich bin gespannt ob der Server sofort zusammen bricht unter der Last eurer fleißigen und interessierten Tests. Außerdem bin ich mir unsicher, ob das ein sinnvolles Vorgehen war, denn wirklich effektiv scheint es mir nicht zu sein.

Aber es funktioniert und ist bestimmt für eine Applikation die nicht von mehreren Benutzern gleichzeitig aufgerufen werden kann (dazu demnächst mehr!). Wenn jemand einen Verbesserungsvorschlag hat, dann bin ich offen und dankbar dafür!

Bis dahin frohes ausprobieren und voranschreiten!

ProfessorWeb 6. März 2009 (22:58 Uhr)

Hi, bin zufällig auf diesen Artikel gestoßen.
Dein Lösungsansatz ist gut. Zumindest um eine solche Progressbar umzusetzen.

Ich persönlich denke jedoch, dass eine Fortschrittsanzeige unsinnig ist. So lange eine Verbindung nur vom Client aus aufgebaut werden kann, lohnt sich der Aufwand nicht für das was man herausbekommt.

Sinnvoll wird soetwas, wenn der Server Nachrichten an den Browser schicken kann und zwar von sich aus, nicht erst dann wenn man vorher eine Abfrage gemacht hat.

Deine Demonstration generiert theoretisch 100 HTTP-Requests (alle 100ms einen, und das 10 Sekunden lang), praktisch vielleicht die Hälfte, da der Counter von 100ms erst gestartet wird, wenn ein Ergebnis zurückgegeben wurde. Und das kann auch länger als 100ms dauern.

In deinem Artikel hast Du ja schon geschrieben, dass sich diese Lösung nicht für viele, simultane Zugriffe eignet. Und Grund dafür ist die Flut an Zugriffen an den Server. Mal abgesehen von dem Traffic der permanent erzeugt wird, sowohl auf Server- als auch auf Client-Seite.

Wenn man sich diese Nachteile ansieht, sollte man sich ernsthaft die Frage stellen: Brauche ich eine Progressbar? Sind meine Berechnungen auf dem Server so aufwändig, dass der Nutzer denken könnte der PC hätte sich aufgehangen, wenn er keinen konkreten Fortschritt sieht?

Ich denke ein guter und bewährter Mittelweg sind die Activity Indicators: Animierte GIF-Dateien die dem Benutzer suggerieren es würde etwas geschehen. Der Traffic liegt je nach Bild bei ca. 10-20kB und es ist nur ein HTTP-Request notwendig um dieses eine Bild herunter zu laden.

Super Beitrag! :)
Technisch einwandfrei, jedoch meiner Meinung nach mit Kanonen auf Spatzen geschossen. Der Aufwand und Verbrauch an Ressourcen rechtfertigt nicht den Nutzen.

Gruß
Armin
ProfessorWeb.de

Paul 7. März 2009 (13:11 Uhr)

Hallo Armin,

vielen Dank für deine ausführliche Antwort! Für eine “normale” Webseite eignet sich diese Technik wirklich nicht. Ich habe es in zwei Projekten eingesetzt bei denen zum einen eine erhebliche Anzahl Mails verschickt werden, zum anderen viele und langwierige Datenbankoperationen ausgeführt werden (beides nur nach Anmeldung). Beides Vorgänge hatte ich mit einem Ajax Indikator visualisiert, aber nach 10 Sekunden wurden die Redakteure unruhig und einige klickten sogar auf Neuladen – trotz riesigem Warnhinweis – naja. Und so habe ich mich dann für die Progressbar entschieden.

Die vorgestellte Lösung ist allerdings nicht besonders “smooze”. Es kommt vor das die ersten Schritte ewig brauchen und dann mit einem Schlag die Bar voll ist.
Ich baue es deshalb gerade um: Ein Aufruf der Funktion get_remote() animiert die Schritte zwischen dem aktuellen Status und dem neuen, vom Server erhaltenen. Anschließend wird eine neue Anfrage gesendet. Das reduziert die Serverlast erheblich.
Außerdem sollte das ausführende Script die remote-bar-24124.temp Datei nicht löschen, sonst läuft die letzte Anfrage von get_remote() ins Leere und erzeugt einen Fehler.

Es sieht also ganz so aus als gäbe es bald einen zweiten Teil und ich würde mich freuen auch dazu deine Meinung zu hören! Dann macht das bloggen gleich doppelt Spaß!

Vielen Dank und schönes Wochenende,

Paul

Gunah 9. Oktober 2009 (09:24 Uhr)

Moin

Sehr gute Anleitung, allerdings habe ich schon 10 Hoster auf meiner Liste stehen, wo kein flush untersützt wird :-/

Gabriel 3. Dezember 2009 (00:25 Uhr)

Vielen Dank für die Inspiration! Ich habe etwas ähnliches implementiert. Als Tipp will ich allen geneigten Lesern noch folgendes mitgeben: Achtet darauf, dass das lang laufende Skript keine Ressources blockiert, die das Status-Skript benötigt! Schreiboperationen in Sqlite, Datei-Handles und, ganz gemein: Sessions! Sessions sollten im lang laufenden Skript mit session_write_close() geschlossen werden.

Paul 4. Dezember 2009 (10:45 Uhr)

Hi Gabriel, das ist ein wichtiger Hinweis. Vielen Dank! Ich setzte diese Lösung meistens bei der Backend-Entwicklung ein, da ist es dann meistens nur ein Benutzer der langwierige Operationen ausführt.

Marko 19. Februar 2010 (14:22 Uhr)

Warum gehst du denn den Umweg mit einer temporären Datei?

Paul 23. Februar 2010 (22:11 Uhr)

Hi Marko, das PHP Script vollführt ja eine langwierige Operation und will an verschiedenen Stellen einen Hinweis an den Benutzer ausgeben.
Diesen Speichere ich in einer temporären Datei damit ich die per jQuery abfragen und ausgeben kann. Das arbeitende PHP Script kann ich ja nicht alle halbe Sekunde mit jQuery aufrufen, denn sonst würde es die Arbeit wieder von vorn anfangen oder einen Fehler zurück geben.

Hast du einen weiteren Vorschlag?

Einen Kommentar schreiben

(wird nicht veröffentlicht)

(wird veröffentlicht!)