Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

22 Februar, 2019

Clojure Template Engines

TL;DR:

Für Webanwendungen gibt es in jeder Sprache zwei bis drei große Frameworks, die man verwenden sollte. Sie ersparen Zeit, vermeiden viele Anfängerfehler und stellen die Basisfunktionalität bereit (meist ein MVC Pattern). Gerade bei PHP (Symfony, Laravel, CodeIgniter), Python (Django, Pyramid, Flask) und Ruby (Rails, Sinatra, Hanami) sind sie sehr beliebt. In Clojure-Land funktioniert die Welt anders: Hier ist man selbst verantwortlich dafür, sich seine Komponenten nach eigenem Geschmack zusammenzusuchen. Die Basis ist Ring, ein Abstraktionslayer für HTTP (ähnlich wie Rack für Ruby). Es gibt ein paar Meta-Frameworks wie Luminus, Pedestal oder Macchiato, welche im Kern allerdings auch nur ein Zusammenschluss von Bibliotheken mit einer Beschreibung sind. Es lohnt sich also, sich mit Ring und den zur Verfügung stehenden Bibliotheken zu beschäftigen. Da das Rendern von HTML einer der Hauptaufgaben einer Webanwendung ist, mache ich hier einen Rundumschlag der bekannteren Template Engines für Clojure.

Selmer

Wer bereits mit Symfony gearbeitet hat, ist zwangsläufig mit Twig in Berührung gekommen. Selmer ist stark an die Syntax von Twig angelehnt und stellt eine klassische Template Engine bereit. Eine harte Trennung zwischen dem Template (gespickt mit Selmer-Syntax) und dem Controller/Model. Selmer nimmt eine HTML-Datei und ein Set von Variablen entgegen und gibt das gerenderte HTML aus. Ein simples Beispiel:

(selmer.parser/render "Hallo {{ name }}" {:name "Aaron"})

Diese Trennung von Anwendungslogik und Template hat eine lange Geschichte. Konzeptionell ist der Gedanke beim MVC Pattern (Model View Controller), dem üblichen Spathetti-Code entgegenzuwirken, den man in alten Perl- und PHP-Anwendungen findet. MVC macht den Code übersichtlicher und modularer. Zudem war wohl der Wunschgedanke, dass Datenbankspezialisten das Model bereitstellen, Designer die View und der Programmierer dann die Anwendungslogik des Controllers. Mit diesem Gedanken sind auch die meisten Template Engines entstanden; Sie wurden absichtlich so simpel wie möglich gehalten, damit auch Leute ohne Programmierkenntnisse Templates erstellen können (und dass man nicht auf die Idee kommt, Anwendungslogik in die View zu stopfen). Aus dieser Wunschvorstellung ist aber leider nie etwas geworden. In den allermeisten Fällen erstellt eine Person sowohl die Anwendungslogik als auch das Template (zumindest in meinen bisherigen Jobs der letzten 15 Jahre).

Und hier liegt dann schon die erste Schwachstelle von Selmer (und allen anderen traditionellen Template Engines): Die Trennung von Controller und View erzwingt es, dass ein Übergang von Anwendungs-State stattfinden muss. Der Controller übergibt ein Set von (vorbereiteten) Variablen an die View. Die Template Engine nimmt die Variablen entgegen und interpretiert das Template zusammen mit den gegebenen Variablen. Heraus kommt das fertige HTML. Die Template Engine selbst hat keinen State, nur die Variablen, die übergeben werden. Beim Erstellen des Templates ist man also auf die Template Engine und deren Syntax beschränkt.

Das macht sich bei Selmer besonders schnell bemerkbar. Selbst simple Sachen wie eine Schleife die n mal durchläuft, ist nicht möglich. Auch der Zugriff auf den Anwendungs-State ist nur durch viel Umstand (add-tag! und add-filter!) machbar. Prinzipiell gibt es eine Hand voll Kontrollstrukturen wie for oder if und die Möglichkeit Variablen auszugeben. Mit Filter lässt sich die Ausgabe dann mit einer Art Pipeline verändern.

Hallo {{ name|lower|replace:o:0 }}!

Enlive

Der Umstand mit der Template-Sprache hat Enlive ganz geschickt umgangen, in dem das Template nur aus reinem HTML besteht. Es gibt keine Platzhalter und auch keine Schleifen oder Fallunterscheidungen. Der Trick dabei ist, das HTML-Template mit Hilfe von CSS-Artigen Selektoren zu selektieren und anschließend mit Clojure zu bearbeiten und zu ersetzen. Das hört sich im ersten Moment eklig an, ist aber ziemlich genial. Ein Beispiel:

(require '[net.cgrand.enlive-html :as html])

(html/deftemplate projects-template "templates/index.html"
  [name]
  [:head :title] (html/content (str "Hello " name)))

(apply str (projects-template "Aaron"))

Mit dem html/deftemplate Makro wird ein neues Template angelegt -- genauer gesagt eine Funktion. Der erste Parameter ist das HTML-Template, der zweite dient als Übergabeparameter und alle folgenden definieren die Selektoren und deren Aktionen. html/content ist hier ein Transformator, der den Inhalt des selektierten DOM-Node ersetzt. Die daraus resultierende Funktion kann überall aufgerufen werden. Die Rückgabe ist das fertig gerenderte HTML in Form einer Sequenz aus Strings (deshalb das apply).

Die View und der Controller sind hier wie bei den gewöhnlichen Template Engines getrennt. Besser noch, das Template enthält gar keinen Code mehr. Die Mächtigkeit wird an einem größeren Beispiel (aus einem Projekt das mittlerweile fast 5 Jahre besteht) deutlicher:

<!DOCTYPE html>
<html>
  <head>
    <title>okoyono.de -- Buch des Monats</title>
  </head>
  <body>
    <div id="covers">
      <div class="cover-item">
        <a href="#">
          <img src="#" alt="LovelyBooks cover" title="Book title">
        </a>
      </div>
  </body>
</html>

Hier der Controller:

(html/defsnippet cover-item-model "buchdesmonats/layout.html"
  [:div#covers :> :div]
  [link title]
  [:a] (html/set-attr :href link)
  [:img] (html/set-attr :src (url->file link "book-covers") :title title))

(html/deftemplate index-template "buchdesmonats/layout.html"
  [cover-urls]
  [:#covers] (html/content
              (map #(cover-item-model % "zu Lovely Books")
                   cover-urls)))

(apply str (index-template list-of-urls))

Das HTML-Template besteht aus einem Div covers, welches wiederum ein beispielhaftes cover-item enthält. Mit html/defsnippet bauen wir uns eine Funktion, die den beispielhaften Eintrag eines Buch-Covers repräsentiert. Die beiden Parameter link und title werden dann im Template mit html/set-attr in den Link und das Bild eingesetzt. In anderen Template Engines wird dieser Mechanismus gern als Partial bezeichnet, in Enlive wird daraus eine ganz gewöhnliche Funktion mit Parametern. Diese Funktion wird dann gleich in index-template verwendet.

Wie man sieht, existiert gar keine Template-Sprache mehr. Sie wurde von Clojure abgelöst. Somit steht dem Programmierer die volle Power der Programmiersprache zur Verfügung und ist nicht auf ein limitiertes Set von Sprachelementen aus einer Template-Sprache wie Selmer oder Twig angewiesen.

Enlive ist im Grunde ein HTML-Parser und -Generator mit ein paar Hilfsfunktionen. Den Parser-Teil kann man allerdings nicht nur für die eigenen Templates einsetzen, sondern für jede Art von Webseite. So lässt sich ganz einfach ein Web Scraper bauen. Hier ein kleines Beispiel aus demselben Projekt von oben:

(-> (java.net.URL. lovelybooks-url)
    html/html-resource
    (html/select [:img.ResponsiveImage.BookCover])
    first
    (get-in [:attrs :srcset])
    (str/split #" "))

Das sieht nach nicht viel aus, macht aber eine ganze Menge. An lovelybooks-url ist ein String mit einer URL gebunden, der mit html/html-resource zu einer Quelle für Enlive umgewandelt wird. Mit html/select suchen wir nun wieder per CSS-Selektor nach der Stelle die uns interessiert -- in dem Fall die URL zum Buch-Cover. Zurück bekommen wir eine Clojure-Datenstruktur des selektierten. Daraus müssen wir dann nur noch die für uns relevanten Stellen heraussuchen. Ziemlich mächtig also.

Leider ist Enlive hoffnungslos veraltet. Das muss im Clojure-Land nichts Schlechtes bedeuten, manche Bibliotheken sind einfach gut so, wie sie sind und müssen nicht mehr gepflegt werden. Leider scheint das Projekt keinen Maintainer mehr zu haben (Bug-Tracker hat noch offene Issues und die Pull Requests sind auch schon extrem lange offen). Das ist sehr schade. Mit Enliven (Enlive Next) wurde versucht, den genialen Gedanken weiterzutreiben, allerdings ist auch dieses Projekt seit 2014 nicht mehr gepflegt worden. Dennoch ist Enlive benutzbar und der Ansatz ist meiner Meinung nach prima! Besonders für das Web Scraping eignet es sich hervorragend.

Hiccup

Einen Schritt weiter geht Hiccup, in dem komplett auf das HTML Template verzichtet wird. Was? Genau, es gibt in Hiccup keine HTML Templates mehr. Das wirkt im ersten Moment befremdlich, doch reduziert es die Komplexität enorm. Hiccup ist extrem einfach. Die API besteht nur aus einer Hand voll Funktionen, die schnell zu erlernen sind. Unser Hello World Beispiel von oben sähe so aus:

(hiccup.core/html [:div "Hallo Aaron"])

Das html Makro nimmt einen Vector entgegen und gibt HTML zurück. Hiccup ist also nur ein Generator, der aus einem Vector HTML erstellt. Auch hier können wir wieder die Power von Clojure nutzen und sind nicht auf eine Template-Sprache beschränkt. Das Übergeben von Variablen an die View ist somit auch hinfällig. Alles ist Clojure-Code. Unser Beispiel von oben:

(hiccup.core/html
  [:div#covers
  (for [[link title] cover-urls]
    [:div#cover-item
      [:a {:href link}
        [:img {:src link :title title :alt "LovelyBooks cover"}]]]))

Mehr ist es im Grunde nicht. Alles was man tun muss ist einen Vector zu erzeugen und in hiccup.core/html zu geben. Der Rest ist dann Aufgabe von Clojure bzw. dem Programmierer. Es gibt noch ein paar Hilfsfunktionen für Formulare und das sichere Encoding/Escaping von Inhalt. Alles andere ist dem Programmierer überlassen. Wer schon einmal mit Om gearbeitet hat, wird sich deshalb direkt zu Hause fühlen. Und das beste daran ist: Hiccup wird aktiv weiterentwickelt.

Während also die meisten Web-Frameworks noch mit klassischen Template Engines arbeiten, hat der lose komponentenorientierte Ansatz von Clojure ein paar sehr interessante Konzepte zutage gebracht. Im Vergleich zu Hiccup und Enlive wirken Template Engines wie Twig oder Selmer fast schon als wären sie aus einer dunklen Vergangenheit und heute nicht mehr relevant. Dennoch wird gerade Twig heute noch fleißig eingesetzt. Auch moderne Frameworks wie Vue.js setzen auf den Template-Gedanken mit Template-Sprache. Zeit für ein Umdenken?