Hinweis:
Dieser Blogartikel ist älter als 5 Jahre – die genannten Inhalte sind eventuell überholt.
In diesem Artikel wird von Grund auf gezeigt, wie eine Webkomponente mit Polymer 1.0 erstellt wird, welche Aufgaben durch Tools automatisiert werden können und welche Hilfsfunktionen Polymer bereitstellt, um die Webkomponente übersichtlich zu strukturieren. Die Erstellung der Dokumentation, einer Livedemo und einiger Tests runden die Webkomponente ab. Abschließend wird die fertige Komponente auf den zwei populären Verzeichnisseiten für Web Components veröffentlicht. Dieser Artikel bezieht sich auf Polymer in der Version 1.0. Ein weiterer Artikel wird sich mit dem Übergang von 0.5 auf 1.0 und die zukünftige Roadmap für Web Components beschäftigen.
Von Action Sheets bis WebComponents
Vor über zwanzig Jahren entstand das Web zum Austausch von wissenschaftlichen Dokumenten. Erst durch die Entwicklung von JavaScript sind einfache, aber interaktive Webseiten und kleinere Webanwendungen möglich geworden. Die Anforderungen haben sich seither drastisch verändert, und Webanwendungen werden immer komplexer, aber es fehlt an standardisierten Strukturierungsmöglichkeiten auf Seiten des Frontends.
Frameworks auf Server- und Clientseite können zwar bei der Strukturierung helfen, setzen aber in aller Regel auf proprietäre Lösungen – ein Standard fehlt. Dadurch ist es schwierig, verschiedene Frameworks parallel einzusetzen. Außerdem sind Lösungen nicht auf andere Projekte übertragbar, da sie nicht kompatibel zueinander sind oder das Setup große Schmerzen bereitet.
Historie von Web Components
Bereits seit 1998 wird versucht, dieses Problem der Webplattform zu lösen, um Webanwendungen besser in Verhalten, Aussehen und Struktur zu gliedern. Sämtliche Versuche sind beim W3C eingereicht, haben es bislang aber nie zu einer Empfehlung geschafft. Einen kleinen Rückblick auf die Jahre 1998 bis 2011 zeigt Abb. 1.
Action Sheets sind ein von Netscape im Jahre 1998 verfolgter Ansatz, zusätzlich zur Trennung der Präsentation (CSS) und Struktur (HTML) auch das Verhalten (JavaScript) zu abstrahieren. Ein Action Sheet besitzt dieselbe Struktur wie auch Style Sheets, verknüpft dabei aber Elemente mit JavaScript, anstatt mit Stileigenschaften. Als Attribut sind ausschließlich die nativen Events (onclick, onfocus, …) erlaubt. Der Wert ist JavaScript-Code, der inline als String angegeben wird.
Noch im selben Jahr versuchte Microsoft durch die HTML Components (HTC) einen Ansatz über XML. Anders als bei Action Sheets kann das Verhalten von Standard-HTML-Elementen überschrieben oder erweitert werden. Darüber hinaus können neue Attribute und Methoden auf den Elementen hinzugefügt werden. Die Definition einer Komponente erfolgt in einer XML-Datei mit der Dateiendung .htc. Als neues CSS-Attribut wird behaviour eingeführt, das als Wert url(../file.htc) besitzt. Eine der bekanntesten Komponenten dürfte CSS3 PIE sein, das fehlende CSS3-Unterstützung im Internet Explorer nachrüstet. HTC war bis einschließlich Version 9 des Internet Explorers integriert.
Auch der Versuch, die beiden Ansätze 1999 miteinander in BECSS (Behavioral Extensions to CSS) zu vereinen, scheiterte aufgrund fehlenden Interesses an der Implementierung einer komplexen Binding-Sprache, die hierfür zwingend erforderlich ist.
Im Jahr 2000 startete Mozilla dann die Entwicklung an XBL (XML Binding Language), die durch HTC beeinflusst worden ist und selbst wiederum BECSS beeinflusst hat. Die Entwicklung ist 2006 in einer nahezu vollständigen Neuentwicklung als XBL 2.0 weiterverfolgt worden und die Entwicklungen an Version 1 stoppten.
Im Gegensatz zu BECSS führte XBL 2.0 auch Templates ein, die es erlauben, in bestehende Elemente im Webdokument Knoten einzufügen, die zwar im Renderingprozess berücksichtigt, nicht aber in das Document Object Model des Webdokuments eingefügt werden. In der Spezifikation ist neben Templates bereits die Rede von Shadow-Trees, Event Retargeting und Custom-Setters/Getters, die später unter dem Namen Web Components in HTML-Templates (zum HTML5-Standard gehörend), Shadow DOM und Custom Elements zu großen Teilen übernommen wurden. XBL 2.0 war eine zu komplexe Spezifikation, die versuchte, zu viele Use Cases abzudecken, weshalb sie letztendlich seitens des Google-Chrome-Teams nicht weiter verfolgt wurde.
Ende 2011 verfasste schließlich Dimitri Glazkov (Google) zusammen mit anderen Use Cases und erarbeitete mit Hayato Ito (Google) und Hajime Morrita (Google) die Spezifikationen. Durch die Implementierung des parallel dazu entwickelten Polyfills webcomponents.js, der Bibliothek Polymer und durch eine mittlerweile stark gewachsene Community entstand so eine solide und gut unterstützte Basis, um Web Components bereits heute auf allen relevanten, modernen Browsern – insbesondere auf mobilen Geräten – einzusetzen.
Vorbereitungen
Für einen ersten und einfachen Einstieg in Polymer bietet es sich an, den zur Verfügung stehenden Polymer-Generator zu verwenden, um die grundlegende App-Struktur in einem „Best-Practice-Ansatz“ vollautomatisch erzeugen zu lassen. Bevor es nun losgeht, muss zunächst Node.js installiert sein. Anschließend können über die Kommandozeile auch dessen Paketmanager npm genutzt und darüber die weiteren Tools Yeoman (Scaffolder) und der Polymer-Generator installiert werden:
1 |
npm install -g yo@1.4.8 && npm install -g generator-polymer@1.2.1 |
Bower (Dependency-Manager) und gulp/Grunt (Taskrunner) werden hier nicht explizit installiert, da dies später vom Generator übernommen wird.
1. Erstellen des Grundgerüsts
Prinzipiell bietet der Generator zwei Wege, um eine neue Komponente zu erstellen:
- Erstellung einer wiederverwendbaren Komponente, die mit anderen geteilt werden soll.
- Erstellung einer Komponente, die nur innerhalb eines Projekts genutzt werden soll.
Für die umzusetzende Komponente wäre der erste Weg sicherlich der richtige, allerdings ist es einfacher, zunächst eine simple App durch den Generator erstellen zu lassen. Die Migration in eine Standalone-Komponente wird später in diesem Artikel erklärt.
Folgender Befehl erstellt ein vollständiges Polymer-Projekt mit entsprechendem Boilerplate-Code und einigen beispielhaft implementierten Komponenten:
1 |
mkdir tiny-polyblog && cd tiny-polyblog && yo polymer:app |
Bei den Rückfragen werden der web-component-tester und die Dokumentation ausgewählt.
gulp, welches als Build-Tool mitinstalliert wird, ist bereits vollständig vorkonfiguriert und integriert neben einem Webserver auch so genannte File Watchers, die den Browser beim Ändern von Dateien zum Neuladen der Seite veranlassen. Zusätzlich sind die folgenden Funktionen integriert:
- CSS-Autoprefixer, um präfixfreies CSS schreiben zu können
- CSS-/JS-/HTML-/Bild-Minifizierer zur Reduzierung der Dateigrößen
- Polymer Vulcanizer zur Konkatenation von HTML-Imports in eine einzige Datei
Damit ist eine solide Out-of-the-box-Lösung verfügbar, mit der direkt gearbeitet werden kann. Die Ordnerstruktur enthält neben den Konfigurationsdateien und 3rd-Party-Komponenten/-tools die eigentliche Anwendung im Ordner app.
Das Starten des Webservers erfolgt auf der Konsole mit gulp serve. Der Browser sollte daraufhin geöffnet werden und zeigt die Beispielwebapplikation. Würde die Webapplikation deployt werden, so wird durch den Befehl gulp serve:dist die Anwendung nach oben genannten Optimierungen im Ausgabeordner dist erzeugt.
2. Erstellen der Komponente
Als praktisches Beispiel soll eine Komponente erstellt werden, die aus einem JSON-, RSS- oder XML-Dokument eine Listendarstellung mit Titel, Datum, Kategorie, Inhalt und einem Vorschaubild erzeugt. Durch weitere Attribute soll zwischen zwei unterschiedlichen Ansichten umgeschaltet und eine Sortierung der Daten nach Datum auf- oder absteigend möglich sein.
Die Komponente kann über die Konsole im Ordner tiny-polyblog mit folgendem Befehl durch Yeoman erstellt werden:
1 |
yo polymer:el ph-listview iron-ajax iron-image iron-list --docs |
Die Nachfrage, ob ein Import in der Datei app/elements/elements.html hinzugefügt werden soll, kann mit Y bzw. y bejaht werden. Der Yeoman-Generator erzeugt daraufhin die Dateien demo/index.html, index.html, ph-listview.html im Ordner app/elements/ph-listview/. Da die Iron-Elemente ajax, image und list später noch benötigt werden, werden sie direkt in diesem Befehl mit angegeben.
Die Demo- und Indexseiten werden aufgrund des Flags –docs hinzugefügt. Eine Demoseite wird normalerweise nur für eigenständige Komponenten erstellt. Es ist aber durchaus sinnvoll, diese auch für Komponenten einzusetzen, die nur innerhalb eines Projekts verwendet werden, wenn beispielsweise mehrere Entwickler an dem Projekt gemeinsam arbeiten. Außerdem sollten diese Dateien für eine eigenständige Komponente sowieso immer vorhanden sein.
3. Implementierung
Mit der Komponente sind asynchrone Requests möglich, ohne dabei auch nur eine Zeile JavaScript-Code schreiben zu müssen. Konfiguriert werden die Attribute per Data Binding. Für die beiden Darstellungsformen der Liste können bedingte, verschachtelte Templates genutzt werden, die je nach gesetztem Attributwert das entsprechende Template instanziieren. Hierdurch ergibt sich die in Listing 1 gezeigte, vereinfachte Struktur des HTML-Markups.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<link rel="import" href="..."> <dom-module id="ph-listview"> <template> <iron-ajax auto url="{{feed}}" on-response="{{_requestCompleted}}"> </iron-ajax> <template is="dom-if" if="{{_isDefault(_entries, viewType)}}"> <!-- ... --> </template> <template is="dom-if" if="{{_isCompact(_entries, viewType)}}"> <!-- ... --> </template> </template> <script> // ... </script> </dom-modue> |
Die in den doppelt geschweiften Klammern stehenden Variablennamen werden durch das Data Binding mit den entsprechenden Variablennamen aus dem JavaScript-Kontext gebunden. Die beiden Ansichten werden so durch viewType entsprechend beeinflusst. Die Instanziierung des entsprechenden Templates übernimmt Polymer.
Die Komponente wird zwar deklarativ genutzt, ist aber nicht sichtbar. Sie liefert ausschließlich funktionale Aspekte. Als Attribut url wird ein URL durch das öffentliche Attribut feed gebunden, auf der ein einfacher GET-Request ausgeführt wird. Sobald die Antwort vom Server erhalten ist, wird durch den deklarativen Polymer-Callback-Handler on-response ein Event gefeuert, das von der Methode _requestCompleted behandelt wird. Per Konvention beginnen private Methoden und Attribute mit einem Unterstrich.
Der Skriptanteil besteht im Wesentlichen aus der Definition der öffentlichen Attribute der Komponente , sodass diese auch von einem Verbraucher entsprechend konfiguriert werden können. Benötigt werden dabei die folgenden Attribute:
- feed: der URL, der als Datenquelle genutzt werden soll (HTTP-Ressource)
- handle-as: der Typ der Datenquelle (json, rss oder xml)
- view-type: die gewünschte Ansicht (default oder compact)
- sort-by-date-asc: aufsteigende Sortierung der Einträge (true oder false)
- date-format: ein für Moment.js valides Datumsformat, mit dem die Zeit geparst und anschließend sortiert wird
Diese Attribute werden mit ihren Default-Werten, wie in Listing 2 zu sehen, definiert. Die Bindestrich-Notation der Attribute wird für den Javascript-Anteil automatisch auf die entsprechende Darstellung in Camelcase gemappt (z.B. wird handle-as automatisch zu handleAs).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<dom-module id="ph-listview"> <template> <!-- (analog zu Listing 1) --> </template> <script> Polymer({ is: 'ph-listview', properties: { // ... viewType: { type: String, value: 'default', reflect: true // reflektiert Attributswertänderungen innerhalb }, // der Komponente auch in den DOM // ... } // ... }); </script> </dom-module> |
Durch die Komponente verursachte Änderungen dieser Attribute werden per Default nicht in den DOM geschrieben. Diese Reflektion ist auch nur dann sinnvoll, wenn die Außenwelt auf Änderungen reagieren soll. In Listing 2 wird einzig viewType auf den Wert default gesetzt, wenn kein Wert angegeben ist.
Zur Darstellung beliebiger Datenformate kann eine Funktion zum Parsen der Daten übergeben werden. Der Rückgabewert muss dabei ein Array in folgendem Format sein:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[{ title: 'My Title', date: '2010-01-01T05:06:07', category: 'My Category', description: 'A bit more text', link: 'http://www.google.com/' }] |
Entsprechend den enthaltenen Attributen wird daraus eine Liste erzeugt. Bevor dies aber soweit ist, muss zunächst die JavaScript-Implementierung umgesetzt werden (Listing 3). Als Einstiegspunkt in die Komponente werden dabei zunächst alle internen Variablen initialisiert. Dies geschieht durch die Lifecycle-Callbacks. Einer davon ist created. Dieser wird als erster aller Lifecycle-Callbacks ausgeführt. Weitere finden sich im Developer Guide.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
<dom-module is="ph-listview"> <template> <!-- (analog zu Listing 1) --> </template> <script> Polymer({ is: 'ph-listview', properties: { feed: { type: String, value: '', observer: '_feedChanged' }, // ... }, setResponseParseFunc: function(func) { this._responseParseFunc = func; }, created: function() { this._responseParseFunc = null; // response parse function this._sortAsc = false; // the sort order this._entries = []; // holds the parsed response data }, _feedChanged: function(oldVal, newVal) { if (newVal) { // call url and fetch feed } }, // ... }); </script> </dom-module> |
Listing 3 enthält die öffentliche Methode setResponseParseFunc, den Callback created sowie die Definition für das feed-Attribut, das eine Observer-Eigenschaft spezifiziert, das Änderungen der Attribute entsprechend auf die gezeigten Methoden mappt. Es fehlt jetzt nur noch die Implementierung des Callback-Handlers der -Komponente, die die Antwort des Requests an die vom Verbraucher übergebene Funktion übergibt und das Ergebnis in _entries speichert. Bei RSS-Feeds wird keine Parse-Funktion benötigt.
Der Übersichtlichkeit halber wird die RSS-Parse-Funktion als Behavior ausgelagert. Behaviors sind simple JavaScript-Objekte, die gemeinsam verwendete Funktionalität separieren und so in verschiedenen Komponenten eingesetzt werden können.
Listing 4 zeigt den Import der Behavior, die in einer HTML-Datei als Skript eingebunden ist und die private, statische Funktion _handleResponseStatic, die in einer selbstausführenden, anonymen Funktion gekapselt ist. Sie schützt an dieser Stelle vor allem vor dem unbefugten Zugriff der „Außenwelt“ auf die dort definierten Variablen. Gleichzeitig bleiben dadurch die Instanzen der Komponente leichtgewichtiger. Die bislang erwähnten Elemente werden im Polymer API Developer Guide vorgestellt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<link rel="import" href="../helpers.html"> <dom-module id="ph-listview"> <template> <!-- (analog zu Listing 1) --> </template> <script> (function() { // private static in IIFE var _handleResponseStatic = function(handleAs, detail) { // ... return response; }; Polymer({ // ... (analog zu Listing 3) ... _requestCompleted: function(event) { if (event.detail.status != 200) { console.warn('...'); return; } var response = _handleResponseStatic(this.handleAs, event.detail.response); this._entries = this._responseParseFunc(response); // ... } }, behaviors: [Polymer.MyHelper], // ... }); })(); </script> </dom-module> |
Die Behavior wird ganz einfach als einziges Element eines Arrays in der Eigenschaft behaviors übergeben. Der Polymer-Helper kopiert die in MyHelper enthaltenen Objekte zur Laufzeit in die Komponente und erlaubt dadurch die Verwendung der dortigen Funktionen. Sofern unterschiedliche Namen für Variablen und Methoden definiert werden, treten dabei keine Probleme auf.
In Listing 5 ist ein Ausschnitt für die Default-Ansicht des Templates angegeben, das durch die Verwendung von eine performante Endlosliste generiert und mithilfe von ein cropping des Bilds erreicht wird. Zusätzlich werden Computed Bindings eingesetzt, um die Listendarstellung zu sortieren und das Datum zu formatieren, ohne dabei das Array _entries zu verändern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<dom-module id="ph-listview"> <template> <!-- (analog zu Listing 1) --> <iron-list data="{{_filterSortEntries(_entries)}}"> <template> <!-- ... --> <iron-image src="{{ model.thumb }}" sizing="contain"></iron-image> <!-- ... --> <div class="title">{{ model.title }}</div> <div class="subtitle"> <span class="date">{{_filterDateView(model.date)}}</span>, <span class="category">{{ model.category }}</span> </div> <!-- ... --> </template> </iron-list> </template> <!-- ... --> </template> <script> // ... (analog zu Listing 4) ... </script> </dom-module> |
So lassen sich sehr einfach einzelne Sichten erstellen und durch Komposition die Vorteile bereits vorhandener Funktionalität nutzen. Durch die Mischung imperativer und deklarativer Anteile entsteht eine bessere Übersicht, und größere Funktionalitäten lassen sich auf weitere Komponenten aufteilen oder in Behaviors auslagern. Der gesamte Quellcode findet sich im Repository.
4. Dokumentation, Tests und Demoseite
Die Dokumentation kann direkt in Markdown-Syntax und JSDoc-Style in ph-listview.html erfolgen. Ruft man die generierte index.html über einen Browser auf, so werden die Kommentare zu Methoden, Attributen, Events und die in der bower.json angegebene Versionsnummer sauber und ordentlich aufbereitet.
Für die Demoseite empfiehlt es sich, mehrere aussagekräftige Parameterkombinationen zu zeigen und auch zu dokumentieren, um anderen möglichst viele Möglichkeiten der Komponente zu demonstrieren. Tests lassen sich ohne großen Aufwand direkt durch den Aufruf von test/ph-listview-basic.html oder durch gulp:test:local durchführen. Die Dokumentation, Tests und die Demoseite sind aus Platzgründen im Repository angegeben.
Migration und Veröffentlichung der Komponente
Die Migration der Komponente in eine eigenständige Komponente ist einfach und besteht im Wesentlichen aus dem in Abbildung 2 beschriebenen Ablauf.
In einem neuen Verzeichnis können folgende Befehle ausgeführt werden:
1 2 3 4 5 6 7 |
yo polymer:seed ph-listview cd ph-listview bower install PolymerElements/iron-elements#^1.0.0 moment --save bower install PolymerElements/paper-elements#^1.0.0 --save-dev |
Dies erzeugt das Skelett der Komponente und fügt alle benötigten Abhängigkeiten hinzu. Da die Radio-Buttons, die zu den Paper-Elementen gehören, nur in der Demo verwendet werden, können sie als dev-dependencies angegeben werden. Durch das simple Kopieren der CSS-/HTML-Inhalte und der Demo, sowie der Anpassung der HTML-Import-Pfade, der Keywords und Version der bower.json, ist die Komponente bereit zur Veröffentlichung. Damit die Doku und Demo auch direkt auf dem statischen Webserver github.io landen und damit auch ohne eigenen Webserver abrufbar sind, wird der Befehl
1 |
yo polymer:gh |
im Verzeichnis ph-listview ausgeführt. Dieser erzeugt den Git-Branch gh-pages, löst alle Abhängigkeiten auf und pusht den gesamten Inhalt auf GitHub. Erreichbar ist die Komponente dann unter http://.github.io/ph-listview.
Damit die Komponente auch auf den größten Web-Component-Verzeichnisseiten CustomElements.io und Component.kitchen gelistet wird, muss der Master-Branch mit einem Semver-Tag versehen werden und anschließend der Bower-Registry hinzugefügt werden:
1 |
bower register ph-listview git://github.com/<user>/ph-listview.git |
Nun ist alles geschafft und die Komponente in wenigen Tagen auf den Seiten auffindbar.
Verwendung der Komponente
Die Komponente lässt sich nach der Installation über Bower in jedem beliebigen Projekt verwenden:
1 |
bower register ph-listview git://github.com/<user>/ph-listview.git |
Der benötigte HTML-Code ist simpel:
1 2 3 4 5 6 7 8 9 |
<link rel="import" href="../bower_components/ph-listview/ph-listview.html"> <ph-listview feed="http://<url-zum-rss-feed>/feed.rss" handle-as="rss"> </ph-listview> |
Für weitere Codebeispiele sei an dieser Stelle auf die Livedemo verwiesen.
Weitere Ressourcen und „Best Practices“
Neben dem offiziellen API Developer Guide zu Polymer wird seit März 2015 auch eine Art Checkliste vorangetrieben, die bewährte „Best Practices“ für die Erstellung von Komponenten in einem Wiki festzuhalten versucht. Ein ähnlicher Ansatz findet sich ebenfalls auf github.com, bei dem bewährte Polymer-Patterns durch Beispiel-Snippets gezeigt werden. Unter der Endung /PolymerLabs/polymer-patterns ist dieser nachzuvollziehen. Das sind gute Referenzen, die bei der Erstellung eigener Komponenten genutzt werden sollten. Auch der Blick in die Paper- und Core-Elemente ist zu empfehlen.
Fazit
Durch die zur Verfügung stehenden Generatoren für Polymer ist ein sehr einfacher Einstieg in die Welt der Web Components möglich. Der Polymer API Developer Guide gibt durch praktische Beispiele einen schnellen Überblick über die zur Verfügung stehenden Funktionen und Möglichkeiten von Polymer.
Die Veröffentlichung einer Komponente ist mit geringem Aufwand sehr einfach durchzuführen. Dank GitHub ist kein eigener Webserver notwendig, um eine Demo- und Dokuseite für die veröffentlichte Komponente bereitzustellen. Weitere Vorteile ergeben sich durch die Nutzung der Bower-Registry, bei der eigene Komponenten registriert werden können und so die Komponente auch auf den beiden populären Galeriewebseiten CustomElements.io und Component.kitchen erscheint.
Das Tooling, die Polyfills und Polymer werden dabei von erfahrenen Entwicklern und einer sehr großen Community entwickelt, einige davon arbeiten direkt bei Google. Durch diese Art der engen Zusammenarbeit zwischen Webentwicklern, Core-Entwicklern des Chrome-Teams und Spezifikationsschreibenden entsteht ein Dialog, der in der Vergangenheit bei Webstandards selten zu sehen war.
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.
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!
3 Kommentare