Meteor RSS reader

June 8th, 2013 | 10 min read | Handlebars.js, JavaScript, Meteor.js, Node.js

Enkele tutorials geleden heb ik uitgelegd hoe je met Handlebars.js templates een eenvoudige RSS reader kon maken. In deze tutorial ga ik hetzelfde verhaal herhalen met Meteor (en natuurlijk zit daar ook Handlebars.js in). In deze tutorial ga ik aan de hand van de vergelijking tussen gewoonweg JavaScript en een full-stack framework zoals Meteor tonen wat nu juist de voordelen zijn van Meteor.
Om niet alleen een vergelijkende tutorial te maken ga ik in deze tutorial ook tonen hoe je je files (server/client/…) kan splitsen zodat je niet alles in één bestand moet plaatsen.

Project opzetten

Beginnen doen we zoals gewoonlijk door een terminal te openen en het volgende commando uit te voeren:

meteor create

Omdat ik in deze tutorial gebruik ga maken van Twitter Bootstrap kan je dat ook al toevoegen, open hiervoor eerste de map van het project in je terminal en voor daarna volgend commando uit:

meteor add bootstrap

Daarna kan je het project uitvoeren met het volgende commando:

meteor

Maak nu in je project drie mappen aan, namelijk clientpublic en server. Plaats zowel het CSS- als het HTML bestand in de map client. Het JavaScript bestand mag je verwijderen en in plaats daarvan maak je een bestand client.js aan in de map client en een bestand server.js in de map server. Daarnaast maak je in de project-map zelf nog een bestand model.js aan. Als resultaat zou je nu onderstaande project structuur moeten over houden.

project-structure

Als volgende stap gaan we Node.js packages importeren in het Meteor project. Hiervoor gaan we via een terminal (in het huidige project) naar de map .meteor/local/build/server/node_modules. Dit doen we met het volgende commando:

cd .meteor/local/build/server/node_modules

Deze map is een link naar de algemene lib folder van Meteor. Hier kan je custom Node packages toevoegen via de Node.js packager (npm). Je moet hiervoor uiteraard wel Node.js hebben geinstalleerd. Indien je dat nog niet gedaan hebt raad ik je aan deze tutorial te volgen.
Om de juiste modules te importeren gebruik je volgende commandos:

npm install request
npm install feedparser

De kans bestaat dat je dit zal moeten uitvoeren als privileged user (

sudo

voor Ubuntu gebruikers).

Eenmaal klaar mag je terug naar je project folder navigeren met het commando:

cd ../../../../

Model

In deze tutorial gaan we gebruik maken van één collectie, namelijk een collectie voor de articles in de RSS feed. Omdat collecties zowel client- als server-side beschikbaar moeten zijn, moeten ze in een algemeen bestand staan dat op beiden uitgevoerd wordt. Alle bestanden in de project-map zelf worden op zowel client- als server uitgevoerd. Daarom gaan we dit in model.js plaatsen.

articles = new Meteor.Collection('articles');

Templates

De volgende stap is dat we de HTML templates uit de Handlebars.js tutorial een beetje gaan aanpassen en in het HTML bestand gaan plaatsen in de client map.
Als eerste stap geven we hier zowel de

<head />

als de

<body />

op met:

<head>
    <title>rss-reader-meteor-example</title>
</head>

<body>
    <div class="container">
        <header>
            <h1>Meteor - RSS Reader</h1>
        </header>
        {{> form}}
        {{> entries}}
    </div>
</body>

Zoals je kan zien spreken we hier twee templates aan, namelijk één voor de form en één voor de entries. Het is eenvoudiger om ook voor de form een template te maken omdat event handlers in Meteor ook aan een template gekoppeld worden.

De form template is vrij eenvoudig:

<template name="form">
    <form id="go-form" class="form-horizontal">
        <div class="controls-row">
            <input class="span10" id="feed" placeholder="e.g. https://dimitr.im/feed" type="text" />
            <button class="span2 btn" type="submit">
                Go!
            </button>
        </div>
    </form>
</template>

In tegenstelling tot de Handlebars tutorial ga ik in deze tutorial de error– en de entries template samen voegen in één template. Ik ga echter wel een aparte template voorzien voor elke entry.

<template name="entries">
    {{#if success}}
        {{#each entries}}
                {{> entry}}
        {{/each}}
    {{else}}
        <div class="alert alert-error">
            <strong>Oh snap!</strong> {{message}}
        </div>
    {{/if}}
</template>

Ten slotte hebben we nog de entry template:

<template name="entry">
    <article>
        <header>
            <h3>{{title}} <small>{{formatDate pubDate}}</small></h3>
        </header>
        {{{description}}}
        <footer>
            <a href="{{link}}">Lees meer &raquo;</a>
        </footer>
    </article>
    <hr />
</template>

We kunnen niet zoals bij de Handlebars tutorial een uitzondering maken voor het laatste artikel. De reden hiervoor is dat Meteor bij wijzigingen nooit de volledige collectie rendert. Stel dat er op de één of andere manier achteraan in de collectie een entry bij wordt gevoegd, dan gaat Meteor gewoon die laatste entry renderen. Dit heeft wel als gevolg dat de HTML van de andere entries niet meer aangepast wordt en waardoor het dus moeilijk wordt de laatste entry te bepalen.

RSS feed ophalen

Om de RSS feed op te halen gaan we als eerste stap het submit-event van de form template opvangen en de waarde van het invoerveld doorsturen naar de server om daar de RSS op te halen en te parsen via één van de Node.js modules die we eerder geïmporteerd hadden.

Om het submit-event op te vangen moet je volgende code in client.js plaatsen:

Template.form.events = {
    'submit' : function(evt) {
        evt.preventDefault();
        var feed = document.getElementById('feed').value;
        Session.set('feed', { feed: feed });
        Meteor.call('getFeed', feed, function(err) {
            Session.set('error', (err == null ? undefined : err));
        });

    }
};

Zoals je kan zien gebruiken we hier

Meteor.call()

om de server aan te spreken. We maken hier ook gebruik van

Session.set()

, dit zorgt ervoor dat we sessie (of client) afhankelijke data kunnen opslaan. By default zou Meteor namelijk alle artikels naar alle clients sturen terwijl dat helemaal niet nodig is. Enkel de clients die naar de juiste RSS feed zoeken moeten de artikels krijgen en geen andere clients.

In server.js moeten we nu een antwoord geven op de

Meteor.call()

met volgende code:

// NPM packages
var FeedParser = Npm.require('feedparser');
var Fiber = Npm.require("fibers");
var request = Npm.require("request");

Meteor.methods({
    'getFeed' : function(url) {
        try {
            request(url).pipe(new FeedParser()).on('error', function(error) {
                Fiber(function() {
                    throw new Meteor.Error(400, error.message);
                });
            }).on('article', function(article) {
                Fiber(function() {
                    articles.remove({
                        link : article.link,
                        feed: url
                    });
                    articles.insert({
                        title : article.title,
                        pubDate : new Date(article.date),
                        description : article.description,
                        link : article.link,
                        feed: url
                    });
                }).run();
            });
        }  catch (ex) {
            throw new Meteor.Error(400, ex.message);
        }
    }
});

Meteor.startup(function() {

});

Op de eerste lijnen kan je zien dat we Node.js packages importeren via

Npm.require()

.

Node package Beschrijving
feedparser Parser package voor RSS feeds
request Package die gebruikt wordt door feedparser om HTTP requests te kunnen maken en data te ontvangen
fibers Deze package zet asynchrone calls om naar synchrone calls. Dit is nodig omdat Node.js voornamelijk asynchroon werkt terwijl Meteor synchroon werken handiger vindt.

Daaronder zie je hoe we via

Meteor.methods()

een antwoord geven op de

Meteor.call()

uit de client. Deze methode bestaat uit het parsen van een RSS feed en het persisteren naar de articles collectie.

Parsen met de feedparser module doe je via:

request(url).pipe(new FeedParser())
.on('error', function(error) {
    ...
}).on('article', function(article) {
    ...
});

Omdat zowel de error– als de article fase via een callback (en dus asynchroon) werken, moeten we voor elke interactie met de Meteor API een synchrone call uitvoeren door gebruik te maken van een

Fiber()

.

De code in deze fibers is gewone Meteor code die we in eerdere tutorials al besproken hebben.

Collectie filteren

In de server-code hebben we alle feed items gepersisteerd naar een collectie articles. Nu moeten we uit deze collectie alle items halen die als feed attribuut de juiste RSS feed bevatten. Hiervoor heb ik een kleine helper-functie geschreven die in client.js terecht moet komen, namelijk:

var getFeed = function() {
    var myFeed = {};
    if (!Session.equals('feed', undefined)) {
        return Session.get('feed');
    }
    return myFeed;
}

Nu kunnen we de feed items ophalen en binden aan de juiste template via:

Template.entries.entries = function() {
    var myEntries = {};
    if (!Session.equals('feed', undefined)) {
        myEntries = articles.find(getFeed(), {
            sort : [["pubDate", "desc"]]
        });
    }
    return myEntries;
};

Zoals je kan zien halen we hier alle articles op als er een waarde voor feed te vinden is in de client sessie.

Afwerking

Ten slotte moeten we ook nog enkele dingen voorzien mocht het misgaan. In de template heb ik een waarde success en message voorzien die bepalen of de foutboodschap getoond moet worden of niet. De JavaScript code die je hiervoor in client.js moet plaatsen is:

Template.entries.success = function() {
    return Session.equals('error', undefined);
};
Template.entries.message = function() {
    return Session.get('error').reason;
};

Dan ten slotte heb ik ook nog een helper functie nodig om JavaScript

Date

objecten te formatteren. Dit doe ik via:

 Handlebars.registerHelper('formatDate', function(date) {
    return date.getDate() + "/" + (date.getMonth() + 1) + "/" + date.getFullYear();
});

Deze roep ik dan in m’n template aan via

{{formatDate pubDate}}

zoals nu al het geval is in de entries template.

Testen

Als we daarmee klaar zijn is het volledige project in orde. Het Meteor project kan je uitvoeren via je terminal met het commando:

meteor

Als je dan je browser opent en naar http://localhost:3000 gaat zal je merken dat de RSS feed applicatie er hetzelfde uitziet als deze uit de Handlebars tutorial.

result

We kunnen nu eender welke feed ingeven (ook cross-domain), de feed wordt immers server-side geparsed waardoor de cross-domain security policy die client-side in de browsers ingesteld is niet van toepassing is.

result-articles

Voeren we een foutieve URL in, dan krijgen we:

result-error

Zoals je kan zien is dit zeer gelijkaardig aan wat we eerder deden met JavaScript, toch zijn er enkele verschillen.

Het eerste verschil is dat de applicatie reactive is. Stel dat er iemand anders dezelfde RSS feed opzoekt en er zouden nieuwe RSS items beschikbaar zijn, dan worden deze automatisch gepusht naar alle andere clients die diezelfde RSS feed bekijken. Meteor is immers reactive, dit wilt zeggen dat het templates automatisch update bij wijzigingen in de achterliggende collecties.

Het tweede verschil is dat we zowel client– als server-side JavaScript code geschreven hebben. De server-side code wordt uitgevoerd op een Node.js server terwijl de client-side logica uitgevoerd wordt in de browser zelf.

Mocht de code van de applicatie ooit wijzigen dan zal ook de code naar alle clients gepusht worden. Er zal dus niemand met een oude versie kunnen blijven werken.

Het mooie van dit alles is dat je er uiteindelijk geen letter code voor hebt moeten schrijven om al deze voordelen te krijgen. Meteor is een platform dat by default reactive is, hot code pushes uitvoert en nog veel meer.

Download project

Download – rss-reader-meteor-example.tar.gz (2 kB)

Back to tutorialsContact me on TwitterDiscuss on Twitter

Profile picture

Dimitri "g00glen00b" Mestdagh is a consultant at Cronos and tech lead at Aquafin. Usually you can find him trying out new libraries and technologies. Loves both Java and JavaScript.