12. März 2017 // von Stefan Pflaum

Einführung und Best Practices zu Dust.js

Je größer ein Software-Projekt und je mehr Programmierer in diesem zusammenarbeiten, desto wichtiger ist es, den Quellcode zu strukturieren und zu organisieren. Tut man das nicht, steht das Entwicklerteam früher oder später vor einem Berg von unübersichtlichen und riesigen Dateien voll mit Quellcode, der die Wartung und Erweiterung bereits vorhandenen Codes zur Qual macht.

Viele Entwicklerteams setzen hier auf das Model View Controller Prinzip, das die Software in die drei Komponenten Daten, Präsentation und Steuerung aufteilt.

Templating Engines wollen diese Aufteilung unterstützen, indem sie Logik auf der Präsentationsebene minimieren. Das erhöht die Lesbarkeit und damit die Wart- und Erweiterbarkeit des Codes und macht es zudem einem Frontendentwickler möglich, die graphische Aufbereitung des Programms unabhängig von einem Backend-Developer anzupassen.

Im Webshop von thomann.de wurde schon sehr früh auf eine derartige Programmarchitektur gesetzt und so profitieren heute über ein Dutzend Vollzeit-Entwickler von einem gut strukturierten Code, der von Bugfixes bis Neuumsetzungen einfach zu handhaben ist.

Zur Unterstützung dieser Architektur und des Entwicklungs-Workflows verwendeten wir anfangs eine selbstgeschriebene Template Engine, sind jetzt aber schon seit vielen Jahren auf die Javascript Templating Engine "Dustjs" umgestiegen, die heute in fast allen Bereichen unseres Webshops zum Einsatz kommt.

Warum Dust.js?

Bei der Entscheidungsfindung für unsere neue Template Engine hatten wir selbstverständlich hohe Anforderungen, denn wir wären nicht Thomann, wenn wir nicht die für unsere Kunden und unsere Mitarbeiter beste Lösung anstreben würden.

Nach langer Recherche und reichlichen Überlegungen haben wir uns letztendlich für Dust.js entschieden. Dust.js bietet viele Vorteile gegenüber anderen Template Engines, die für uns wichtigsten Eigenschaften waren:

Performance

Dust.js lädt und rendert seine Daten asynchron. Das führt zu einer schnelleren Gesamt-Ladezeit der Website und daraus folgend zu einer besseren Erfahrung für den Besucher.

Funktionen

PHP wird aus dust-Templates verbannt. Stattdessen bietet Dust.js zahlreiche built-in Funktionen, die sogenannten "helper". Diese helper werden verwendet, um auch in der Darstellungs-Komponente Kontrollstrukturen wie Bedingungen und Schleifen verwenden zu können. Dennoch bleibt der Code aufgeräumt und strukturiert.

Erweiterbarkeit

Neben den built-in helper bietet Dust.js die Möglichkeit, eigene helper zu definieren, die dann in den Templates verwendet werden können. Das vermeidet hässliche Workarounds und bietet uns die Möglichkeit, sämtliche Funktionalität fürs Template bereitzustellen, ohne Logik mit Darstellung zu vermischen.

Struktur

Jede Template Engine, die etwas auf sich hält, bietet dem Programmierer eine Möglichkeit, seine Ausgaben zu strukturieren und bestimmte Elemente wiederverwertbar zu machen. In Dust.js werden solche wiederverwertbaren Elemente "partials" genannt. Korrekt umgesetzte Partials können auf mehreren Seiten mit unterschiedlichen Parametern eingebunden werden und zeigen dann den für den Kontext korrekten Inhalt in der immer gleichen Darstellung - das spart Schreibarbeit und macht Anpassungen schnell und einfach.

Wie wir Dust.js verwenden

thomann.de hat wahnsinnig viele unterschiedliche Seiten. Es ist völlig logisch, dass nicht für jede Seite der komplette Inhalt von Grund auf neu umgesetzt werden kann.

Viel sinnvoller ist es doch, grundlegende Elemente wie z.B. Produktlisten, Slider, Kategorie Listen, etc. als Elemente zu realisieren, die einmal definiert immer bei Bedarf eingebunden und ausgegeben werden.

Diese Elemente heißen in Dust.js "partials" und sind ein wesentlicher Bestandteil unseres umfangreichen Shops. Doch wie sieht ein partial aus und wie wird es von uns verwendet?

Partials

Nehmen wir das Beispiel "Artikelliste":

Es gibt wohl kein Element, das auf thomann.de öfter verwendet wird. Es wäre sehr umständlich, wenn wir an jeder Seite mit einer Produktlistung die Listen-Artikel neu umgesetzt hätten. Selbst kleine Änderungen wie das Vertauschen des Artikelbildes mit den Bewertungssternen müssten dann auf allen Seiten einzeln durchgeführt werden.

Es ist also kein Wunder, dass wir hier ein partial verwenden. Beispielhaft könnte die Umsetzung des Listen-Artikels wie folgt aussehen (aus Platzgründen sind die Values teilweise verkürzt dargestellt):

An Dust bereitgestellte Daten

[
  {
    "url":"https:\/\/www.th[...]65_supra_phonic.htm",
    "image":"https:\/\/thumbs1.static-thomann.[...]4416.jpg",
    "rating": {
      "stars":4.5,
      "count":21
    },
    "name":"Ludwig LM402 Supra Phonic Snare",
    "category":"14\" Snare Drum",
    "checklist": [
      "Supra Phonic Chrome Serie",
      "Gr\u00f6sse: 14\" x 6,5\"",
      "Chrom bes[...] (nahtlos gezogen)"
    ],
    "availability":1,
    "price":749,
    "oldPrice":850,
    "manufacturer":"https:\/\/[...]ludwig.jpg"
  },
  {
    "url":"https:\/\/www.thoma[...]pra_phonic.htm",
    "image":"https:\/\/thumbs1.stati[...]15.jpg",
    "rating": {
      "stars":5,
      "count":6
    },
    "name":"Ludwig LM400 14\"x05\" Supra Phonic",
    "category":"14\" Snare Drum",
    "checklist": [
      "Supra Phonic Chrome",
      "Gr\u00f6sse: 14\" x 5\"",
      "chromb[...]uminium Kessel (nahtlos gezogen)"
    ],
    "availability":1,
    "price":699,
    "oldPrice":792,
    "manufacturer":"https:\/\/thumbs[...]logos\/ludwig.jpg"
  }
]

Die Daten zu den jeweiligen Artikeln der Liste werden über PHP an Dust übergeben und stehen dann in allen innerhalb dieses Requests eingebundenen Templates zur Verfügung.

ArticleList.dust

{#articles}
    {>"partials/ArticleListEntry" /}
{/articles}

Alle Dust Kontrollstrukturen werden mit der geschweiften Klammer gekennzeichnet.

{#articles} steht dabei für eine foreach-Schleife auf dem Array "articles" (das wir zuvor an Dust übergeben haben). {/articles} markiert dabei das Ende der Kontrollstruktur und entspricht in diesem Beispiel einem “endforeach;”.

Innerhalb einer Schleife in Dust kann man ohne speziellen Index direkt auf das Element der aktuellen Iteration zugreifen. Z.B. würde ein {name} innerhalb der Schleife im obigen Beispiel den Namen jedes einzelnen Artikels ausgeben. Praktisch!

Partials werden in Dust über {>"pfad/zum/partial” /} eingebunden. Optional können diesem Aufruf Parameter übergeben werden, die dann nur im Template des partials zur Verfügung stehen.

partials/ArticleListEntry.dust

<a class="article" href="{url}">
  <div class="column1">
    <img src="{image}" />
    {>"partials/Rating" ratingData=rating /}
  </div>
  <div class="column2">
    <strong>{name}</strong>
    <span>{category}</span>
    <ol>
      {#checklist}
        <li>{.}</li>
      {/checklist}
    </ol>

    {>"partials/Availability" status=availability /}
  </div>

  <div class="column3">
    <span class="price">{price}</span>
    <span class="old-price">{oldPrice}</span>
    <img src="{manufacturer}" />
    {>"partials/Compare" /}
  </div>
</a>

Wie im partial "ArticleListEntry" zu sehen, können auch partials selbst andere partials einbinden. So haben wir hier zum Beispiel das Element zum Anzeigen der Bewertungen oder die Verfügbarkeit des Artikels in eigene partials ausgelagert, damit diese Elemente z.B. auf der Detailseite des Artikels wieder verwendet werden können.

Ein solches Aufteilen des Codes erleichtert einem Programmierer oder Frontend-Entwickler den Arbeitsalltag ungemein. Die Sterne bei den Bewertungen sollen größer werden? Kein Problem: eine Änderung im richtigen partial passt alle Bewertungs-Sterne auf der gesamten Website an. Für die verschiedenen Verfügbarkeiten sollen neue Symbole verwendet werden? Einfach das partial anpassen und schon bekommt kein Besucher mehr die alten zu Gesicht!

Während das Verwenden von normalen partials schon sehr viel Ordnung in das Code-Wirr-Warr bringen kann, so stoßen diese partials in der Realität eines Webshops von den Dimensionen von thomann.de allerdings bald an ihre Grenzen.

Man denke nur mal an die verschiedenen Artikel-Listungen in der Sidebar:


Auch wenn der Grundaufbau immer gleich ist, unterscheiden sich diese Listungen teilweise signifikant voneinander. Die bei den Top-Sellern und Trends sinnvolle Reihenfolge z.B. ist bei den neuen Hotdeals überflüssig. Auch ergibt die Anzahl der aktuellen Beobachter nur bei den Trends so wirklich Sinn genauso wie die Sternchenauszeichnung nur bei der "Topbewertet" Sidebar benötigt wird.

Der scharfsinnige und fachkundige Leser fragt sich an dieser Stelle sicher, warum das ein Problem sein soll? Wie bereits erwähnt ist beim Einbinden eines partials das Übergeben von Parametern möglich und gleichzeitig können wir innerhalb eines partials Alternativen von Listen-Artikeln definieren. Das würde in unserem bereits verwendeten Beispiel in den Templates wie folgt aussehen:

zusätzliche an Dust bereitgestellte Daten

{"mode": "trends"}

Zusätzlich zu der (um einige Daten erweiterte) Artikelliste selbst wird jetzt die Definition des gewünschten Anzeigemodus nötig. In diesem Beispiel wollen wir die Artikel-Listung des "Trends"-Sidebar-Elements.

SidebarArticleList.dust

{#articles}
    {>"partials/SidebarArticleListEntry" displayMode=mode /}
{/articles}

Der Anzeigemodus wird im Template der Liste beim Einbinden des partials an dieses übergeben.

partials/SidebarArticleListEntry.dust

<a class="sidebarArticle" href="{url}">
  {@ne key=displayMode value="newHotdeals"}
    {@ne key=displayMode value="topRated"}
      <div class="column1">{rank}</div>
    {/ne}    
  {/ne}
  <div class="column2">
    <img src="{image}" />
  </div>
  <div class="column3">
    <span class="name">{name}</span>

    {@eq key=displayMode value="trends"}
      {>"partials/Watchers" /}
    {/eq}

    <span class="price">{price}</span>

    {@eq key=displayMode value="newHotdeals"}
      <span class="old-price">{oldPrice}</span>
    {/eq}

    {@eq key=displayMode value="topRated"}
      <span class="stars">{stars}</span>
    {/eq}
  </div>
</a>

Im partial selbst werden Teile des Elements abhängig vom "displayMode" angezeigt oder ausgeblendet. Dabei sorgt beispielsweise der Helper "ne" ("not equal") dafür, dass die erste Spalte mit dem Artikel-Rang nur angezeigt wird, wenn der Anzeigemodus weder dem der neuen Hotdeals noch der Toprated Sidebar entspricht.
Helper kann man in Dust am @-Zeichen erkennen. Elemente innerhalb eines "eq"-Helperaufrufs ("equal") werden dementsprechend nur angezeigt, wenn die Variable "key" den Wert "value" hat. Das partial "Watchers" für die aktuellen Beobachter wird also nur im Anzeigemodus "trends" eingebunden.

Obwohl diese Herangehensweise natürlich nicht falsch ist und in unserem Beispiel voll und ganz ihren Zweck erfüllen würde, könnte sie den Entwickler in bestimmten Szenarien vor Herausforderungen stellen:

  • Kommt ein neuer Anzeigemodus dazu, müssen wahrscheinlich weite Teile des partials angepasst werden. Das kann auch die bereits vorhandenen Anzeigemodi beeinflussen und so Fehler verursachen, die u.U. lange Zeit unerkannt bleiben.
  • Wird eine ganz spezielle neue Sonderform benötigt, muss ein neuer Anzeigemodus angelegt werden, ganz egal wie klein und einfach die Unterschiede dieser Sonderform sind.
  • Durch die zahlreichen nötigen Kontrollstrukturen, um die verschiedenen Modi anzeigen zu können, wird der Code unübersichtlicher und Debugging wird ungemein schwieriger.

Natürlich kann man auch einfach statt eines Anzeigemodus ein eigenes partial definieren, das nur für einen speziellen Anwendungsfall gedacht ist. Da das allerdings zumindest in Teilen unserem Bestreben beim Verwenden von partials entgegen wirkt, ist das keine Option.

Zum Glück bekommt man auch hier ein gutes Werkzeug zur Lösung der Probleme von Dust an die Hand gegeben: Extensible Partials.

Extensible Partials

Man kann das Einbinden eines extensible partials mit dem Erben von einer Klasse in PHP vergleichen:

Bindet man ein extensible partial in einem Template ein, so wird der Inhalt des extensible partial an der Stelle ausgegeben. Bis hierhin also kein Unterschied zu einem normalen partial.

Möchte man jetzt aber eine Kleinigkeit abändern, muss man nicht mehr den Inhalt des extensible partial Templates anpassen, sondern man hat die Möglichkeit, vordefinierte Blöcke zu überschreiben.

Das funktioniert genauso wie in vielen Programmiersprachen das Überschreiben von Klassenmethoden der geerbten Klasse: sobald man einen Block eines extensible partials überschreibt, wird der vordefinierte Inhalt ignoriert und nur noch der neue Inhalt zur Anzeige gebracht. Nachfolgend unser Beispiel der Sidebar-Listen, nun allerdings als extensible partial realisiert:

partials/ExtensibleSidebarArticleListEntry.dust

<a class="sidebarArticle" href="{url}">
  {+rankBlock}
    <div class="column1">{rank}</div>
  {/rankBlock}

  {+imageBlock}
    <div class="column2">
      <img src="{image}" />
    </div>
  {/imageBlock}

  {+infoBlock}
    <div class="column3">
      {+nameBlock}
        <span class="name">{name}</span>
      {/nameBlock}

      {+watchersBlock}
        {>"partials/Watchers" /}
      {/watchersBlock}

      {+priceBlock}
        <span class="price">{price}</span>
        {+oldPriceBlock/}
      {/priceBlock}
    </div>
  {/infoBlock}
</a>

Wie in dem Beispiel zu sehen sind auch Verschachtelungen möglich. So muss ich z.B. nicht die gesamte letzte Spalte kopieren, wenn ich nur den "nameBlock" überschreiben möchte.

Das Überschreiben von Blöcken eines extensible partials funktioniert wie folgt:

SidebarArticleList.dust

{#articles}
  {>"partials/ExtensibleSidebarArticleListEntry" /}

  {<rankBlock}{/rankBlock}

  {<watchersBlock}{/watchersBlock}

  {<oldPriceBlock}
    <span class="old-price">{oldPrice}</span>
  {/oldPriceBlock}
{/articles}

Wie hier sehr schön zu sehen, muss man beim Überschreiben eines Blockes keinen neuen Inhalt definieren bzw. ist auch ein leerer Inhalt gültig. Das ermöglicht es uns, Teile eines extensible partials zu entfernen.

In diesem Beispiel binden wir unser extensible partial für Sidebar-Artikel in der Liste der neuen Hotdeals ein. Wir entfernen die komplette erste Spalte und die Anzeige der Beobachter, indem wir die entsprechenden Blöcke leeren. Zudem fügen wir in einem extra dafür vorgesehenen, aber leeren Block den Streichpreis der Artikel hinzu.

Durch unsere Änderungen werden weder das extensible partial, noch andere Listen, die das selbe partial verwenden, beeinflusst. Trotzdem habe ich alle Freiheiten bei meinen Listen und kann dennoch Anpassungen, die alle Listen beeinflussen sollen, über das extensible partial realisieren. Ein Traum!

In diesem Beitrag haben wir natürlich nur die Oberfläche von Dust.js und seinen Möglichkeiten angekratzt. In der Praxis gibt es noch jede Menge hilfreicher Tipps und Tricks.

Verwandte Blogposts