Schlagwörter

, ,

Die gängigen Formate zum Austausch von Daten per Ajax sind JSON und XML. Ein gängiges daraus resultierendes Problem entsteht wenn man Seiten-Elemente zum einen dynamisch nachladen, aber auch schon beim initialen Ausliefern der Seite ausliefern möchte.

Das Formatieren der Daten beim initialen Ausliefern übernimmt die JSP, dort gibt es dann ein Konstrukt, das etwa wie folgt aussieht:

<table>
<tr><th>Kunde</th><th>Guthaben</th></tr>
<c:forEach items="table" var="row">
  <tr><td>${row.customer}</td><td>${row.money}</td></tr>
</c:forEach]
</table>

Um die Ajax-Request zu rendern, muss man in Javascript etwas analoges umsetzen

var table = "";
table += "<table>"
table += "<tr><th>Kunde</th><th>Guthaben</th></tr>"
for (var row : data) {
  table += "<tr><td>" + row.customer + "</td><td>" + row.money  +"</td></tr>"
}
table += "</table>"
addToDom(table)

An dieser Stelle ist eine Menge redundanter Code entstanden, der uns immer auf die Füße fallen wird, wenn irgendeine Design-Änderung an der Formatierung vorgenommen wird.

Die Alternative dazu ist, dass man für den initialen Seitenaufbau dieses Element gar nicht mitsendet, sondern im document.ready den Ajax-Call absetzt. So kann man sich das serverseitige Rendern sparen.

Der Datenaustausch über JSON hat damit folgende Nachteile:

  • Redundanter Code (wenn man direkt serverseitig in die Seite rendert)
  • Schlechtere SEO-Performance (wenn man beim document.ready nachlädt)
  • Schlechtere Seiten-Performance (wenn man beim document.ready nachlädt, da ein neuer Serverroundtrip benötigt wird, um die Seite vollständig zu rendern)
  • Javascript ist keine geeignete Sprache, um HTML-Formatierungen vorzunehmen (wie Listing 2 zeigt)
  • Bei multilingualen Seiten muss man sein Javascript lokalisieren (um z.B. die Begriffe „Kunde“ und „Guthaben“ aus den Headern zu übersetzen)

Dem gegenüber steht natürlich auch ein Vorteil, den ich nicht verschweigen möchte:

  • Nicht-Formatierte Daten benötigen weniger Bandbreite als formatierte

Zu der Datengröße habe ich ein paar nicht repräsentative Tests gemacht und auf unterschiedlichen Seiten die Datengröße eines übertragenen JSON-Strings mit dem entstandenen HTML vergleichen. Der Unterschied in der Datenmenge nachdem beides GZIP-komprimiert wurde lag bei unter 20%.

Wer nicht so high-sophisticated unterwegs ist, dass er mit diesen 20% leben kann, dem empfehele ich einen Blick auf Taconite zu werfen. Taconite ist eine Bibliothek, die es einem komfortabel erlaubt per Ajax serverseitig generierte HTML-Elemente in den DOM Tree zu hängen (auch mehrere pro Request).

Taconite gibt es als JQuery-Plugin, das im Wesentlichen dafür sorgt, dass bei Ajax-Requests per JQuery jedes Response-Dokument, das dem folgendem Aufbau entspricht, geparst wird und die entsprechenden Element-Definitionen im DOM landen

<taconite>
  <replaceContent select="someId"><![CDATA[
    some HTML
  ]]></replaceContent>
</taconite>

Es gibt noch einiges mehr an Befehlen als nur replaceContent, darüber gibt die Taconite-Doku am besten Aufschluss.

Und wie hilft uns das jetzt die oben beschriebenen Probleme zu lösen?

Zunächst brauchen wir für die Elemente, die man potentiell per Ajax aktualisieren kann eine eigene JSP, die unsere Daten formatiert:

<table>
<tr><th>Kunde</th><th>Guthaben</th></tr>
<c:forEach items="table" var="row">
  <tr><td>${row.customer}</td><td>${row.money}</td></tr>
</c:forEach]
</table>

Um das Table-Element nun serverseitig in eine Seite zu rendern, müssen wir diese JSP nur inkludieren:

<%-- do a lot of stuff --%>
<div id="tableId">
  <jsp:include page="tableView.jsp" />
</div>
<%-- do more stuff --%>

Für die Ajax-Calls brauchen wir noch etwas Infrastruktur, die es uns erlaubt in unserem Controller anzugeben welche Elemente wir aktualisieren wollen, und wie das gemacht wird

public class AjaxHandler {
  private Map<String, String> replaceContents = new HashMap<String, String>();

  public Map<String, String> getReplaceContents() {
    return replaceContents;
  }

  public void addReplaceContent(String id, String view) {
    this.replaceContents.put(id, view);
  }
}

 

<taconite>
  <c:forEach items="${ajaxHandler.replaceContents}" var="repl">
    <replaceContent select="#${repl.key}"><![CDATA[
      <jsp:include page="/WEB-INF/views/${repl.value}.jsp" />
    ]]></replaceContent>
  </c:forEach>
</taconite>

Damit haben wir unserem Controller nun die Möglichkeit gegeben bei einem Ajax-Aufruf beliebige Elemente zu aktualisisieren.

@Controller
@RequestMapping("mycontroller")
public class MyController {
@RequestMapping("main")
  public String getMainPage(Model model) {
    model.addAttribute("table", getMoneyTable());
    return "somePage";
  }

  @RequestMapping("updateTable")
  public String updateTableAjax(Model model) {
    model.addAttribute("table", getMoneyTable());
    AjaxHandler handler = new AjaxHandler();
    handler.addReplaceContent("tableId", "tableView");
    model.addAttribute("ajaxHandler", handler);
    return "taconite";
  }
}

Fazit
Indem wir das Denk-Paradigma „per Ajax darf man nur strukturierte Daten übertragen“ durchbrochen und eine ziemlich coole Bibliothek als Unterstützung geholt haben, konnten wir einige Probleme beim Umgang mit Ajax lösen. Auch wenn die obere Lösung nur eine Skizze darstellt und an einigen Stellen noch aufgebohrt werden muss, ist der Aufwand sehr überschaubar.
Insbesondere haben wir eine leichtgewichtige Lösung geschaffen, die uns außer einer sauberen Strukturierung unserer JSPs keine weiteren Auflagen macht.