Viele Elemente dynamisch aktualisieren

Ein aktuelles Projekt interagiert auf sehr starke Art und Weise mit dem Benutzer. Das bedeutet neben einer anpassbaren Oberfläche wird der Benutzer ständig über den Status diverser Objekte auf dem laufenden gehalten. Dabei können sich verschiedene Objekte untereinander beeinflussen. jQuery bietet ja ein tolles Ajaxkonstrukt an mit dem es kein Problem ist Daten vom Server zu holen. Aber auf die Dauer wird es doch etwas nervig immer wieder zu tippen. Und weil “objektorientiertes Javascript” und selber entwickeln so viel Spaß macht, habe ich mir eine alternative Lösung einfallen lassen: Im Projekt werden an geeigneter Stelle Elemente erzeugt, die alle benötigten Informationen enthalten. Diese werden in einem Updater registriert, welcher sich um die regelmäßige Abfrage kümmert. Das war die Theorie und jetzt kommt die Praxis.

Ein Artikel von Paul Lunow, erschienen 2009 auf Interaktionsdesigner.de.

Zuletzt überarbeitet am von : .

Denkst Du darüber nach zu gründen? Eine Familie oder ein Startup oder beides? In der zweiten Staffel meines Podcasts spreche ich mit tollen Menschen genau darüber. Lass Dich inspirieren und abonniere meinen Podcast: Auf Apple Podcast, Spotify und auf www.gründerväter.net.

Das Element

Ein Element besteht aus einer URL auf dem Server, dem Typ der zurückerwartenden Daten und einer Funktion die aufgerufen wird, wenn die Aktion erfolg hatte. Im Quelltext sieht das so aus:

var Paul = new element('count/paul', 'json', function(j) {
    alert("Paul hat "+j.count+" Einträge.");
});

Diese Elemente können überall im Projekt erzeugt werden. Alternativen zu json sind alle Formen die jQuerys Ajaxfunktion erkennt: Html, Text und Jsonp.

Der Updater

Ganz am Anfang (oder am Ende, je nach Geschmack) wird der Updater initalisiert:

var Updater = new updater();

Der Updater akzeptiert zwei Einstellungen:

Updater.intervall = 1000; // Zeit bis zur nächsten Abfrage
Updater.baseurl = "http://kleiner.test.com/"; // String der vor jede URL geschrieben wird

Und hat eine handvoll Funktionen für den dynamischen Umgang mit dem Objekt:

Updater.start(); // Startet den Vorgang sofern Elemente vorhanden sind
Updater.stop(); // Stoppt die Aktualisierungen
Updater.isRunning(); // Prüft ob sich der Updater in Aktion befindet
Updater.log(); // Schreibt alle gespeicherten Elementen in die Konsole
Updater.register(Element); // Fügt ein neues Element hinzu
Updater.remove(Element); // Entfernt ein Element

Die Verwendung

Dem Updater werden beliebig viele Elemente, auch zur Laufzeit, übergeben:

Updater.register(Paul);
Updater.start();

Das wars! Die Funktion im übergebenen Element wird jetzt, im erfolgsfall, alle 1000 Millisekunden aufgerufen.

Die technische Seite vom Element

So sieht das Element aus:

function element(url, dataType, successFunction) {

  //einstellungen
  this.url = url;
  this.data = {};
  this.type = 'get'
  this.dataType = dataType || 'html';

  //callback um die daten zu bekommen
  this.getData = function() {
    return this.data;
  }

  //callback um an die URL zu kommen
  //erster parameter ist die im Updater gespeicherte baseurl
  this.getUrl = function(BASEURL) {
    return this.url;
  }

  //callback wenn eine antwort vorliegt
  //erster Parameter ist die Antwort vom Server, zweiter der Updater an sich
  this.success = successFunction || function(html, Updater) {
    console.log('Anfrage #'+Updater.counter);
    console.log(html);
  }

  //callback wenn ein fehler aufgetreten ist
  //erster parameter ist der Updater an sich um dieses Element daraus zu entfernen
  this.error = function(Updater) {
    //bei einem fehler entfernt sich das objekt selbstständig aus dem updater
    Updater.remove(this);
  }

}

Zwei Funktionen sind wichtig: getData() wird aufgerufen, bevor die Anfrage an den Server gesendet wird. Der Rückgabewert der Funktion wird an den Server geschickt. Damit ist es also möglich, auf Veränderungen im Frontend zu reagieren. Zum Beispiel:

Paul.getData = function() {
    return {relation: $('body').attr('rel'), project: $('#project_selector').val()};
}

Schön, oder? Natürlich kann man sich noch viel einfallen lassen. Etwa mit der URL, welche mit Hilfe der Funktion getUrl() aus dem Element geholt wird. Es soll ja Fälle geben (CakePHP), bei denen es leichter ist Daten per URL zu übergeben:

Paul.getUrl = function(BASEURL) {
    return BASEURL+this.url+'/'+$('#team .paul').attr('rel');
}

Die Funktionen success(return, Updater) und error(Updater) erklären sich von selbst und haben als Parameter immer das umfassende Element dabei. Deshalb kann sich das Objekt in einem Fehlerfall auch selbstständig entfernen. Es könnte auch das Intervall verändern, ein neues Objekt hinuzfügen, oder was auch immer!

Die technische Seite vom Updater

Als erstes der gesamte Block Quelltext:

function updater() {

  //einstellungen
  this.intervall = 1000;
  this.elements = new Array();
  this.active_element = 0;
  this.baseurl = BASEURL;

  this.running = false;
  this.counter = 0;
  this.timeout = '';

  //alle elemente anzeigen
  this.log = function() {
    console.log(this.elements);
  }

  //neues element hinzufügen
  this.register = function(element) {
    this.elements.push(element);
  };

  //ein element entfernen
  this.remove = function(element) {
    for(i in this.elements) {
      if(this.elements[i] == element) {
        //delete this.elements[i];
        this.elements.splice(i, 1);
        return true;
      }
    }
    return false;
  }

  //elemente durchgehen
  this.process = function(active_element) {
    this.active_element = active_element || 0;

    if(this.elements[this.active_element]) {
      _this = this;
      _elem = this.elements[this.active_element];

      //ajaxaufruf
      $.ajax({
        //einstellungen
        url: _elem.getUrl(_this.baseurl),
        data: _elem.getData(),
        type: _elem.type,
        dataType: _elem.dataType,

        //hat funktioniert!
        success: function(result) {
          _elem.success(result, _this);
        },

        //ein fehler
        error: function(error) {
          //das object soll entscheiden wie es weitergeht
          _elem.error(_this);
        },

        //wie auch immer soll das nächste element aufgerufen werden
        complete: function() {
          _this.next();
        }
      });     

    }
    else {
      this.active_element = 0;
      this.next();
    }
  };

  //ruft process erneut auf, verzögert um intervall mit dem activen element
  this.next = function() {
    if(this.isRunning()) {
      //nur so zum spaß
      this.counter++;

      //zeit die bis zum nächsten aufruf vergehen soll
      var _intervall = 0;

      //nächstes element suchen
      this.active_element++;

      //schon am ende angelangt?
      if(this.active_element >= this.elements.length) {
        this.active_element = 0;

        //längerer intervall bis es wieder los geht
        _intervall = this.intervall;
      }

      //erneut aufrufen
      _this = this;
      _this.timeout = window.setTimeout(function() {
        _this.process(_this.active_element);
      }, _intervall);
      return true;
    }
    else {
      return false;
    }
  }

  //startet den vorgang
  this.start = function() {
    this.active_element = -1;
    this.running = true;
    this.counter = 0;
    this.next();
  }

  //stoppt das updaten
  this.stop = function() {
    this.running = false;
    window.clearTimeout(this.timeout);
    return this.counter;
  }

  //prüfen ob der Updater läuft
  this.isRunning = function() {
    return this.running;
  }
}

Nett, oder? Was ist passiert? Die Einstellungen sollten klar sein. In der Variable timeout wird der aktuelle Timeout gespeichert. Damit kann man beim Abbrechen alle Verbindungen trennen, anstatt noch eine ausführen zu müssen. Log(), register() und remove() sind an sich uninteressant. Außer eine Erwähnung, dass der Operator delete ein Element zwar entfernt, aber danach nicht das Array neu durchnummeriert. Da liegen dann lauter “undefinied” rum. Deshalb habe ich splice() benutzt. Funktioniert super. Spannend wird es in der Funktion process(). Diese wird für jedes Element aufgerufen. Als Parameter erwartet sie den Key vom aktiven Element. Wenn ein Element vorhanden ist, wird die Ajaxabfrage ausgeführt. Innerhalb der Funktion Ajax() bekommt das this eine neue Bedeutung. Deshalb musste ich es in _this zwischenspeichern. Die Funktion ruft also, je nach umstand, success() oder error() auf und übergibt die Kontrolle an das Element. In jedemfall wird aber _this.next() durch die Funktion Ajax.complete() aufgerufen. Diese wird, unabhängig vom Status, nach jeder Ajaxanfrage ausgeführt. In der Funktion next() wird das nächste Objekt gesucht. Sofern der Updater läuft isRunning() **wird der Counter erhöht und das Intervall auf Null gesetzt. Das passiert, weil alle registrierten Elemente sofort nacheinander aufgerufen werden sollen und erst anschließend die in this.intervall angegebene Zeit gewartet wird. Wenn alle Elemente durchgearbeitet wurden, wird der **Index auf Null und der Intervall auf die eingestellte Zeit gesetzt. Anschließend gibt es ein klassisches window.setTimeout() um das ganze Spielchen wieder von vorne anzufangen. Start(), stop() und isRunning() erklären sich wieder von selbst, und damit ist die Vorstellung beendet!

Fazit

Der große nächste Erweiterungsschritt ist in dem Updater alle Anfragen zu sammeln undin einem Packet an den Server zu schicken. Die Antwort kann dann direkt vom Updater auseinander gefriemelt und an die verschiedenen Elemente verteilt werden. Dazu muss der Server allerdings noch vorbereitet werden, und bisher sind es noch nicht viele Benutzer. Aber ich denke es ist eine solide Grundlage die gut erweitert werden kann. Das war auch die Intention dieses Postings. Hast du einen Vorschlag? Kritik? Hinweis? Freue mich über Kommentare und Inspiration. Danke für die Aufmerksamkeit.


Deine Meinung

Sind wir einer Meinung? Sind noch Fragen offen geblieben?

Mehr zum Thema