Andreas Bruns

Softwareentwicklung für Oldenburg und Bremen

Meteor – ein neuer Webframework-Stern

Als SW-Entwickler erlebt man bei der Nutzung von Web-Technologien immer mal wieder dieses Aha-Gefühl, sodass einem sofort deutlich wird: „Diese Technologie wird die Web-Entwicklung maßgeblich beeinflussen“. Die Programmiersprache, das Web-Framework oder die Denkweise, mit der man aktuell ein Web-Projekt umsetzt, wird in wenigen Jahren als schwergewichtig und veraltet angesehen werden.

Dieses Aha-Gefühl hatte ich gerade wieder, als ich die Meteor-Plattform ausprobiert hatte. Es scheint mir einen ähnlich wegweisenden Einfluss zu haben, wie andere bekannte Technologien:

Mit meiner kleinen Beispiel-Anwendung kann ein Anwender (passend zur Bundestagswahl 2013) seine Wahlstimme abgeben, welche Partei er wählt und einen entsprechenden Marker auf einer Karte in der Nähe seines Heimatortes setzen. Als clientseitige Standalone-Lösung läßt sich das einfach mit OpenLayers realisieren, allerdings sollen die Wahlstimmen serverseitig gespeichert werden und andere Clients automatisch benachrichtigt werden! Das Beispiel besteht gerade einmal aus 60 Zeilen HTML-Code und 160 Zeilen JavaScript-Code (davon 100 Zeilen für OpenLayers) !!! Ja, bei so wenig Code kann man sich drei Ausrufezeichen mal leisten.

Meteor: Wahlstimmen auf OpenStreetMap-Karte

Meteor: Wahlstimmen auf OpenStreetMap-Karte

Die Meteor-Plattform hat über 11 Millionen Dollar Risikokapital erhalten und wird von klugen Köpfen anderer bekannten Internet-Unternehmen unterstützt (XenSource, Reddit, Facebook, Heroku, GMail), sodass eine nachhaltige Weiterentwicklung von Meteor gewährleistet ist.

Bei Meteor handelt es sich um eine Plattform, die serverseitig Node.js als Webserver und MongoDB als Datenbank einsetzt. Die Daten werden per Websockets mit den Web-Clients (Browser) synchronisiert. Sich ändernde Daten werden automatisch zwischen dem Node.js-Server und allen Clients abgeglichen, sodass sich Entwickler „nur“ um den auszuführenden Programmcode bei Datenänderungen kümmern müssen (Reaktives Programmieren). Außerdem bietet Meteor Werkzeuge für Paket-Management, Build der Anwendung und Deployment der Anwendung.

Dann wollen wir Meteor (aktuelles Release: 0.6.5.1) mal installieren und starten. Unter MacOs und Linux genügen die folgenden Befehle und dann müsste mit dem Browser eine Webanwendung unter http://localhost:3000/ erreichbar sein.

bruno$ curl https://install.meteor.com | sh
bruno$ which meteor
/usr/local/bin/meteor
bruno$ meteor --version
Release 0.6.5.1
bruno$ meteor create example-meteor-osm
example-meteor-osm: created.
To run your new app:
   cd example-meteor-osm
   meteor
bruno$ cd example-meteor-osm
bruno$ meteor
[[[[[ /blog-examples/example-meteor-osm ]]]]]
Initializing mongo database... this may take a moment.
=> Meteor server running on: http://localhost:3000/

Mit ‚meteor help‚ kann man sich die Meteor-Befehle auf der Kommandozeile anzeigen lassen. Folgende Befehle nutze ich häufig:

– meteor help
– meteor help reset
– meteor create my-project
– meteor update
– meteor reset
– meteor list
– meteor list –using
– meteor deploy my-project.meteor.com

Änderungen an Dateien werden sofort von Meteor übernommen, sodass kein Neustart notwendig ist. Zur Trennung von clientseitigem und serverseitigem JavaScript-Code sollte man sich entsprechende Verzeichnisse (client bzw. server) anlegen. Gemeinsamer JavaScript-Code bleibt im Wurzelverzeichnis der Anwendung und statische Dateien des Webservers kommen in das Verzeichnis public.

Die folgenden Beispieldateien sorgen zunächst einmal dafür, dass Wahlstimmen abgegeben werden können und auf allen Clients angezeigt werden (ohne OpenStreetMap-Karte):

  • example-meteor-osm.html: HTML-Template-Datei, Template-Tags werden in JavaScript übersetzt
  • public/example-meteor-osm.css: CSS-Datei mit den Farben für die Parteien
  • model.js: Gemeinsames JavaScript für Client + Server, Anpassung der ‚create‘- und ‚update‘-Methoden
  • server/server.js: JavaScript des Servers legt initial Beispiel-Wahlstimmen an
  • client/client.js: JavaScript des Clients erweitert Template-Tags um JavaScript-Logik

Das bestehende Projekt kann nun um folgende Verzeichnisse, Dateien und den angegebenen Programmcode erweitert werden.

bruno$ mv example-meteor-osm.js model.js
bruno$ mkdir client
bruno$ touch client/client.js
bruno$ mkdir server
bruno$ touch server/server.js
bruno$ mkdir public
bruno$ mv example-meteor-osm.css public/
bruno$ tree
.
├── client
│   └── client.js
├── example-meteor-osm.html
├── model.js
├── public
│   └── example-meteor-osm.css
└── server
    └── server.js

HTML-Template: example-meteor-osm.html

<head>
  <title>example-meteor-osm</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <link rel="stylesheet" href="example-meteor-osm.css" type="text/css">
  <link rel="stylesheet" href="http://openlayers.org/dev/theme/default/style.css" type="text/css">
  <script src="http://openlayers.org/dev/OpenLayers.js"></script>
</head>

<body>
  {{> map}}
  {{> election}}
</body>

<template name="map">
  {{#constant}}
  <div id="map"/>
  {{/constant}}
</template>

<template name="election">
    <table id="votes">
      <tr>
        <th>
          <input type="text" class="name" name="name" placeholder="Mein Name" size="10" />
        </th>
        <th>
          <select class="party" name="party" size="1">
            <option value="CDU">CDU</option>
            <option value="SPD">SPD</option>
            <option value="Gruene">Grüne</option>
            <option value="FDP">FDP</option>
            <option value="Linke">Linke</option>
            <option value="Piraten">Piraten</option>
          </select>
        </th>
        <th>
          <select class="location" name="location" size="1">
            <option value="10;53.5">Hamburg</option>
            <option value="8.8;53.05">Bremen</option>
            <option value="13.4;52.5">Berlin</option>
            <option value="7;50.95">Köln</option>
            <option value="8.7;50.1">Frankfurt</option>
            <option value="12.4;51.35">Leipzig</option>
            <option value="11.5;48.15">München</option>
          </select>
          <input type="button" class="add" value="Wählen" />
        </th>
      </tr>
      {{#each votes}}
        {{> vote}}
      {{/each}}
    </table>
</template>

<template name="vote">
  <tr class="vote {{party}}">
    <td class="name">{{name}}</td>
    <td class="party">{{party}}</td>
    <td class="date">{{prettifyDate created}}</td>
  </tr>
</template>

JavaScript-Model für Client und Server: model.js

Votes = new Meteor.Collection("votes");
Votes.allow( { 
  insert: function(id, doc) { 
    doc.created = new Date();
    return true; 
  }, 
  update: function (id,doc) {
    return true; 
  }
});

JavaScript des Servers: server/server.js

Meteor.startup(function () {
  if (Votes.find().count() === 0) {
    var votes = [
      {name: "Stefan", party: "SPD", longitude: 8.8, latitude: 53.05},
      {name: "Claudia", party: "FDP", longitude: 11.5, latitude: 48.15 },
      {name: "Otto", party: "Piraten", longitude: 13.4, latitude: 52.5 },
      {name: "Melanie", party: "CDU", longitude: 8.7, latitude: 50.1 },
      {name: "Else", party: "Gruene", longitude: 10, latitude: 53.5 },
      {name: "Marvin", party: "SPD", longitude: 7, latitude: 50.95 },
      {name: "Michael", party: "Linke", longitude: 12.4, latitude: 51.35 }
    ];
    for (var i = 0; i < votes.length; i++) {
      Votes.insert({name: votes[i].name, party: votes[i].party, longitude: votes[i].longitude, latitude: votes[i].latitude, created: new Date()});
    }
  }
});

JavaScript des Clients: client/client.js

Template.election.votes = function () {
  return Votes.find({}, {sort: {created: -1}, limit: 10});
};

Template.election.events({
  'click input.add': function (event, template) {
    var name = template.find("th .name").value;
    var party = template.find("th .party").value;
    var deltaLongitude = ((Math.random() - 0.5) * 2.0);
    var longitude = parseFloat(template.find("th .location").value.split(";")[0]) + deltaLongitude;
    var deltaLatitude = ((Math.random() - 0.5) * 1.0);
    var latitude = parseFloat(template.find("th .location").value.split(";")[1]) + deltaLatitude;
    Votes.insert({name: name, party: party, longitude: longitude, latitude: latitude});
  }
});

Template.vote.prettifyDate = function(timestamp) {
  var prettifiedDate = "";
  if(typeof timestamp != "undefined") {
    var s = timestamp.getSeconds();
    var min = timestamp.getMinutes();
    var h = timestamp.getHours();
    var d = timestamp.getDate();
    var m = timestamp.getMonth() + 1;
    var y = timestamp.getFullYear();    
    prettifiedDate = d + '.' + m + '.' + y + '  ' + h + ':' + (min <= 9 ? '0' + min : min) + ':' + (s <= 9 ? '0' + s : s);
  };
  return prettifiedDate;
};

CSS des Clients: public/example-meteor-osm.css

html,body,#map {
  width: 100%;
  height: 100%;
  margin: 0;
}

#map {
    background: #ddd;
}

#votes {
    margin: 0;
    font-size: larger;
    background: #ddd;

    position:fixed;
    top:60px;
    left:60px;
    border:5px solid #777777;
    width: 400px;
    z-index: 1000;
}

#votes th input {
    margin: 10px 5px;
}

#votes th input.name {
    width: 90%;
}

#votes td {
    padding: 10px;
}

#votes td.name {
    width: 120px;
    max-width: 120px;
}

#votes td.party {
    width: 60px;
}

#votes td.date {
    width: 220px;
    text-align: right;
}

.CDU {
    background: #aaa;
}

.FDP {
    background: #ff4;
}

.SPD {
    background: #f44;
}

.Gruene {
    background: #4f4;
}

.Linke {
    background: #d4d;
}

.Piraten {
    background: #fa4;
}

Wenn man Wahlstimmen über das Formular anlegt, sollte sich die Liste aktualisieren und die Aktualisierung sollte auch in weiteren Browser-Reitern bzw. anderen Browsern erfolgen. Außerdem kann man in der JavaScript-Konsole auch direkt Wahlstimmen-Daten manipulieren:

console.log("Anzahl Votes: " + Votes.find().fetch().length);
Votes.insert({name: "Stefan", party: "FDP", longitude: 10, latitude: 50.95});
console.log("Anzahl Votes: " + Votes.find().fetch().length);
var vote = Votes.find().fetch()[Votes.find().fetch().length-1];
console.log(vote);
Votes.update({_id:vote._id}, {$set: {party:"CDU" }});
var vote = Votes.find().fetch()[Votes.find().fetch().length-1];
console.log(vote);
Meteor: Wahlstimmen-Übersicht

Meteor: Wahlstimmen-Übersicht

Wenn das alles funktioniert, dann benötigen wir noch ein bisschen OpenLayers-JavaScript, damit eine Karte mit den passenden Markern (Beispiel: OpenLayers-Marker) angezeigt wird. Meteor bietet Reactivity und Live-HTML für solche JavaScript-Aktualisierungen. Der folgende Code muss einfach in der Datei ‚client/client.js‚ angehängt werden und wir sehen eine OpenStreetMap-Karte mit Markern.

var osmMap = null;
var votesVectorLayer = null;
var proj4326 = null;
var projmerc = null;
var partyColorMap = null;

Template.map.rendered = function () {
  if (!osmMap) {
    Deps.autorun(function () {
      var newest = Votes.findOne({}, {sort: {created: -1}, limit: 1});
      if (newest) {
        Session.set("lastVote", newest);
      }
    });

    proj4326 = new OpenLayers.Projection("EPSG:4326");
    projmerc = new OpenLayers.Projection("EPSG:900913");
    partyColorMap = {"CDU":"#aaa", "SPD":"#f44", "Gruene": "#4f4", "FDP":"#ff4", "Linke": "#d4d", "Piraten": "#fa4"};

    var mapCenterPositionAsLonLat = new OpenLayers.LonLat(7, 51.3);
    var mapCenterPositionAsMercator = mapCenterPositionAsLonLat.transform(proj4326, projmerc);
    var mapZoom = 6;
 
    osmMap = new OpenLayers.Map("map", {
       controls: [
          new OpenLayers.Control.KeyboardDefaults(),
          new OpenLayers.Control.Navigation(),
          new OpenLayers.Control.LayerSwitcher({'ascending':false}),
          new OpenLayers.Control.PanZoomBar(),
          new OpenLayers.Control.MousePosition()
        ],
        maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34, 20037508.34, 20037508.34),
        numZoomLevels: 18,
        maxResolution: 156543,
        units: 'm',
        projection: projmerc,
        displayProjection: proj4326
    } );
 
    var osmLayer = new OpenLayers.Layer.OSM("OpenStreetMap");
    osmMap.addLayer(osmLayer);
    votesVectorLayer = new OpenLayers.Layer.Vector("Votes Layer", {
                styleMap: new OpenLayers.StyleMap({
                    pointRadius: 10,
                    fillColor: "${color}"
                })});
    osmMap.addLayer(votesVectorLayer);
    osmMap.setCenter(mapCenterPositionAsMercator, mapZoom);

    selectControl = new OpenLayers.Control.SelectFeature(votesVectorLayer);
    osmMap.addControl(selectControl);
    selectControl.activate();

    votesVectorLayer.events.on({
      'featureselected' : function(evt) {
        feature = evt.feature;
        popup = new OpenLayers.Popup.FramedCloud("featurePopup",
          feature.geometry.getBounds().getCenterLonLat(),
            new OpenLayers.Size(100, 100), "<h2>"
            + feature.attributes.title + "</h2>"
            + feature.attributes.description, null, true,
          function (evt) { selectControl.unselect(this.feature); });
        feature.popup = popup;
        popup.feature = feature;
        osmMap.addPopup(popup);
      },

      'featureunselected' : function(evt) {
        feature = evt.feature;
        if (feature.popup) {
          popup.feature = null;
          osmMap.removePopup(feature.popup);
          feature.popup.destroy();
          feature.popup = null;
        }
      }
    });
  }

   Votes.find().forEach(function (vote) {
     createOsmVote(vote);
   });

  var self = this;
  if (! self.handle) {
    self.handle = Deps.autorun( function(){
      var vote = Session.get("lastVote");
      if (vote != null) {
        createOsmVote(vote);
      };
    });
  }
};

function createOsmVote(vote) {
     var votePoint = new OpenLayers.Geometry.Point(vote.longitude, vote.latitude).transform(proj4326, projmerc);
     var voteAttributes = {
        title: vote.party, 
        description: ("<b>" + vote.name + ": </b>" + Template.vote.prettifyDate(vote.created)),
        color: partyColorMap[vote.party]
      };
     var voteFeature = new OpenLayers.Feature.Vector(votePoint, voteAttributes);
     votesVectorLayer.addFeatures(voteFeature);
};

Mit ‚meteor deploy example-meteor-osm.meteor.com‚ wird die Webanwendung in der Meteor-Cloud deployt und ist unter http://example-meteor-osm.meteor.com erreichbar. Der gesamte Programmcode ist unter GitHub verfügbar: https://github.com/me4bruno/blog-examples -> example-meteor-osm). Wenn man für seine Webanwendung eine Synchronisation zwischen Server und allen Browsern benötigt, gibt es wohl gerade nichts Einfacheres als Meteor. Viel Spaß beim Experimentieren 😉

Kommentare sind geschlossen.