Aus Linux-Magazin 03/2014

Echtzeitfähige Webanwendungen mit dem Framework Meteor

© Sergey Nivens, 123RF.com

Javascript sowohl im Browser als auch auf dem Server: Das Webframework Meteor verspricht Anwendungen aus einem Guss, die sich dank vieler fertiger Pakete rasch programmieren lassen.

Viele Webentwickler verwenden mehrere Programmiersprachen für eine Anwendung: Code in PHP, Ruby, Python oder Java auf dem Server und Javascript für den Browser. Mit dem Open-Source-Framework Meteor [1] können sie dagegen Webanwendungen ganz in Javascript programmieren, dazu kommen wie gewohnt nur noch HTML und CSS. Das soll den Entwicklungsaufwand merklich reduzieren, der automatisierte Build-Mechanismus sowie eine Vielzahl vorgefertigter Pakete ebenfalls.

Dieser Artikel zeigt, wie man eine interaktive Webanwendung mit Meteor, Javascript, HTML 5 und CSS 3 umsetzt. Als anschauliches Beispiel soll ein einfaches Redaktionssystem mit einer Liste von Beiträgen, einem Editor sowie einer Zugriffskontrolle dienen, das den Namen Starpublisher erhält.

Installation

Meteor erscheint unter MIT-Lizenz und lag bei Redaktionsschluss in einer Vorabversion mit der Nummer 0.7.0 vor. Mit Version 1.0 soll das Framework im Frühjahr 2014 Produktionstauglichkeit erreichen. Meteor selbst ist ausschließlich in Javascript geschrieben. Server-seitig setzt es auf Node.js. Die Meteor Development Group entwickelt die Software als Open-Source-Projekt, das mehrere Investoren aus der Internetbranche fördern.

Listing 1 installiert Meteor unter Ubuntu 12.04 im Homeverzeichnis des Benutzers und erzeugt das Grundgerüst der Beispielanwendung mit dem Namen Starpublisher. Zeile 1 holt zunächst das Kommandozeilen-Werkzeug Curl mittels Apt aufs System. Zeile 2 nutzt das Tool, um die aktuelle Meteor-Distribution aus dem Netz zu laden und zu installieren. Zeile 3 erzeugt das Starpublisher-Projekt, Zeile 4 wechselt in das Projektverzeichnis, Zeile 5 startet die Beispielanwendung. Eine ausführliche Dokumentation zu Meteor findet sich unter [2].

Listing 1

Installation

01 sudo apt-get install curl
02 curl https://install.meteor.com | /bin/sh
03 meteor create starpublisher
04 cd starpublisher
05 meteor

Entwicklungsabläufe

Eine Meteor-Anwendung ist bereits nach dem Erstellen des Grundgerüsts lauffähig. Abbildung 1 zeigt Starpublisher im Firefox-Browser. Standardmäßig sind Meteor-Anwendungen per HTTP auf Port 3000 zu erreichen. Die Aufgabe des Entwicklers besteht nun darin, benötigte Meteor-Pakete in das Projektverzeichnis zu installieren und die Anwendungslogik zu programmieren.

Abbildung 1: Meteor-Anwendungen sind bereits nach dem Erzeugen des Grundgerüsts lauffähig.

Abbildung 1: Meteor-Anwendungen sind bereits nach dem Erzeugen des Grundgerüsts lauffähig.

Meteor unterstützt den Programmierer bei seiner Arbeit in vielerlei Hinsicht. Beispielsweise startet das Framework nach jeder Änderung im Projektverzeichnis den Webserver neu und aktualisiert die Browseransicht selbsttätig. Abbildung 2 zeigt Meteors Ausgaben im Terminalemulator. Meteor meldet dort Neustarts und ermuntert den Anwendungsentwickler Fehler zu beheben.

Abbildung 2: Meteor dokumentiert fortlaufend den Status der Entwicklungsarbeit im Terminalfenster.

Abbildung 2: Meteor dokumentiert fortlaufend den Status der Entwicklungsarbeit im Terminalfenster.

Das Webframework verarbeitet ausschließlich Programmcode in Javascript. Auf dem Server ist der mitgelieferte Webserver Node.js dafür zuständig [3], im Browser dessen Javascript-Engine. Meteor sortiert den Javascript-Code aus dem Projektverzeichnis selbsttätig.

Abbildung 3 zeigt die Zuordnung von Javascript-Dateien zur Server- (s) oder Client-seitigen (c) Ausführung am Verzeichnisbaum eines Projektverzeichnisses. Die Zahlen geben die Reihenfolge beim automatischen Laden an. Javascript-Dateien aus dem Verzeichnis »server« führt nur der Webserver aus, die in »client« nur der Browser. Alle anderen laufen auf beiden, ausgenommen sind die Ordner »public« (für Bilder und statische Dateien) und »private« (Dateien, die der Server, aber nicht der Browser lesen darf).

Abbildung 3: Meteor lädt Javascript-Code selbsttätig in den Webserver (s) oder den Browser (c).

Abbildung 3: Meteor lädt Javascript-Code selbsttätig in den Webserver (s) oder den Browser (c).

Darüber hinaus ermöglichen es die beiden booleschen Eigenschaften »Meteor.isServer« und »Meteor.isClient« in Listing 2, die Ausführung von Javascript-Code auf den Webserver oder den Browser zu beschränken. Das Beispiel stammt aus der Datei »main.js« aus dem Projektverzeichnis in Abbildung 3. Zeile 1 wird von Webserver und Javascript-Engine, Zeile 3 nur vom Webserver und Zeile 5 nur von der Javascript-Engine im Browser ausgeführt.

Listing 2

Kontextspezifische Ausführung

01 console.log('Beide Kontexte');
02 if (Meteor.isServer)
03   console.log('Webserver Kontext');
04 else if (Meteor.isClient)
05   console.log('Browser Kontext');

Datenhaltung

Meteor speichert Anwendungsdaten in Form von Javascript-Objekten, für Persistenz sorgt das freie NoSQL-Datenbanksystem Mongo DB. Der Datenbankserver liegt der Meteor-Distribution bereits bei und muss nicht zusätzlich installiert werden. Im Browser verfügt Meteor über eine stets aktuelle Kopie des Datenbestands der Mongo DB. Listing 3 zeigt die Speicherung eines Javascript-Objekts in der Datensammlung »Articles« unter Meteor. Zeile 1 öffnet die Datensammlung »Articles« und instanziert dabei ein Objekt vom Typ »Collection« . Zeile 2 speichert das leere Objekt »{}« in »Articles« durch Aufrufen der Methode »insert()« .

Listing 3

Zugriff per Collection-API

01 var Articles = new Meteor.Collection("articles");
02 Articles.insert({});

Reaktivität

Abbildung 4 macht anschaulich, wie sich das Hinzufügen eines Objekts zur Datensammlung »Articles« auswirkt. Den Anstoß gibt das Ausführen von Listing 3 im Browser unten links im Bild. Meteor überträgt das gespeicherte Objekt in Echtzeit in die Mongo DB auf dem Webserver und aktualisiert anschließend die Kopien der Anwendungsdaten in allen anderen Browsersitzungen. Anschließend reagieren in allen Browsern sämtliche Templates, die aus der Datensammlung »Articles« lesen: Sie kalkulieren sich automatisch neu und aktualisieren den DOM-Baum des Browsers.

Abbildung 4: Änderungen breiten sich lawinenartig und in Echtzeit über alle beteiligten Instanzen aus.

Abbildung 4: Änderungen breiten sich lawinenartig und in Echtzeit über alle beteiligten Instanzen aus.

Berechnet sich Programmcode selbsttätig neu, wenn sich die Daten ändern, von denen er abhängt, so spricht man von Reactive Programming [4]. Um Daten in Echtzeit auszutauschen, verbindet sich der Webserver jeweils über eine Websocket-Verbindung [5] mit den Browsern. Abbildung 5 zeigt die Auswirkung des reaktiven Verhaltens an der Beispielanwendung. Nach dem Speichern eines neuen Beitrags im rechten Browser erscheint der Beitrag selbsttätig im linken Browser.

Abbildung 5: Die Beispielanwendung bietet eine Artikelliste (links) und einen Editor (rechts).

Abbildung 5: Die Beispielanwendung bietet eine Artikelliste (links) und einen Editor (rechts).

Meteor-Pakete erweitern den Funktionsumfang einer Webanwendung. Der Befehl »meteor –list« listet in der Shell alle Pakete auf, die der Meteor-Distribution beiliegen. Der Befehl »meteor add Paketname« installiert ein Meteor-Paket, »meteor remove Paketname« entfernt es wieder. Pakete mit den Javascript-Bibliotheken Jquery, Underscore und Handlebars sind standardmäßig in Meteor-Anwendungen eingebunden.

Externe Pakete bindet der Versions- und Paketmanager Meteorite [6] ein. Er kann Pakete aus dem öffentlichen Verzeichnis unter https://atmosphere.meteor.com beziehen. Obwohl das Paketformat von Meteor noch nicht abschließend spezifiziert ist, beherbergt Atmosphere bereits rund 800 Pakete. Listing 4 bringt Meteorite auf ein Ubuntu-12.04-System. Zeile 1 installiert zunächst das erforderliche Softwarekontrollsystem Git mittels Apt. Anschließend nutzt Zeile 3 den Node.js-Paketmanager Npm aus der Meteor-Distribution, um Meteorite einzuspielen. Zuvor fügt Zeile 2 das Verzeichnis mit der Npm-Binärdatei der Umgebungsvariablen »PATH« hinzu. Schließlich installiert Meteorite mit dem Kommando »mrt« die Javascript-Bibliothek Moment aus dem gleichnamigen Meteor-Paket (Zeile 4).

Listing 4

Meteorite bindet externe Pakete ein

01 sudo apt-get install git
02 PATH=$PATH:~/.meteor/tools/0b2f28e18b/bin
03 npm install -g meteorite
04 mrt add moment

Beispielanwendung

In Abbildung 5 ist zweimal die Beispielanwendung zu sehen, die im Folgenden mit Meteor entsteht. Das recht schlichte Redaktionssystem Starpublisher zeigt im linken Browser eine Liste von Beiträgen samt Erstellungsdatum, Autor, Titel und Text. Die Beiträge sind absteigend nach dem Datum sortiert, sodass der neueste am oberen Ende steht. Ein Klick auf einen Beitrag öffnet ihn im Editor, wie ihn der rechte Browser darstellt. Dort aktualisiert die Schaltfläche »Speichern« den Beitrag, »Löschen« entfernt ihn. Der Menüpunkt »Hinzufügen« legt einen neuen Beitrag im Editor an.

Nur ein angemeldeter Benutzer kann einen Beitrag verfassen, und er allein kann ihn später überarbeiten oder löschen. Die Anmeldung erfolgt über den Menüpunkt »Sign In« , in Abbildung 6 rechts oben zu sehen. Zunächst erzeugt der Entwickler die Kernfunktionen zum Auflisten, Speichern und Löschen der Artikel. Das Menü und die Zugriffsverwaltung reicht er anschließend nach.

Abbildung 6: Über das Hauptmenü kann sich der Besucher anmelden sowie Beiträge auflisten, bearbeiten oder einen neuen Eintrag anlegen.

Abbildung 6: Über das Hauptmenü kann sich der Besucher anmelden sowie Beiträge auflisten, bearbeiten oder einen neuen Eintrag anlegen.

Listing 5 initialisiert den Zugriff auf die Datensammlung »Articles« . Dieser Javascript-Code findet in der Datei »model.js« im Wurzelverzeichnis des Projekts Platz, von wo ihn Webserver und Browser laden und ausführen. Das Template aus Listing 6 übernimmt die Darstellung der Beiträge aus der Datensammlung »Articles« im Browser. Es ist in der Template-Sprache Handlebars [7] abgefasst und in der Datei »template.html« im Verzeichnis »client« gespeichert. Zeile 2 ruft das Template »articles« (Zeilen 6 bis 15) auf, die Zeile 3 das »editor« -Template aus den Zeilen 17 bis 27. Vor der Darstellung im Browser ersetzt Handlebars die Template-Aufrufe mit deren Rückgabewerten.

Listing 6

client/template.html

01 <body>
02   {{> articles}}
03   {{> editor}}
04 </body>
05
06 <template name="articles">
07  <div id="articles">
08   {{#each articles}}
09   <article>
10     <span class="date">{{formatDate date}}</span>
11     <h2>{{title}}</h2>
12     <p>{{text}}</p>
13   </article>
14   {{/each}}
15 </template>
16
17 <template name="editor">
18  <div id="editor">
19   <input type="hidden" id="id" value=""/>
20   <h2>Title</h2>
21   <input type="text" id="title" value="">
22   <h2>Text</h2>
23   <textarea id="text"></textarea>
24   <button id="save">Speichern</button>
25   <button id="remove">Löschen</button>
26  </div>
27 </template>

Listing 5

Zugriff auf Anwendungsdaten

01 Articles = new Meteor.Collection("articles");

Template mit Schleife

Das Template »Articles« iteriert (Zeilen 8 bis 14) mittels »#each« über alle Beiträge, die in der Template-Variablen »articles« gespeichert sind. Pro Schleifendurchlauf gibt es einen Beitrag mit Datum, Titel und Text aus. Der Editor aus dem gleichnamigen Template speichert im Input-Element in Zeile 19 die aktuelle Beitragskennung, im Input-Element in Zeile 21 den Titel und im Textarea-Element in Zeile 23 den Lauftext. Die Knöpfe in den beiden folgenden Zeilen speichern oder löschen den Beitrag.

Der Javascript-Code aus Listing 7 versorgt das Template aus Listing 6 mit Daten und versieht es mit Interaktivität. Die Zeilen 1 bis 3 binden die Template-Variable »articles« aus Listing 6 an eine Funktion, in der die Methode »find()« in Zeile 2 alle Objekte der Datensammlung »Articles« ausliest. Das Objekt im zweiten Parameter des Aufrufs sortiert die Objekte über das Feld »date« absteigend.

Listing 7

client/client.js

01 Template.articles.articles = function() {
02  return Articles.find({}, {sort: {date: -1}});
03 }
04
05 Template.articles.helpers({
06  formatDate: function(date) {
07   return moment(date).format("lll");
08  }
09 });
10
11 Template.articles.events({
12  'click': function() {
13   var doc = Articles.findOne({_id: this._id});
14   load(doc._id, doc.title, doc.text);
15  }
16 });
17
18 Template.editor.events({
19  'click #save': function() {
20   var now = new Date();
21   Articles.upsert(val('id'), {title:val('title'), text:val('text'), date:now});
22   load();
23  },
24  'click #remove': function() {
25   Articles.remove(val('id'));
26   load();
27  }
28 });

Der Zugriff auf die Template-Variable »articles« aus Listing 6 geschieht intern über den von »find()« zurückgegeben Datenbankcursor. Ändert sich die Datensammlung, dann veranlasst der Cursor die Neuberechnung des Templates aus Listing 6, Zeilen 6 bis 15.

Die Zeilen 5 bis 9 erzeugen in Listing 7 die Hilfsfunktion »formatDate« , die Zeile 10 in Listing 6 aufruft. Sie formatiert ein Datumsobjekt mit Hilfe der in Listing 4 installierten Javascript-Bibliothek Moment als lesbare Zeichenkette. Der Aufruf der Methode »events()« in den Zeilen 11 bis 16 und 18 bis 28 von Listing 7 definiert die Ereignisbehandlung für ausgewählte HTML-Elemente durch Rückruffunktionen. Der Code übergibt den Aufrufen jeweils ein Objekt aus Schlüssel-Wert-Paaren.

Der Schlüssel »click« weist in Zeile 12 dem Klick auf einen Beitrag die Rückruffunktion aus den Zeilen 12 bis 15 zu. In Zeile 14 lädt die Funktion »load()« den Beitrag in den Editor, den zuvor Zeile 13 mittels »findOne()« anhand seiner ID ausgewählt und in der Variablen »doc« gespeichert hat.

Der Schlüssel »click #save« veranlasst in Zeile 19 beim Klick auf den Speichern-Knopf aus Listing 6 den Aufruf der Methode »upsert()« in der Rückruffunktion aus den Zeilen 19 bis 23. »upsert« aktualisiert bestehende Beiträge und legt sie ansonsten neu an.

Die Methode Upsert übernimmt die Beitragskennung und den Beitrag in Form eines Javascript-Objekts. Die benötigten Werte liest der Code mit der Funktion »val« aus. Beim Löschen entfernt die Rückruffunktion den Datensatz mit der Methode »remove()« (Zeilen 24 bis 27).

Zugriffskontrolle

Der Prototyp aus den Listings 5 bis 7 ermöglicht das Auflisten, Speichern und Löschen von Beiträgen, jedoch ohne jede Zugriffskontrolle. Bislang kann jeder Client alle Beiträge bearbeiten. Listing 8 bereitet die Beispielanwendung auf eine Zugriffskontrolle vor, indem es Meteor-Pakete entfernt und andere hinzufügt. Zeile 1 zwingt die Beispielanwendung dazu, nach der Deinstallation des Pakets »insecure« die Erlaubnis zum Speichern und Löschen von Datenbankinhalten explizit zu vergeben. Zeile 2 bewirkt, dass nach dem Entfernen des Pakets »autopublish« Datensammlungen zum Lesen explizit freigegeben werden. Die Zeilen 3 und 4 installieren mit den Paketen »accounts-password« und »accounts-ui« eine Benutzerverwaltung. Abbildung 7 zeigt die Anmeldung an der Anwendung.

Listing 8

Vorbereiten der Zugriffskontrolle

01 meteor remove insecure
02 meteor remove autopublish
03 meteor add accounts-password
04 meteor add accounts-ui
Abbildung 7: Anmeldung an der Beispielanwendung.

Abbildung 7: Anmeldung an der Beispielanwendung.

Lesen erlaubt

Listing 9 erlaubt es durch Aufrufen der Methode »publish()« (Zeilen 1 bis 3) auf dem Webserver dem Browser, alle Objekte aus der Datensammlung »Articles« zu lesen. Mit dem Ausdruck »return Articles.find({public:True})« könnte Zeile 2 auch nur eine Teilmenge der Objekte mit der Eigenschaft »Public=True« zugänglich machen. Der Code wird in der Datei »server.js« im gleichnamigen Verzeichnis gespeichert.

Listing 9

server/server.js

01 Meteor.publish("articles", function() {
02   return Articles.find();
03 });

Umgekehrt erlaubt der Ausdruck »Meteor.subscribe(“articles”);« dem Browser, die in Listing 9 veröffentlichen Daten zu lesen. Diese Zeilen fügt der Programmierer in die Datei »client/client.js« ein. Der Javascript-Code aus Listing 10 speichert und löscht Objekte der Datensammlung »Articles« gemäß der oben beschriebenen Zugriffskontrolle. Diese Codezeilen erweitern die Datei »model.js« . Der Aufruf der Methode »allow()« beschreibt (Zeilen 2 bis 5) die Ereignisbehandlung beim Aufruf von »remove()« : Nur wenn die Rückruffunktion den Wert »True« zurückgibt, wenn also der gegenwärtige Benutzer der Ersteller des Artikels ist, erlaubt ihm der Code den Beitrag zu löschen.

Listing 10

Zugriffskontrolle ./model.js

01 Articles.allow({
02   remove: function(userId, doc) {
03     return doc.owner === userId;
04   }
05 });
06
07 Meteor.methods({
08   upsert: function(id, title, text) {
09     if (!this.userId)
10       throw new Meteor.Error(403, 'Bitte erst anmelden');
11     var now = new Date();
12     var email = Meteor.user().emails[0].address
13     var doc = {owner: this.userId, title: title, text: text, date: now, email: email};
14     if (id) {
15       if (Articles.update({_id: id, owner: this.userId}, {$set: doc}) < 1)
16          throw new Meteor.Error(404, 'Zugriff nicht erlaubt');
17     } else {
18       Articles.insert(doc);
19     }
20   }
21 });

Bitte anmelden

Die Methode »methods()« initialisiert in Listing 7 (Zeile 21) auf dem Webserver die Funktion »upsert« . Listing 11 ruft sie in Zeile 4 wie eine gespeicherte Prozedur oder eine Remote Procedure auf. Die Funktion übernimmt Kennung, Titel und Text des Beitrags. Falls der Benutzer nicht angemeldet ist, brechen die Zeilen 9 bis 10 in Listing 10 den Funktionsaufruf mit einem Fehler ab.

Listing 11

client/client.js

01 Template.editor.events({
02   'click #save': function() {
03     error();
04     Meteor.call('upsert', val('id'), val('title'), val('text'), function(e) {
05       error(e.message);
06       nav();
07     });
08     nav(1);
09   },
10   'click #remove': function() {
11     Articles.remove(val('id'));
12     nav(1);
13   }
14 });

Zeile 13 konstruiert gleich anschließend das zu speichernde Objekt. Das Feld »owner« nimmt die Kennung aus der Benutzerverwaltung auf. Da nach der Deinstallation des Pakets »insecure« der direkte Aufruf der Methode »upsert()« nicht mehr zulässig ist, werden bestehende Artikel in Zeile 15 mittels »update()« aktualisiert oder in Zeile 18 mittels »insert()« neu eingefügt.

Der Aufruf von Update knüpft das Aktualisieren in Zeile 15 an zwei Bedingungen: »_id: id« schränkt die Aktualisierung auf den vorliegenden Beitrag ein und »owner: this.userId« auf dessen Ersteller. Im zweiten Aufrufparameter erzwingt der Bezeichner »$set« , das Objekt vollständig zu ersetzen. Erfolgt keine Aktualisierung, bewirkt der Rückgabewert 0 in Zeile 15, dass Meteor einen Fehler meldet.

Der Javascript-Code aus Listing 11 bringt die Funktion »upsert()« aus Listing 10 ins Spiel. Der Aufruf der Methode Events ersetzt den Aufruf aus Listing 7, Zeilen 18 bis 28. Die Rückruffunktion zum Ereignis Speichern (Zeilen 2 bis 9) ruft in Zeile 4 mit »call()« die Funktion »upsert()« auf dem Webserver auf. Call übernimmt im ersten Parameter den Funktionsnamen, die weiteren Parameter werden bis auf den letzten dem Server-seitigen Funktionsaufruf übergeben.

Der letzte Parameter enthält eine Rückruffunktion, die Meteor im Fehlerfall ausführt. Sie gibt in Zeile 5 eine Fehlermeldung im Editor aus. Anschließend schaltet der Aufruf von »nav()« in Zeile 6 in die Editoransicht. Mit dem Wert 1 steuert »nav()« in den Zeilen 8 und 12 die Listenansicht an.

Deployment

Damit ist die Anwendung auf dem lokalen Rechner fertiggestellt. Beim Übertragen auf den Webserver hilft Meteor ebenfalls. Ruft der Entwickler »meteor bundle starpublisher.tgz« im Wurzelverzeichnis des Projekts auf, erzeugt das Framework eine vollständige Node.js-Anwendung und speichert sie in der Archivdatei »starpublisher.tgz« .

Nach dem Entpacken auf dem Webserver benötigt die Anwendung lediglich eine Node.js-Instanz und einen Mongo-DB-Server. Eine Anleitung zur Inbetriebnahme der Anwendung enthält die mit ins Archiv gepackte Readme-Datei. Alternativ veröffentlicht der Befehl »meteor deploy starpublisher.meteor.com« die Beispielanwendung zu Demonstrationszwecken direkt unter der Webadresse http://starpublisher.meteor.com.

Stern am Firmament

Das Javascript-Framework Meteor erleichtert schon vor seiner Version 1.0 die Entwicklung von Webanwendungen. Es reduziert wesentlich den Aufwand beim Programmieren interaktiver Webanwendungen, wobei die Vielzahl vorgefertigter Meteor-Pakete eine wichtige Rolle spielt. Im Unterschied zu anderen Webframeworks wie etwa Ruby on Rails kommt Meteor auf Server und Client mit Javascript als der einzigen eingesetzten Programmiersprache aus.

Dank seines Protokolls zum Verteilen von Anwendungsdaten bringt Meteor zudem Echtzeitfähigkeit in die Anwendungen. Ebenso wie Rails mit Active Record bietet es einen kompakten Zugriff auf die Anwendungsdaten. Dabei arbeitet Meteor allerdings ausschließlich mit dem NoSQL-Datenbanksystem Mongo DB zusammen. (mhu)

Infos

  1. Meteor-Homepage: http://www.meteor.com
  2. Meteor-Dokumentation: http://docs.meteor.com
  3. Andreas Möller, “Schneller knoten”: Linux-Magazin 05/13, S. 84
  4. Reactive Programming: http://en.wikipedia.org/wiki/Reactive_programming
  5. Andreas Möller, “Blühende Oberflächen”: Linux-Magazin 01/12, S. 28
  6. Meteorite: https://github.com/oortcloud/meteorite/
  7. Handlebars: http://handlebarsjs.com
  8. Öffentliche Beispielinstallation: http://starpublisher.meteor.com
  9. Listings zum Artikel: https://www.linux-magazin.de/static/listings/magazin/2014/03/meteor/

Der Autor

Dipl.-Phys. Andreas Möller http://pamoller.com beschäftigt sich seit 2001 mit der Entwicklung Internet-basierter Software. Dazu zählen Datenbank- und Webanwendungen sowie Arbeiten auf dem Gebiet des Single Source Publishing. Zurzeit ist er als Berater und freier Autor tätig.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben