Polymer in Dart [Tutorial]

Gepostet am: 05. April 2016

Von

Javascript ist in vielerlei Hinsicht nicht die optimale Wahl, um komplexe Web-Applikationen zu entwickeln. Da die Sprache nicht von Anfang an sorgfältig entworfen wurde, gibt es – abgesehen von Problemen wie fehlender Typisierung und Modularisierung – viele kleine Unschönheiten. Um den Entwicklern eine ordentlich aufgebaute, saubere Sprache zu bieten, mit der Web-Applikationen erstellt werden können, hat Google Dart entworfen, das die Unzulänglichkeiten von Javascript überwinden soll.

Was ist Dart

Auf den ersten Blick erinnert Dart dabei an Java – mit Klassen ohne Mehrfachvererbung, Interfaces, Generics und Annotations werden viele Möglichkeiten geboten, um sauberen objekt-orientierten Code zu schreiben. Dart unterstützt statische Typisierung, diese ist jedoch optional und kann deaktiviert werden – auf diese Weise ist es möglich, bei der Entwicklung statische Typ-Checks zu nutzen, diese im Produktivbetrieb jedoch zu deaktivieren.

Mit pub steht ein an npm erinnernder Paketmanager zur Verfügung, der Dependency Management und Build-Steuerung übernimmt. Pakete können in der zentralen Registry pub.dartlang.org veröffentlicht werden. Die Abhängigkeiten eines Projektes liegen dabei immer in der zentralen Datei pubspec.yaml.

Mit Dart ist es möglich, sowohl Server-Anwendungen als auch Web-Applikationen zu entwickeln – für ersteres steht die Dartium-VM bereit, um das Dart-Programm performant auszuführen, für letzteres eignet sich das Tool Dart2JS, das den nativen Dart-Code in Javascript umwandelt, das anschließend in jedem gewöhnlichen Browser ausgeführt werden kann. Durch ein spezielles Paket ist es möglich, Javascript-Bibliotheken aus dem Dart-Code heraus anzusprechen.

Polymer in Dart – Aufbau

Für die Polymer-Bibliothek wird zusätzlich zur Javascript-Version eine Dart-Version gepflegt, die die Erstellung von Web-Components direkt aus Dart möglich macht. Dabei wird eine einzelne Komponente immer in 2 oder mehr Dateien aufgeteilt – die HTML-Datei mit Template und CSS-Definitionen und die Dart-Datei, die die Logik der Komponente kapselt. Aus Sicht von Dart ist eine Polymer-Komponente eine herkömmliche Dart-Klasse mit Annotationen, die Polymer erlauben, die Komponente im Browser zu registrieren und zu verwenden:

Die zugehörige HTML-Datei mit Template und Styles unterscheidet sich dabei im Aufbau nicht von Polymer-Komponenten, die auf herkömmliche Weise mit Javascript entwickelt wurden, Databinding und CSS-Selektoren sind identisch:

Um eine solche Komponente in einem Dart-Projekt zu verwenden, muss die Basisdatei pubspec.yaml angepasst werden. Dabei müssen die korrekten Dependencies geladen und der Webcomponent-Transformer verknüpft werden, der die verwendeten Webcomponents auflöst. Transformer werden vor der Auslieferung von Dateien ausgeführt und führen Modifikationen am Inhalt durch, in diesem Fall die Anwendung von Polyfills für Shadow DOM, Custom Elements und HTML Imports.

In der Haupt-HTML-Datei, dem sogenannten Entry Point, der der Startpunkt der Webapp und der Abhängigkeits-Auflösung ist, kann die erstellte Komponente verwendet werden:

Hier wird die Haupt-Dart-Datei referenziert, die die Startmethode main() enthält, die wie in Java-Anwendungen den Programmstartpunkt markiert. Im Falle von Webcomponents muss die Dart-Datei nur die benötigten Komponenten importieren und Dart starten, der Rest wird dann von Polymer und den einzelnen Komponenten selbst übernommen:

Mit pub serve wird ein lokaler Entwicklungs-Server gestartet, der die Polymer-Komponenten kompiliert und die Entrypoint-Datei in einem Webserver zur Verfügung stellt. In einem Browser kann anschließend das Endprodukt betrachtet werden. pub bemerkt, wenn Quelldateien sich ändern und kompiliert das Projekt bei Bedarf automatisch neu. Der komplette Beispiel-Code kann auch auf Github eingesehen werden.

Verwendung von Bibliotheken und Komponenten

Das wichtigste Merkmal von Webcomponents ist ihre Fähigkeit zur Wiederverwendung und Komposition mit anderen Komponenten. Die von Google selbst gepflegten Komponenten-Bibliotheken (iron, paper, google, …) sind in Dart portiert und können über das pub-Package polymer_components geladen werden. Das Einbinden der Komponenten funktioniert wie im herkömmlichen Javascript-Dart mit einem Link-Tag (z.B. <link rel="import" href="packages/polymer_elements/iron_list.html">), durch den die Komponente geladen wird und ab sofort in Templates verwendet werden kann. Es ist auch möglich, Instanzen von Komponenten imperativ im Dart-Code zu erzeugen, dafür kann die Funktion Element.tag aus dem dart:html-Package verwendet werden, das die Arbeit mit dem DOM durch ein einheitliches Interface ermöglicht (ähnlich wie jQuery in Javascript).

Verwendung von anderen Webcomponents

Es ist auch möglich, Webcomponents zu nutzen, die nicht auf Dart portiert wurden – da Webcomponents nur einen modernen Browser benötigen, um eingebunden zu werden, können sie einfach so genutzt werden wie in der herkömmlichen Webcomponents-Programmierung: Durch Importieren der HTML-Datei. So kann beispielsweise die Komponente app-router genutzt werden, um ohne Aufwand clientseitige Seitennavigation zu implementieren:

Im oben stehenden Beispiel wird die app-router-Komponente verwendet, um auf der Seite #/home/abc die von uns in Dart entwickelte Komponente darzustellen – Parameter, die mit der Platzhalter-Syntax im path-Attribut definiert wurden, werden automatisch der Komponente übergeben; in diesem Fall würde das Element initialisiert werden. Da die Schnittstelle zwischen den Komponenten nur auf HTML bzw. dem DOM basiert, ist die Tatsache, dass eine Komponente mit Dart entwickelt wurde, nur ein Implementierungsdetail und behindert die Komposition nicht.

Probleme bei Javascript-Dart vs. Polymer-Dart

Probleme entstehen, wenn Komponenten verwendet werden, die Polymer in der Javascript-Version nutzen. Da Polymer eigene Komponenten registriert (z.B. dom-module), kollidieren zwei Instanzen von Polymer, die auf der gleichen Website ausgeführt werden. Momentan ist es daher nicht möglich, ohne Modifikationen gleichzeitig Komponenten zu nutzen, die mit Dart-Polymer bzw. Javascript-Polymer entwickelt wurden. Für das bereits erwähnte polymer_elements Package, das die Komponenten nicht von Grund auf neu implementiert, sondern nur ein Dart-Interface für die Javascript-Komponenten zur Verfügung stellt, wurde das Problem umgangen, indem der HTML-Import von Polymer nachträglich von Javascript-Polymer auf Dart-Polymer geändert wurde. Diese Strategie kann beispielsweise auch für über Bower geladene Webcomponents übernommen werden – ein Post-Install-Script kann alle referenzierten Polymer-Instanzen auf die Dart-Variante umändern und so das Problem von mehreren laufenden Polymer-Instanzen verhindern. Wird das polymer_elements Package verwendet, muss die gleiche Vorgehensweise auch für die dort definierten Komponenten angewendet werden – die Komponente Paper-Button darf nicht zweimal – einmal aus der Bower-Dependency, einmal aus der Dart-Dependency – initialisiert werden. Um die Anpassung der Referenzen zu automatisieren, kann das Bower-Paket polymer-highlander genutzt werden.

Eine andere Möglichkeit zur Auflösung des Konflikts stellt das Package custom_element_apigen dar, das auch für die Google-Elemente in polymer_elements genutzt wurde. Damit kann ein Dart-Wrapper für beliebige Webcomponents geschrieben werden. Der manuelle Aufwand ist allerdings groß und das Tool schlecht dokumentiert.

Die Nutzung von zwei unterschiedlichen Dependency-Management-Systemen mag zwar umständlich und unsauber erscheinen, im Moment ist das Ökosystem von Dart jedoch noch nicht groß genug, um eigene Bibliotheken für alle denkbaren Anwendungsfalle liefern zu können und es steht in Frage, ob das jemals der Fall sein wird, da Google seine Bemühungen aufgegeben hat, jeden Browser mit einer Dart-Runtime auszustatten. Daher muss in vielen Fällen auf Javascript-Pendants zurückgegriffen werden.

Integration von reinen Javascript-Bibliotheken

Javascript hat ein riesiges Ökosystem aus Bibliotheken und Frameworks für die verschiedensten Anwendungsfälle. Oft macht es Sinn, aus einer Dart-Polymer Anwendung heraus auf diese Bibliotheken zuzugreifen, da keine Dart-Bibliothek existiert, die ähnliche Funktionalität bereitstellt. Die Datums-Bibliothek moment.js beispielsweise liefert viele praktische Hilfsmethoden, wie z.B. fromNow – die die Zeitspanne zwischen einem Datum und der aktuellen Zeit in lesbarer Form ausgibt. Diese soll im folgenden Beispiel in einer Dart-Polymer-Komponente zugänglich gemacht werden. Um die Bibliothek zu integrieren, ist der einfachste Weg, diese per Bower zu laden: bower install --save moment. Nun kann die Javascript-Datei im HTML-Part der Komponente mit einem herkömmlichen Script-Tag geladen werden: <script src="bower_components/moment/moment.js" type="application/javascript"></script>. Um nun in Dart-Code auf die Bibliothek zuzugreifen, wird das Core-Package dart:js benötigt, das es ermöglicht, mit Dart Javascript-Code anzusprechen. Mithilfe dieses Package ist es möglich, einen Dart-Proxy für moment zu entwickeln:

Im Konstruktor der Dart-Klasse wird eine Javascript-Instanz von moment aufgerufen. Dafür muss auf die Variable context zugegriffen werden, die den globalen Namespace in Javascript darstellt. Die Moment-Bibliothek registriert dort automatisch die globale Variable moment, die mit der Methode callMethod aus dem dart:js Package aufgerufen werden kann. Als Parameter wird der Parameter des Dart-Konstruktors durchgeschleust, der absichtlich nicht typisiert wurde, da die Moment-Bibliothek verschiedene Werte als Parameter akzeptiert. Das resultierende JsObject wird als Instanz-Variable gespeichert. Ab jetzt sind die verschiedenen Funktionen der Bibliothek zugänglich, wie in diesem Beispiel die fromNow-Methode, die auch wieder mit callMethod aufgerufen wird. Da de Rückgabe-Wert von callMethod immer vom Typ JsObject ist, muss er in diesem Fall noch mit toString() in einen normalen Dart-String umgewandelt werden, um die Typsicherheit zu gewährleisten. Die Funktionalität könnte nun in der Komponente als Computed Binding im Template verfügbar gemacht werden:

Ausgabe: The beginning of the unix-epoch was 46 years ago

Es ist zu sehen, dass die Verwendung von Javascript-Bibliotheken aus Dart heraus nicht schwierig ist, jedoch immer den Aufwand nach sich zieht, eine Proxy-Klasse zu definieren. Außerdem fühlen sich die typenlosen Interfaces der Javascript-Bibliotheken u.U. in typisiertem Dart-Code merkwürdig an. Daher sollte auf die Faustregel zurückgegriffen werden, die Dart-Welt nur zu verlassen, wenn dies unbedingt nötig ist, um den Overhead der Grenzüberschreitung zu vermeiden – da das Dart-Ökosystem immer weiter wächst, wird die Not, Bibliotheken aus Dart einzubinden, mit der Zeit wahrscheinlich immer kleiner werden. Zusätzlich ist mit dem js-Package eine weitere Möglichkeit in aktiver Entwicklung, auf schnelle und einfache Art Interfaces zu bestehenden Javascript-Bibliotheken zu erstellen, die vollkommen deklarativ funktioniert.

Der Build

Um aus den erstellten Dart-Klassen eine auslieferbare Webapp zu erstellen, sollte das Pub-Tool mit den Transformern web_components und reflectables verwendet werden – mit dem Befehl pub build --mode=release geht der Transformer vom spezifizierten Entry-Point aus und nimmt alle benötigten Dart-Klassen und Assets wie HTML- oder CSS-Files in den Compile-Vorgang mit auf. Um den ausgegebenen Code möglichst klein zu halten, wird Tree shaking angewendet. Dabei wird aller Code, der im aktuellen Programm nicht erreicht wird und damit unnötig ist, ausgeschlossen. Anschließend wird mit dem Tool Dart2JS das Dart-Programm in Javascript übersetzt, das auch im Browser ausführbar ist. Daraus resultieren im build-Ordner einige Javascript- und Asset-Files, die die komplette Webapp beinhalten und von einem Webserver ausgeliefert werden können. Vorsicht: Es werden auch Dateien im build-Ordner abgelegt, die im Endprodukt gar nicht benötigt werden; HTML-Templates von verwendeten Webcomponents werden automatisch in der Haupt-HTML-Datei gebündelt abgelegt und sind daher nicht mehr als eigenständige Datei notwendig.

Veröffentlichen von eigenen Komponenten

Im Dart-Ökosystem wird der Pub-Packagemanager verwendet, um anderen eigene Bibliotheken zur Verfügung zu stellen. Da im Grunde alle Dart-Projekte bereits Pub-Projekte sind, da sie so Abhängigkeiten zu anderen Bibliotheken auflösen, fehlen nur noch wenige Schritte zum Erstellen eines eigenen Packages. Wichtig dabei ist, dass den Layout-Spezifikationen von Pub-Packages gefolgt wird. Die folgende Ordner-Struktur ist dabei vorgegeben:

Auf oberster Verzeichnis-Ebene befindet sich die pubspec.yaml-Datei, die die Konfiguration des Packages beinhaltet. In den Dateien README.md, CHANGELOG.md und LICENSE befinden sich zusätzliche Hilfsinformationen für Entwickler, die dieses Package nutzen wollen. Dart-Code und Assets, die später ins eigene Projekt importiert werden sollen, müssen sich im Ordner lib befinden. Dabei ist es Konvention, dass die Hauptdatei, mit der die ganze Funktionalität des Packages importiert werden kann, den gleichen Namen wie das Projekt selbst trägt.

Im Falle der selbstgeschriebenen Polymer-Komponente müsste diese also nur im Lib-Ordner abgelegt werden, um ein gültiges, zur Veröffentlichung bereites Package zu erstellen. Über den Befehl pub publish --dry-run kann ein Testlauf durchgeführt werden, der prüft, ob alle Informationen vollständig sind und ob das Package problemlos von anderen Entwicklern genutzt werden kann. Dabei wird z.B. auch überprüft, ob Ranges von Versionen für Abhängigkeiten des Packages angegeben werden – wird eine spezifische Abhängigkeitsversion gewählt, kann das Probleme mit den Abhängigkeiten von anderen Packages ergeben, die vom Anwender ebenfalls genutzt werden. Nach der Veröffentlichung mit pub publish, das einen Google-Account zur Authentifizierung benötigt, ist das Package auf pub.dartlang.org mitsamt automatisiert erstellten API-Docs verfügbar und kann in anderen Packages eingebunden werden. Das Package polymer_time_ago zeigt eine nach dieser Methodik veröffentlichte Polymer-Komponente in einem Minimalbeispiel.

Testen von Polymer-Apps mit Dart

Um die selbsterstellten Komponenten zu testen, kann das test-Package per Pub installiert werden, mit dem beliebige Unit-Tests umgesetzt werden können. Mittels der Annotation @TestOn(‚Browser’) kann eine Test-Datei als Browser-Test markiert werden. Im Falle von Webcomponents muss, damit die Dart-Komponenten im Browser korrekt initialisiert werden, zuerst die Datei interop_support.html aus dem web_components Package eingebunden werden. Um die HTML-Umgebung des Tests auf diese Weise vorzubereiten, kann pro Dart-Testdatei eine HTML-Datei hinterlegt werden. Diese muss den gleichen Namen wie die Dart-Datei tragen und per die Unit-Tests importieren. Die HTML- und Dart-Dateien sollten dabei in einem separaten Ordner test im Projektverzeichnis liegen.

Für die Unit-Tests selbst können die Funktionen setUp() und test() verwendet werden, die einzelne Test-Cases separieren. Die setUp-Methode wird dabei vor jeder test()-Methode ausgeführt. Es existieren viele weitere Hooks, um auch komplexe Testcases abbilden zu können. In einem beispielhaften Testcase wird für die gerade erstelle Komponente my-component getestet, ob der Parameter param korrekt im Template angezeigt wird.

Um die Tests erst dann zu starten, wenn Polymer initialisiert ist und Komponenten genutzt werden können, muss auf die Methode initPolymer gewartet werden. In der setUp-Methode wird dann ein frisches Element der Komponente generiert. Auf diese Weise wird verhindert, dass sich die einzelnen Testcases über den State der Komponente gegenseitig beeinflussen können. Mit PolymerDom() kann auf die in einer Komponente gekapselten Elemente zugegriffen werden – der Zugriff muss an dieser Stelle polymer-spezifisch erfolgen, da die Elemente nicht immer in einem echten Shadow-DOM liegen sondern auch mithilfe von Polymers Polyfill Shady DOM direkt im HTML-Dokument. PolymerDom erkennt, welche Technik angewendet wurde und gibt die Referenz auf den richtigen DOM-Knoten zurück.

Um die Tests auszuführen, wird der Befehl pub run test-p chrome ausgeführt. Polymer-Komponenten nutzen jedoch den web_components Transformer, um den Output im Browser ausführbar zu machen. pub run test würde die benötigten Dateien direkt vom Dateisystem lesen und so den Transformer umgehen, was die Tests fehlschlagen ließe. Damit die Tests auf den vom Transformer lauffähig gemachten Komponenten ausgeführt werden, muss mittels pub serve ein lokaler Webserver gestartet werden und in einem anderen Terminal der Test-Befehl pub run test -p chrome --pub-serve=8081 eingegeben werden. Das Flag --pub-serve weißt den Testrunner an, die benötigten Dateien nicht vom Dateisystem sondern vom lokalen Webserver zu laden.

Fazit

Es sind noch einige Unschönheiten zu bereinigen, um Dart in Kombination mit Polymer zu nutzen, vor allem das Zusammenspiel von in Javascript und in Dart entwickelten Komponenten muss noch vereinfacht werden. Abgesehen von diesen Kinderkrankheiten ist es mit Dart bereits heute möglich, in einer typisierten, sorgfältig designten Sprache komponentenbasierte Web-Applikationen zu entwickeln, die sowohl vom wachsenden Dart-Ökosystem als auch – wenn auch auf kleinen Umwegen – vom bestehenden Javascript-Ökosystem profitieren können.

Links & Quellen

We’re hiring!

Tapetenwechsel gefällig? Wir sind auf der Suche nach begeisterten Frontend-Entwicklern, die unsere Projektteams im Umfeld von JavaScript, HTML und CSS unterstützen und auch vor innovativen Themen wie AngularJS und Progressive Web Apps nicht zurückschrecken. Jetzt Bewerben!

Weiterlesen

Mehr Informationen zu unseren Dienstleistungen rund um die Web-Entwicklung gibt es auf unserer Website. Unser Portfolio umfasst außerdem die Anwendungsentwicklung für Android & iOS mit speziellem Fokus auf Enterprise-Apps. Für direkten Kontakt schreibt an info@inovex.de oder ruft an unter +49 721 619 021-0.

2017-11-28T17:27:05+00:00