Discover Meteor

Building Real-Time JavaScript Web Apps

Introduzione

1

Facciamo un esperimento concettuale assieme. Immagina di aprire la stessa cartella in due finestre separate del tuo computer.

Ora cancella un documento da una delle due finestre. Il documento è scomparso anche nell'altra finestra?

Non serve che tu faccia veramente quanto detto sopra per sapere che è scomparso. Quando modifichiamo qualcosa nel nostro filesystem locale, il cambiamento viene applicato ovunque senza la necessità di refresh o callbacks. É così.

Ora prova a pensare come lo stesso scenario si presenterebbe sul web. Ad esempio, diciamo che hai aperto la pagina di amministrazione dello stesso sito WordPress in due browser diversi e creato un nuovo post da uno dei due. Diversamente da quel che succede sul filesystem, non importa quanto attendi, sull'altra finestra non vedrai i cambiamenti fin quando non ricaricherai la pagina deliberatamente.

Nel corso degli anni, ci siamo fatti l'idea che un sito web è un qualcosa con cui si comunica solamente in brevi e separate riprese.

Ma Meteor è parte di una nuova classe di framework e tecnologie che stanno cercando di mettere in discussione lo status quo rendendo il web real-time e reattivo.

Che cosa è Meteor?

Meteor è una piattaforma basata su Node.js che permette di costruire applicazioni web real-time. È la parte che sta tra il database della tua app e la sua interfaccia utente e garantisce che entrambe le cose siano mantenute sincronizzate.

Visto che si basa su Node.js, Meteor utilizza JavaScript sia lato client che lato server. In più, Meteor permette di condividere codice tra le due parti.

Il risultato complessivo è una piattaforma che riesce ad essere molto potente e veramente semplice astraendo e nascondendo molte delle scocciature e insidie caratteristiche dello sviluppo per il web.

Perché Meteor?

Dunque perché dovresti investire tempo per imparare Meteor anziché un altro framework per il web? Lasciando da parte tutte le varie caratteristiche di Meteor, crediamo ci sia un'unica ragione: Meteor è semplice da imparare.

Più di ogni altro framework, Meteor permette di avere un'applicazione web pronta e funzionante in poche ore. E se in passato hai già fatto sviluppo di front-end, sarai già pratico di JavaScript e non dovrai nemmeno imparare un nuovo linguaggio.

Meteor potrebbe essere il framework ideale per le tue necessità, come potrebbe non esserlo. Ma visto che puoi fartene una buona idea nel corso di poche serate o di un fine settimana, perché non provarlo e scoprirlo di persona?

Perché Questo Libro?

Durante gli ultimi 6 mesi, abbiamo lavorato a Telescope, un'applicazione Meteor, open-source, che permette a chiunque di creare il proprio sito di notizie (pensa a Reddit oppure Hacker news), in cui le persone possono inviare link e votarli.

Abbiamo imparato un'esagerazione di cose costruendo l'applicazione, ma non sempre è stato facile trovare risposte alle nostre domande. Abbiamo dovuto mettere assieme cose trovate da diverse fonti e in molti casi persino inventare nostre soluzioni. Con questo libro, abbiamo voluto condividere tutte queste lezioni e creare una semplice guida passo passo che ti accompagni nella costruzione di una applicazione Meteor completa partendo da zero.

L'app che costruiremo è una versione un po’ semplificata di Telescope, che chiamiamo Microscope. Durante il suo sviluppo, considereremo tutti gli aspetti necessari a costruire un'applicazione Meteor, come gli account utente, le collezioni Meteor, il routing e molto altro.

Quando avrai finito di leggere il libro, se vorrai andare oltre, sarai in grado di capire senza sforzo il codice di Telescope perché rispecchia la stessa struttura.

Riguardo Gli Autori

Nel caso tu ti stia chiedendo chi siamo e per che motivo dovresti fidarti di noi, ti diamo qualche dettaglio in più su entrambi.

Tom Coleman è parte di Percolate Studio, la cui attività di sviluppo web è focalizzata sulla qualità e l'esperienza utente. È anche uno dei creatori di Meteorite e il repository di pacchetti Atmosphere, ed è anche impegnato in molti altri progetti open-source di Meteor (come il Router).

Sacha Greif ha lavorato in startup come Hipmunk e RubyMotion come designer di prodotto e web. È il creatore di Telescope e Sidebar (che è basato su Telescope), ed è anche il fondatore di Folyo.

Capitoli & Approfondimenti

Abbiamo fatto in modo che questo libro possa risultare utile sia per i novizi di Meteor che per i programmatori più esperti, per questo abbiamo separato i capitoli in due categorie: capitoli regolari (numerati da 1 a 14) e approfondimenti ( mezzi numeri, .5).

I capitoli regolari ti guideranno nella costruzione dell'applicazione, cercando di renderti operativo il più velocemente possibile spiegandoti i passaggi più importanti senza però farti perdere il filo con troppi dettagli.

D'altra parte, gli approfondimenti andranno più nel dettaglio riguardo i meandri di Meteor, aiutandoti a capire meglio cosa succede veramente dietro le quinte.

Quindi, se sei un principiante, alla prima lettura salta pure gli approfondimenti, e riprendili più avanti quando avrai fatto un po’ di pratica con Meteor.

Commit & Istanze Live

Non c'è nulla di peggio che seguire un libro di programmazione e all'improvviso realizzare che il tuo codice non è più sincronizzato con gli esempi e nulla funziona più come dovrebbe.

Per evitarlo, abbiamo predisposto un repository di Microscope su GitHub e forniremo collegamenti diretti ai commit di git ogni poche modifiche al codice. In più, ogni commit è anche collegato ad un'istanza funzionante dell'applicazione relativa al commit stesso, in modo che tu possa confrontarla con la tua copia locale. Questo è un esempio di come compariranno:

Commit 11-2

Display notifications in the header.

In ogni caso, il fatto che mettiamo a disposizione i commit git non vuol dire che tu debba semplicemente saltare da un git checkout all'altro. Imparerai molto meglio prendendoti il tempo di scrivere a mano il codice della tua applicazione!

Altre Risorse

Se volessi approfondire di più qualche aspetto particolare di Meteor, il miglior punto da cui iniziare è la guida ufficiale di Meteor.

Suggeriamo anche Stack Overflow per domande e possibili soluzioni a problemi, e il canale IRC #meteor se ti serve aiuto immediato.

Mi serve Git?

Anche se per seguire questo libro non è strettamente necessario avere familiarità con il sistema Git di controllo versione, lo raccomandiamo vivamente.

Se vuoi fartene un'idea velocemente, raccomandiamo la pagina Git Is Simpler Than You Think di Nick Farina.

Se sei un novizio di Git, ti suggeriamo anche l'applicazione GitHub for Mac, che ti permette di clonare e gestire i repository senza utilizzare la linea di comando.

Come Metterci in Contatto

Primi Passi

2

Le prime impressioni sono importanti, e l'installazione di Meteor dovrebbe essere indolore. Nella maggior parte dei casi, sarai operativo in meno di 5 minuti.

Per cominciare, possiamo installare Meteor aprendo una finestra della riga di comando e scrivere:

$ curl https://install.meteor.com | sh

Questo comando installerà l'eseguibile meteor nel tuo sistema e sarai pronto per usare Meteor.

Non Installare Meteor

Se non puoi (o non vuoi) installare Meteor localmente, ti suggeriamo di dare un'occhiata a Nitrous.io.

Nitrous.io è un servizio che ti permette di eseguire applicazioni e editare il loro codice direttamente dal tuo browser: abbiamo scritto una breve guida per aiutarti ad utilizzarlo.

Puoi seguire la guida anche solo fino alla sezione “Installing Meteor & Meteorite” (inclusa), dopodiché tornare a seguire il libro ripartendo dalla sezione “Creare una semplice App” di questo capitolo.

Meteorite

Dato che Meteor non supporta ancora pacchetti di terze parti nativamente, Tom Coleman (uno degli autori di questo libro) e alcuni membri della comunità hanno creato Meteorite, un wrapper per Meteor. Meteorite si prende anche cura di installare Meteor per te collegandolo a qualsiasi pacchetto tu possa trovare.

Visto che ci appoggeremo a pacchetti di terze parti per alcune delle funzionalità di Microscope, installiamo Meteorite.

Installare Meteorite

Dovrai assicurarti che node e git siano installati sulla tua macchina. Installali nel modo standard tipico del tuo SO, oppure prova questi link:

Dopodiché, installa Meteorite. Siccome è un eseguibile di npm (Node Packaged Module, formato standard dei moduli di Node), lo installiamo con il seguente comando:

$ npm install -g meteorite

Problemi con i Permessi?

Su alcune macchine potresti aver bisogno dei permessi di root per installare Meteorite. Per evitare problemi, assicurati di usare sudo -H:

$ sudo -H npm install -g meteorite

Puoi leggere di più riguardo questo argomento nella documentazione di Meteorite.

Tutto qua! Da qui in poi Meteorite gestirà tutto.

Nota: al momento non c'è ancora supporto per Meteorite in Windows, ma puoi dare un'occhiata al nostro tutorial per windows.

mrt vs meteor

Meteorite installa l'eseguibile mrt, che useremo per installare pacchetti dentro la nostra applicazione. Quando invece vogliamo eseguire il nostro server, useremo l'eseguibile meteor.

Creare una semplice App

Ora che abbiamo installato Meteorite, creiamo una applicazione. Per farlo, utilizziamo il comando mrt di Meteorite da riga di comando:

$ mrt create microscope

Questo comando scaricherà Meteor e imposterà per te un semplice e basilare progetto Meteor pronto all'uso. Quando ha terminato, dovresti vedere la cartella, microscope/, con all'interno quanto segue:

microscope.css
microscope.html
microscope.js
smart.json

L'applicazione che Meteor ha creato per te è un semplice modello standard di applicazione che dimostra alcuni semplici schemi.

Anche se la nostra applicazione non fa granché, possiamo lanciarla. Per eseguirla, torna al tuo terminale e digita:

$ cd microscope
$ meteor

Ora apri un browser e vai all'indirizzo http://localhost:3000/ (o l'equivalente http://0.0.0.0:3000/) e dovresti vedere qualcosa come questo:

Meteor's Hello World.
Meteor’s Hello World.

Commit 2-1

Created basic microscope project.

Congratulazioni! Hai eseguito la tua prima applicazione Meteor. A proposito, tutto quello che devi fare per fermarla è tornare alla finestra del terminale da dove l'hai lanciata e premere ctrl+c.

Aggiungere un Pacchetto

Ora useremo Meteorite per aggiungere un pacchetto smart che ci permetterà di includere Bootstrap nel nostro progetto:

$ mrt add bootstrap

Commit 2-2

Added bootstrap package.

Una Considerazione sui Pacchetti

Quando si parla di pacchetti nel contesto di Meteor, è meglio essere precisi. Meteor utilizza cinque tipi base di pacchetti:

  • Il nucleo stesso di Meteor è diviso in diversi core packages. Questi vengono inclusi in ogni applicazione Meteor, e non dovrai quasi mai preoccupartene.
  • Gli smart packages di Meteor sono un gruppo di circa 37 pacchetti (puoi ottenere la lista completa con meteor list) che vengono forniti assieme a Meteor e che puoi importare facoltativamente dentro la tua applicazione. Puoi aggiungerli anche se non usi Meteorite, usando il comando meteor add nomepacchetto.
  • I Local packages (pacchetti locali) sono pacchetti personalizzati che puoi creare tu stesso e mettere nella cartella /packages. Per usarli non serve nemmeno Meteorite.
  • Gli Atmosphere smart packages sono pacchetti Meteor di terze parti la cui lista si trova su Atmosphere. Per importarli ed utilizzarli è necessario Meteorite.
  • Gli NPM packages (Node Packaged Modules) sono pacchetti di Node.js. Nonostante non siano pacchetti pronti all'uso per Meteor, possono essere utilizzati dai precedenti tipi di pacchetti.

La Struttura Dei File Di Una Applicazione Meteor

Prima di cominciare a scrivere codice, dobbiamo organizzare il nostro progetto in maniera adeguata. Per assicurarsi di avere una struttura pulita, aprite la cartella microscope e cancellate microscope.html, microscope.js, e microscope.css.

Di seguito create cinque cartelle all'interno di /microscope: /client, /server, /public, /lib, e /collections, e create due file vuoti, main.html e main.js, all'interno di /client. Non preoccupatevi se questo compromette temporaneamente la vostra applicazione, inizieremo a riempirli nel prossimo capitolo.

È necessario notare che alcune di queste cartelle sono speciali. Quando si tratta di struttura dei file, Meteor ha alcune regole:

  • Il codice nella cartella /server viene eseguito solo sul server.
  • Il codice nella cartella /client viene eseguito solo sul client.
  • Tutto il resto viene eseguito sia sul client che sul server.
  • I file nella cartella /lib vengono caricati prima di ogni altra cosa.
  • I file main.* sono caricati dopo ogni altra cosa.
  • Le risorse statiche (font, immagini, ecc.) vanno messe nella cartella /public.

È bene notare che, anche se Meteor ha queste regole, non vi obbliga ad usare una struttura predefinita per la vostra applicazione se non ne avete intenzione. Perciò la struttura che vi suggeriamo è semplicemente il nostro modo di procedere, non una regola scolpita nella roccia.

Vi invitiamo a controllare la documentazione ufficiale di Meteor se volete maggiori dettagli su questo argomento.

Meteor è un MVC?

Se ti stai avvicinando a Meteor venendo da altri framework come Ruby on Rails, ti starai forse chiedendo se le applicazioni Meteor adottano lo schema MVC (Model View Controller).

La risposta breve è no. Diversamente da Rails, Meteor non impone nessuna struttura predefinita alla tua applicazione. Quindi in questo libro organizzeremo il codice nel modo che secondo noi ha più senso, senza preoccuparci più di tanto degli acronimi.

Nessuna Cartella public?

OK, abbiamo mentito. Non ci serve la cartella public/ per la semplice ragione che Microscope non usa nessun asset statico! Ma visto che la maggior parte delle applicazioni Meteor includeranno almeno un paio di immagini, abbiamo pensato fosse importante parlare anche di questo.

A proposito, potresti anche notare una cartella nascosta chiamata .meteor. Questo è il posto dove Meteor salva il proprio codice: modificare cose lì dentro è di solito una gran brutta idea. Infatti, in realtà non ti servirà mai guardare dentro questa cartella. L'unica eccezione è per i file .meteor/packages e .meteor/release, che sono utilizzati rispettivamente per elencare i tuoi smart packages e la versione in uso di Meteor. Quando aggiungi pacchetti o cambi versione di Meteor, può essere utile controllare le modifiche a questi file.

Underscores vs CamelCase

L'unica cosa che possiamo dire a proposito dell'ormai storico dibattito tra underscore (my_variable) e camelCase (myVariable) è che non è importante quale modalità si scelga, l'importante è che una volta scelta la vostra preferita vi atteniate a pieno ad essa.

In questo libro utilizziamo il camelCase perché è lo standard per quanto riguarda JavaScript (dopotutto si chiama JavaScript, non java_script!).

Le uniche eccezioni a questa regola sono i nomi dei file, che utilizzeranno gli underscore (my_file.js), e le classi CSS, che useranno il trattino (.my-class). Il motivo di queste scelte è che nel filesystem gli underscore sono utilizzati frequentemente, mentre la sintassi CSS utilizza già i trattini (font-family, text-align, ecc.).

Prendersi Cura dei CSS

Questo libro non tratta di CSS. Quindi per non rallentarti con dettagli relativi agli stili, abbiamo deciso di rendere disponibile fin da subito lo stylesheet completo, di modo che tu non debba più preoccupartene.

I file CSS vengono caricati e minificati automaticamente da Meteor, quindi diversamente da altri asset statici vanno messi dentro /client, e non dentro /public. Continua e crea ora la cartella client/stylesheets/ e metti al suo interno questo file style.css:

.grid-block, .main, .post, .comments li, .comment-form {
    background: #fff;
    border-radius: 3px;
    padding: 10px;
    margin-bottom: 10px;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
body {
    background: #eee;
    color: #666666;
}
.navbar { margin-bottom: 10px }
.navbar .navbar-inner {
    border-radius: 0px 0px 3px 3px;
}
#spinner { height: 300px }
.post {
    *zoom: 1;
    -webkit-transition: all 300ms 0ms;
    -webkit-transition-delay: ease-in;
    -moz-transition: all 300ms 0ms ease-in;
    -o-transition: all 300ms 0ms ease-in;
    transition: all 300ms 0ms ease-in;
    position: relative;
    opacity: 1;
}
.post:before, .post:after {
    content: "";
    display: table;
}
.post:after { clear: both }
.post.invisible { opacity: 0 }
.post .upvote {
    display: block;
    margin: 7px 12px 0 0;
    float: left;
}
.post .post-content { float: left }
.post .post-content h3 {
    margin: 0;
    line-height: 1.4;
    font-size: 18px;
}
.post .post-content h3 a {
    display: inline-block;
    margin-right: 5px;
}
.post .post-content h3 span {
    font-weight: normal;
    font-size: 14px;
    display: inline-block;
    color: #aaaaaa;
}
.post .post-content p { margin: 0 }
.post .discuss {
    display: block;
    float: right;
    margin-top: 7px;
}
.comments {
    list-style-type: none;
    margin: 0;
}
.comments li h4 {
    font-size: 16px;
    margin: 0;
}
.comments li h4 .date {
    font-size: 12px;
    font-weight: normal;
}
.comments li h4 a { font-size: 12px }
.comments li p:last-child { margin-bottom: 0 }
.dropdown-menu span {
    display: block;
    padding: 3px 20px;
    clear: both;
    line-height: 20px;
    color: #bbb;
    white-space: nowrap;
}
.load-more {
    display: block;
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.05);
    text-align: center;
    height: 60px;
    line-height: 60px;
    margin-bottom: 10px;
}
.load-more:hover {
    text-decoration: none;
    background: rgba(0, 0, 0, 0.1);
}
client/stylesheets/style.css

Commit 2-3

Re-arranged file structure.

Una Considerazione su CoffeeScript

All'interno di questo libro scriveremo codice in puro JavaScript. Ma se preferisci CoffeeScript, Meteor ha ciò che ti serve. Aggiungi semplicemente il pacchetto CoffeeScript e sarai pronto a partire:

mrt add coffeescript

Deployment

Sidebar 2.5

Alcune persone preferiscono lavorare silenziosamente su di un progetto finché non è perfetto, mentre altre non vedono l'ora di farlo vedere al mondo intero.

Se appartenete alla prima categoria di persone, e per ora preferite sviluppare in locale, sentitevi liberi di saltare questo capitolo. Diversamente, se volete prendervi un po’ di tempo per imparare come fare il deploy (trasferire il progetto funzionante su di un server remoto) online della la vostra applicazione Meteor, abbiamo la soluzione per te.

Impareremo come fare il deploy di un'applicazione Meteor in diversi modi. Sentitevi liberi di utilizzare ciascuno di esse in ogni momento del vostro processo di sviluppo, sia che stiate lavorando a Microscope che a qualsiasi altra app Meteor. Iniziamo!

Introduzione agli Approfondimenti

Questo è un capitolo di approfondimento. Gli approfondimenti forniscono uno sguardo più approfondito ad argomenti di Meteor più generali, in modo indipendente dal resto del libro.

Se preferite continuare a costruire Microscope, potete quindi saltare temporaneamente questo capitolo e tornare indietro più avanti.

Fare il Deploy Su Meteor

Fare il deploy su di un sotto-dominio di Meteor (ad esempio http://myapp.meteor.com) è la soluzione più semplice, e la prima che che vogliamo presentare. Questo approccio può essere utile per presentare la vostra applicazione ad altri durante i primissimi sviluppi, oppure per configurare velocemente un server di pre-produzione.

Fare il deploy sul server di Meteor è piuttosto semplice. Aprite semplicemente il vostro terminale, spostatevi nella cartella della vostra applicazione Meteor, e scrivete:

$ meteor deploy myapp.meteor.com

Ovviamente dovrete sostituire “myapp” con un nome di vostra scelta e preferibilmente con uno che non sia già in uso. Se è la prima volta che fate il deploy di un'app, vi sarà chiesto di creare un account Meteor. Se tutto va a buon fine, dopo alcuni secondi potrete accedere alla vostra applicazione all'indirizzo http://myapp.meteor.com.

Potete far riferimento alla documentazione ufficiale per avere maggiori informazioni rguardo a cose come accedere direttamente al database dell'istanza della vostra applicazione o su come configurare un dominio personalizzato.

Fare il Deploy Su Modulus

Modulus è un'opzione fantastica per fare il deploy di applicazioni NodeJS. È anche uno dei pochi provider PaaS (platform-as-a-service) che supportano Meteor ufficialmente, e ci sono già molte persone che ci fanno girare applicazioni Meteor in produzione.

Demeteorizer

Modulus ha rilasciato uno strumento open-source chiamato demeteorizer che converte la vostra applicazione Meteor in un'applicazione NodeJS standard.

Iniziamo creando un account. Per fare il deploy della vostra app su Modulus, dovete poi installare gli strumenti di Modulus per la riga di comando:

$ npm install -g modulus

Dopodiché dovete autenticarvi con:

$ modulus login

Creeremo ora un progetto di Modulus (nota: è possibile farlo anche dal pannello di controllo web di Modulus):

$ modulus project create

Dovrete poi creare un database MongoDB per la vostra app. Potete anche creare un database MongoDB con Modulus stesso, MongoHQ o con qualsiasi altro provider MongoDB nel cloud.

Una volta creato il vostro database MongoDB, potete ottenere il MONGO_URL per il vostro database dalla UI web di Modulus (andate in Dashboard > Databases > Select your database > Administration), e utilizzatelo poi per configurare la vostra app in questo modo:

$ modulus env set MONGO_URL "mongodb://<user>:<pass>@mongo.onmodulus.net:27017/<database_name>"

È giunto il momento di fare il deploy della vostra app. Non potrebbe essere più semplice, scrivete:

$ modulus deploy

Avrete così fatto con successo il deploy della vostra applicazione su Modulus. Fate riferimento alla documentazione di Modulus per avere maggiori informazioni riguardo l'accesso ai log, l'impostazione di un nome di dominio personalizzato e l'accesso con SSL.

Meteor Up

Nonostante ogni giorno appaiano nuove soluzioni per il cloud, spesso queste presentano problemi e limitazioni. Ad oggi, fare il deploy di applicazioni Meteor sui propri server rimane la soluzione migliore per andare in produzione. L'unica cosa è che gestire un deploy da soli non è così semplice, in particolar modo se siete interessati a fare una distribuzione di qualità.

Meteor Up (o mup in breve) è un altro tentativo per risolvere questo problema: è uno strumento per la riga di comando che si cura di tutte le impostazioni e di fare il deploy. Vediamo quindi come fare il deploy di Microscope utilizzando Meteor Up.

Prima di qualsiasi altra cosa, avremo bisogno di un server su cui inviare l'applicazione. Suggeriamo, indifferentemente, Digital Ocean, con tariffe che partono da 5$ al mese, o AWS, che mette a disposizione gratuitamente istanze Micro (incontrerete presto problemi di scalabilità, ma se avete intenzione di fare giusto qualche prova con Meteor Up dovrebbe essere sufficiente).

Qualsiasi sia il servizio scelto, dovrete annotare le tre seguenti cose: l'indirizzo IP del vostro server, il nome utente (di solito root o ubuntu) e la password per l'accesso. Tenetele da qualche parte ben al sicuro, ci serviranno presto!

Inizializzare Meteor Up

Per iniziare, dobbiamo installare Meteor Up utilizzando npm come segue:

$ npm install -g mup

Creeremo poi una speciale cartella separata che conterrà le nostre impostazioni di Meteor Up per un particolare deploy. Usiamo una cartella separata per due ragioni: primo, è buona pratica evitare di includere qualsiasi credenziale privata nel vostro repository Git, in particolare se state lavorando ad un progetto pubblico.

Secondo, utilizzando più cartelle separate potremo gestire più configurazioni di Meteor Up in parallelo. Questo risulterà utile, ad esempio, per gestire diversamente istanze di pre-produzione e di produzione.

Creiamo quindi questa nuova cartella ed utilizziamola per inizializzare un nuovo progetto di Meteor Up:

$ mkdir ~/microscope-deploy
$ cd ~/microscope-deploy
$ mup init

Condivisione tramite Dropbox

Un modo fantastico per assicurarvi che voi e il vostro team utilizziate tutti le stesse impostazioni di deploy è semplicemente creare la vostra cartella di configurazione per Meteor Up dentro al vostro Dropbox, o qualsiasi servizio simile.

Configurare Meteor Up

Quando si inizializza un nuovo progetto, Meteor Up crea per voi due file: mup.json e settings.json.

mup.json conterrà tutte le vostre impostazioni per il deploy, mentre settings.json conterrà tutte le impostazioni relative all'applicazione (chiavi OAuth, chiavi per analytics, etc.)

Il passo successivo è la configurazione del file mup.json. Di seguito è riportato il file mup.json generato di default con mup init, e tutto quello che dovrete fare è riempire gli spazi vuoti:

{
  //server authentication info
  "servers": [{
    "host": "hostname",
    "username": "root",
    "password": "password"
    //or pem file (ssh based authentication)
    //"pem": "~/.ssh/id_rsa"
  }],

  //install MongoDB in the server
  "setupMongo": true,

  //location of app (local directory)
  "app": "/path/to/the/app",

  //configure environmental
  "env": {
    "ROOT_URL": "http://supersite.com"
  }
}
mup.json

Passiamo in rassegna ognuna di queste impostazioni.

Autenticazione sul Server

Noterete che Meteor Up supporta l'autenticazione basata sia su password che su chiave privata (PEM), e può quindi essere utilizzato con praticamente qualsiasi servizio cloud.

Nota importante: se scegliete di utilizzare l'autenticazione basata su password, assicuratevi di aver prima installato sshpass (fate riferimento a questa guida).

Configurazione di MongoDB

Il prossimo passo è la configurazione del database MongoDB per la vostra applicazione. Vi suggeriamo di utilizzare MongoHQ, o qualsiasi altro fornitore cloud di MongoDB, visto che offrono un servizio di supporto professionale a strumenti di gestione migliori.

Se decidete di utilizzare MongoHQ, impostate setupMongo a false e aggiungete la variabile d'ambiente MONGO_URL nel blocco env del file mup.json’s. Se invece decidete di installare MongoDB sul vostro server con Meteor Up, impostate setupMongo a true e Meteor Up si preoccuperà del resto.

Il Percorso della App Meteor

Visto che la nostra configurazione di Meteor Up si trova in una cartella diversa, dovremo puntare Meteor Up alla nostra applicazione utilizzando la proprietà app. Digitate semplicemente il percorso locale completo, che potete anche ottenere utilizzando il comando pwd dal terminale quando vi trovate all'interno della cartella della vostra applicazione.

Variabili d'Ambiente

Potete specificare tutte la variabili d'ambiente per la vostra applicazione (come ROOT_URL, MAIL_URL, MONGO_URL, etc.) all'interno del blocco env.

Impostazioni e Deploy

Prima di poter fare il deploy, avremo bisogno di impostare il server in modo che sia pronto per ospitare applicazioni Meteor. La magia di Meteor Up racchiude questo processo complesso in un singolo comando!

$ mup setup

Potranno servire alcuni minuti, a seconda delle performance del server e della connettività di rete. Una volta che il comando avrà completato con successo l'impostazione del server, potremo finalmente fare il deploy della nostra app lanciando il comando:

$ mup deploy

Questo impacchetterà l'applicazione Meteor e ne farà il deploy sul server che abbiamo appena impostato.

Visualizzare i Log

I resoconti di log sono molto importanti e Meteor Up fornisce un modo per gestirli molto semplice, emulando il comando tail -f. Digitate:

$ mup logs -f

Questo completa la nostra panoramica su quello che Meteor Up può fare. Per avere maggiori informazioni, vi suggeriamo di visitare il repository di Meteor Up su GitHub.

Questi tre modi di fare il deploy di applicazioni Meteor dovrebbero essere sufficienti per la maggior parte dei casi d'uso. Chiaramente sappiamo che alcuni preferirebbero avere il pieno controllo del proprio server Meteor impostandolo partendo da zero. Ma questo è argomento per un altro giorno… o forse per un altro libro!

Templates

3

Per rendere semplice l'apprendimento dello sviluppo in Meteor, adotteremo un approccio dall'esterno. In altre parole realizzeremo prima una semplice struttura in HTML/JavaScript e la integreremo con le funzionalità dell'applicazione solo in un secondo momento.

Ciò significa che in questo capitolo ci preoccuperemo solamente di ciò che accade all'interno della cartella /client.

Creiamo un nuovo file chiamato main.html all'interno della cartella /client e inseriamoci il seguente codice:

<head>
  <title>Microscope</title>
</head>
<body>
  <div class="container">
    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="/">Microscope</a>
      </div>
    </header>
    <div id="main" class="row-fluid">
      {{> postsList}}
    </div>
  </div>
</body>
client/main.html

Questo sarà il template principale della nostra applicazione. Come potete notare si tratta di semplice HTML ad eccezione del tag {{> postsList}}, che rappresenta il punto di inserimento del template postsList che vedremo a breve. Per ora creiamo altri 2 template.

Template in Meteor

Alla base, un sito di notizie social è composto di post organizzati in liste, e questo è esattamente il modo in cui andremo ad organizzare i nostri template.

Creiamo un cartella /views all'interno della cartella /client. In questa cartella andremo a posizionare tutti i nostri template e per mantenere la struttura ordinata creiamo una cartella /posts all'interno di /views solo per i template relativi ai post.

Come trovare i file

Meteor è grandioso a trovare i file. Non importa dove e come mettete il codice all'interno della cartella /client, Meteor lo troverà e lo compilerà correttamente. Questo significa che non dovrete mai inserire manualmente i riferimenti ai file JavaScript e CSS.

Questo significa anche che potete inserire tutti i vostri file nella stessa cartella, o anche tutto il vostro codice in un solo file, ma visto che Meteor compilerà tutto in un singolo file minificato, è meglio tenere tutto ben organizzato in una struttura file ordinata.

Creiamo finalmente il nostro secondo template. Dentro client/views/posts, creiamo posts_list.html:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

E post_item.html:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
  </div>
</template>
client/views/posts/post_item.html

Notate l'attributo name="postsList" dell'elemento template. Questo è il nome che viene utilizzato da Meteor per tenere traccia di dove vanno inseriti i vari template.

È il momento di introdurre il sistema di template di Meteor, Spacebars. Spacebars è semplice HTML, con l'aggiunta di tre particolarità: parziali, espressioni e blocchi di controllo.

I parziali usano la sintassi {{> templateName}}, e dicono semplicemente a Meteor di sostituire il parziale con il template con lo stesso nome (nel nostro caso postItem).

Le espressioni come {{title}} possono richiamare sia una proprietà dell'oggetto attuale, o il valore restituito da un helper del template come definito nel manager del template attuale (parleremo meglio più avanti di questo).

Infine, i blocchi di controllo sono tag speciali che controllano il flusso del template come {{#each}}…{{/each}} o {{#if}}…{{/if}}.

Più informazioni

Potete far riferimento alla documentazione di Spacebars se volete imparare di più su Spacebars.

Armati di queste nuove conoscenze, possiamo facilmente capire cosa sta succedendo.

Come prima cosa nel template postsList, stiamo iterando su un oggetto posts grazie al blocco di controllo {{#each}}…{{/each}}. Successivamente, per ogni iterazione, includiamo il template postItem.

Da dove arriva questo oggetto posts? Buona domanda. Si tratta di un helper di template che definiremo più avanti quando di occuperemo dei manager di template.

Il template postItem è abbastanza lineare. Usa solo tre espressioni: {{url}} e {{title}} ritornano le proprietà del documento, mentre {{domain}} chiama un helper del template.

Abbiamo nominato molte volte gli “helper del template” in questo capitolo senza in realtà spiegare come funzionano. Prima di poter riparare a questa mancanza dobbiamo parlare dei manager.

Manager dei Template

Fino ad ora abbiamo lavorato con Spacebars, che non è altro che puro HTML con l'aggiunta di qualche tag speciale. Contrariamente ad altri linguaggi come PHP (o anche normali pagine HTML, che possono includere JavaScript), Meteor tiene i template e la logica separati, e i template da soli non sono sufficienti.

Per prendere vita, un template ha bisogno di un manager. Potete immaginare un manager come uno chef che prende ingredienti grezzi (i vostri dati) e li prepara, prima di consegnare il piatto pronto al cameriere (il template) che ve lo presenta una volta pronto.

In altre parole, mentre il ruolo del template è limitato al mostrare o iterare su delle variabili, il manager è colui che si occupa del lavoro duro, assegnando un valore ad ogni variabile.

Manager?

Quando abbiamo chiesto ad altri sviluppatori Meteor come avrebbero chiamato i manager di template, metà ha risposto “controller”, e metà “quei file dove metto il mio codice JavaScript”.

I manager non sono esattamente controller (per lo meno non nel senso di controller MVC) e “QFDMIMCJ” non è certo un acronimo accattivante, così abbiamo accantonato entrambe le proposte.

Dato che avevamo comunque bisogno di indicare in modo comprensibile ciò di cui stavamo parlando, abbiamo optato per chiamarli “manager”, termine che non ha nessun particolare significato all'interno di altri framework web e quindi più comodo per rappresentare qualcosa di nuovo.

Per mantenere una certa semplicità, useremo la convenzione di nominare i manager in base al template, con la differenza di avere l'estensione .js. Creiamo quindi posts_list.js all'interno di /client/views/posts e iniziamo a scrivere il nostro primo manager:

var postsData = [
  {
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  },
  {
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  },
  {
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  }
];
Template.postsList.helpers({
  posts: postsData
});
client/views/posts/posts_list.js

Se avete fatto correttamente, dovreste vedere qualcosa di simile nel vostro browser:

Il nostro primo template con dati statici
Il nostro primo template con dati statici

Commit 3-1

Aggiunto template di base della lista dei post e alcuni d…

In questo codice stiamo facendo due cose. Come prima cosa stiamo inserendo dati di esempio come schema di base nell'array postsData. Questi dati normalmente verrebbero dal database ma dal momento che non abbiamo ancora visto come fare (lo faremo nel prossimo capitolo) stiamo “bluffando” utilizzando dei dati statici.

In secondo luogo, stiamo utilizzando la funzione di Meteor Template.myTemplate.helpers() per definire un helper del template chiamato posts che semplicemente ci restituisce l'array postsData.

Definendo l'helper posts, lo rendiamo disponibile al nostro template:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

In questo modo, il template potrà iterare sull'array postsData ed inviare ogni oggetto contenuto al suo interno al template postItem.

Il valore di “this”

Creiamo ora il manager post_item.js:

Template.postItem.helpers({
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js

Commit 3-2

Setup a `domain` helper on the `postItem`.

Questa volta il valore del nostro helper domain non è un array, ma una funzione anonima. Questo pattern è molto più diffuso (e utile) se paragonato al nostro precedente esempio con semplici dati statici.

Visualizzazione del dominio di ogni link.
Visualizzazione del dominio di ogni link.

L'helper domain riceve un URL e ne restituisce il dominio tramite un po’ di magia in JavaScript. Ma da dove proviene questo URL?

Per rispondere a questa domanda dobbiamo tornare al nostro template posts_list.html. Il blocco di controllo {{#each}} non solo itera sul nostro array ma definisce il valore di this sull'oggetto iterato all'interno del blocco.

Questo significa che all'interno dei tag {{#each}}, ogni post è assegnato al valore di this ad ogni iterazione, e si estende automaticamente all'interno del manager del template incluso (post_item.js).

Ora possiamo capire perché this.url restituisce l'URL dell'attuale post. E inoltre, se usiamo {{title}} e {{url}} all'interno del nostro template post_item.html, Meteor sa che si tratta in realtà di this.title e this.url e ci restituisce i valori corretti.

JavaScript Magic

Sebbene non sia proprio di Meteor, di seguito trovate una veloce spiegazione del precedente pizzico di “magia JavaScript”. Come prima cosa, creiamo un elemento HTML vuoto, (a), e lo teniamo in memoria.

Definiamo poi il valore del suo attributo href tramite il valore dell'URL del post attuale (come abbiamo appena visto, in un helper this è l'oggetto sul quale stiamo al momento operando).

Infine sfruttiamo la proprietà speciale dell'elemento a, hostname, per recuperare il nome del dominio del link senza il resto dell'URL.

Se avete seguito i passaggi correttamente, dovreste vedere una lista di post nel vostro browser. Questa lista è formata solo da dati statici, perciò non si avvale ancora delle caratteristiche in tempo reale di Meteor. Vi mostreremo come modificare questa ‘mancanza’ nel prossimo capitolo!

Ricaricamento del codice

Vi sarete forse accorti che non dovete nemmeno ricaricare il vostro browser ogni volta che modificate un file.

Questo avviene perché Meteor traccia tutti i file all'interno della cartella del progetto e ricarica automaticamente il browser ogni volta che si accorge di una modifica in uno qualsiasi di essi.

Il ricaricamento del codice di Meteor è piuttosto intelligente, infatti mantiene lo stato della vostra applicazione anche nel mezzo di due ricaricamenti!

Utilizzare Git & GitHub

Sidebar 3.5

GitHub è un deposito di stampo social per progetti open-source che si basa sul sistema di controllo versione Git, il cui obiettivo principale è quello di rendere semplice il condividere codice e collaborare su progetti. È anche un gran strumento per l'apprendimento. In questo approfondimento, daremo un rapido sguardo su alcuni modi in cui potete utilizzare GitHub per seguire Discover Meteor.

Questo approfondimento presume che non conosciate molto Git e GitHub. Se invece li utilizzate già entrambi, sentitevi liberi di passare direttamente al prossimo capitolo!

I Commit

La componente di base dell'uso di un repository Git è un commit. Si può pensare ad un commit come a un'istantanea che fotografa lo stato del vostro codice in un dato momento nel tempo.

Invece di dare semplicemente il codice completo di Microscope, abbiamo preso queste istantanee ad ogni passaggio della lavorazione, e le abbiamo rese tutte disponibili su GitHub.

Per esempio, questo è quello che si può vedere nell’ultimo commit dello scorso capitolo:

A Git commit as shown on GitHub.
A Git commit as shown on GitHub.

Quello che si vede qui sotto è un “diff” (sta per “differenza”) del file post_item.js, in altre parole i cambiamenti inseriti in questo commit. In questo caso abbiamo creato il file post_item.js da zero, perciò tutti i suoi contenuti sono evidenziati in verde.

Facciamo un confronto con un esempio preso un po’ più avanti nel libro:

Modifying code.
Modifying code.

Questa volta solo le righe che sono state modificate sono evidenziate in verde.

Ovviamente, qualche volts non vengono aggiunte o modificate delle righe di codice, ma vengono eliminate:

Deleting code.
Deleting code.

Questo è il principale utilizzo di GitHub: poter vedere cosa è cambiato con una sola occhiata.

Come curiosare il codice di un Commit

La visualizzazione di un commit di Git mostra i cambiamenti inclusi in quel commit, ma a volte si ha bisogno di controllare file che non hanno subito cambiamenti, per sincerarsi che il loro contenuto sia esattamente quello che ci si aspetta.

Ancora una volta ci si può avvalere dell'aiuto di GitHub. Quando ci si trova sulla pagina di un commit, cliccare il bottone Browse code:

The Browse code button.
The Browse code button.

Si ha ora accesso alla repository così come è in quel commit:

The repository at commit 3-2.
The repository at commit 3-2.

GitHub non offre indicazioni chiare a livello visivo che si sta guardando un commit, ma si può impostare una comparazione con il master “normale”, e vedere a colpo d'occhio che la struttura dei file è differente:

The repository at commit 14-2.
The repository at commit 14-2.

Accedere ad un Commit in locale

Si è appena visto come curiosare nell'intero codice di un commit direttamente su GitHub. Come si può fare la stessa cosa sulla propria macchina? Ad esempio potrebbe essere utile visualizzare la situazione dell'applicazione in un determinato commit sulla propria macchina, per poter verificare il funzionamento a quel punto della lavorazione.

Per poterlo fare, si deve procedere all'utilizzo dell'interfaccia di git a linea di comando dal terminale. Per chi non ha mai utilizzato questo strumento bisogna prima installare Git. In seguito bisogna clonare (cioè, scaricare una copia locale) il repository di Microscope con questo comando:

$ git clone git@github.com:DiscoverMeteor/Microscope.git github_microscope

La dicitura github_microscope in fondo al comando serve a dare il nome della cartella locale dove si vuole copiare l'applicazione. Assumendo che sia già presente una cartella microscope, si può scegliere un qualsiasi altro nome (non è necessario che sia lo stesso della repository presente su GitHub).

Una volta entrati nel repository (con il comando cd), si può iniziare ad utilizzare l'interfaccia a linea di comando:

$ cd github_microscope

Clonando il repository da GitHub, si è scaricato tutto il codice dell'applicazione, il che significa che quello che si vede è il codice del commit finale.

Fortunatamente c'è un modo per tornare indietro nel tempo e fare “checkout” di uno specifico commit senza che ciò abbia ripercursioni sugli altri. Nel terminale digitare:

$ git checkout chapter3-1
Note: checking out 'chapter3-1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at a004b56... Added basic posts list template and static data.

Git informa che si è ora in uno stato denominato “detached HEAD”, che significa che fino a che Git rimane in questo stato è possibile osservare i vecchi commit ma non modificarli. Si può pensare a un mago che guarda il passato da una sfera di cristallo.

(Si noti che Git ha anche un comandi che permettono di cambiare commit precedenti. Questo è più paragonabile a un viaggiatore nel tempo che per caso calpesta una farfalla, ma è al di là di quello che vogliamo trattare in questo libro.)

Il motivo per il quale è bastato digitare chapter3-1 è perché abbiamo nominato in precedenza tutti i commit di Microscope con un corretto indicatore (tag) del capitolo. Se non l'avessimo fatto si sarebbe dovuto prima trovare l’hash del commit, cioè il suò identificatore univoco.

Ancora una volta GitHub rende la vita più semplice. È possibile recuperare l'hash del commit nell'angolo in basso a destra dell'header box blu del commit come mostrato di seguito:

Finding a commit hash.
Finding a commit hash.

Si usi ora un hash al posto di un tag:

$ git checkout c7af59e425cd4e17c20cf99e51c8cd78f82c9932
Previous HEAD position was a004b56... Added basic posts list template and static data.
HEAD is now at c7af59e... Augmented the postsList route to take a limit

Infine, come smettere di guardare nella sfera di cristallo e tornare al presente? Si dice a Git di fare checkout del branch master:

$ git checkout master

Notate che potete anche lanciare l'applicazione con il comando meteor in qualsiasi momento del processo, anche se siente nello stato “detached HEAD”. Poterbbe essere che sia necessario lanciare prima mrt update se Meteor si lamenta di alcuni pacchetti mancanti, questo perché il codice dei pacchetti non è incluso nel repository Git di Microscope.

Modalità History

Un altro scenario comune in Git è questo: guardando un file si notano alcuni cambiamenti mai visti prima. Il problema è quello di capire quando il file è stato modificato. Si potrebbe scorrere ad uno ad uno tutti i commit fino a trovare quello giusto, ma c'è un modo più semplice grazie alla modalità History di GitHub.

Da uno dei file del repository di GitHub, si cerchi il bottone “History”:

GitHub's History button.
GitHub’s History button.

Si può vedere un lista completa dei commit in cui è stato modificato il file in questione:

Displaying a file's history.
Displaying a file’s history.

Modalità Blame

Per concludere, si veda la modalità Blame:

GitHub's Blame button.
GitHub’s Blame button.

Questa visualizzazione mostra linea per linea chi ha modificato il file e in quale commit (in altre parole, chi bisogna redarguire - blame - quando le cose non funzionano come ci si aspetta):

GitHub's Blame view.
GitHub’s Blame view.

Git è uno strumento abbastanza complesso - così come GitHub -, non possiamo sperare di poter spiegare tutti in un solo capitolo. Infatti, abbiamo solo visto molto in superficie cosa sia possibile fare con questi strumenti. Speriamo che anche queste poche nozioni si rivelino utili mentre si prosegue nel resto del libro.

Collezioni

4

Nel capitolo uno, abbiamo parlato della caratteristica principale di Meteor: la sincronizzazione automatica dei dati tra client e server.

In questo capitolo, analizzeremo più nel dettaglio questo meccanismo, osservando il funzionamento della tecnologia chiave che lo permette: la collezione (collection) di Meteor.

Stiamo costruendo un'applicazione sociale di notizie, e la prima cosa che vogliamo fare è creare una lista di link che le persone hanno inserito. Chiameremo “post” ognuno di questi oggetti.

Naturalmente, dovremo salvare questi posts da qualche parte. Meteor installa anche Mongo che viene eseguito sul vostro server e funge da base di dati persistente.

Dunque, anche se il browser dell'utente può contenere alcune informazioni di stato (ad esempio la pagina corrente, o il commento che sta scrivendo in quel momento), il server, e più precisamente Mongo, contiene la sorgente di dati permanente canonica. Canonica significa che è la stessa per tutti gli utenti: ogni utente può essere su di una pagina diversa, ma la lista originale dei post è la stessa per tutti.

Questi dati sono salvati in Meteor all'interno di una Collezione. Una collezione è una struttura dati speciale che, per mezzo di pubblicazioni e sottoscrizioni, si prende cura della sincronizzazione in tempo reale dei dati tra il database Mongo e ogni browser degli utenti connessi, in entrambe le direzioni. Vediamo come.

Vogliamo che i post siano permanenti e condivisi tra gli utenti, inizieremo quindi creando una collezione chiamata Posts dentro cui salvarli. Se non lo avete ancora fatto, create la cartella collections/ dentro la cartella principale della vostra applicazione, e dentro essa il file posts.js. Quindi aggiungete:

Posts = new Meteor.Collection('posts');
collections/posts.js

Commit 4-1

Added a posts collection

Il codice che non si trova all'interno delle cartelle client/ e server/ verrà eseguito in entrambi i contesti. Per questo motivo la collezione Posts risulterà disponibile sia lato client che lato server. Comunque, il ruolo della collezione in ognuno dei due ambienti è molto diverso.

Usare Var O Non Usare Var?

In Meteor, la parola chiave var limita la visibilità di un oggetto al file corrente. Vogliamo rendere la collezione Posts visibile a tutta l'applicazione, motivo per cui la stiamo omettendo dalla sua dichiarazione.

Sul server la collezione ha il compito di parlare con il database Mongo, leggendo e scrivendo qualsiasi cambiamento. Da questo punto di vista, può essere paragonata ad una libreria standard per database. Sul client, diversamente, la collezione è una copia sicura di un sottoinsieme della reale collezione canonica. La collezione lato client è mantenuta costantemente aggiornata, in modo quasi del tutto trasparente, con quel sottoinsieme di dati in tempo reale.

Console vs Console vs Console

In questo capitolo inizieremo a fare uso della console del browser, che non va confusa con il terminale o la shell di Mongo. Di seguito proponiamo una breve guida per ognuna di esse.

Terminale

Il Terminale
Il Terminale
  • Eseguito dal tuo sistema operativo.
  • console.log() eseguito lato server produce output qui.
  • Prompt: $.
  • Conosciuto anche come: Shell, Bash.

Console del Browser

La Console del Browser
La Console del Browser
  • Richiamata dall'interno del browser, esegue codice JavaScript.
  • console.log() eseguito lato client produce output qui.
  • Prompt: .
  • Conosciuta anche come: Console JavaScript, Console dei DevTools

Shell di Mongo

La Shell di Mongo
La Shell di Mongo
  • Lanciata dal Terminale con meteor mongo o mrt mongo.
  • Permette l'accesso diretto al database dell'applicazione.
  • Prompt: >.
  • Conosciuta anche come: Console di Mongo.

Notiamo che, per tutte, non è necessario digitare il carattere del prompt ($, , o >) come parte dei comandi. Possiamo anche assumere che ogni riga che non inizi con il prompt sia il risultato del comando precedente.

Collezioni Lato Server

Sul server, la collezione funge da API per il database Mongo. Questo permette, per il codice lato server, di scrivere comandi Mongo come Posts.insert() oppure Posts.update(): questi modificheranno la collezione posts in Mongo.

Per guardare all'interno del database Mongo, aprite una seconda finestra del terminale (mentre meteor è ancora in esecuzione nella prima) e spostatevi nella cartella della vostra app. Eseguite quindi il comando meteor mongo per lanciare la shell di Mongo: al suo interno potete digitare i comandi standard di Mongo (e come usuale, potete terminarla con la combinazione di tasti ctrl+c). Per fare un esempio, inseriamo un nuovo post:

> db.posts.insert({title: "A new post"});

> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
La Shell di Mongo

Mongo su Meteor.com

Se ospitate la vostra applicazione su *.meteor.com, potete comunque accedere alla shell di mongo dell'applicazione funzionante con meteor mongo myApp.

E, tanto che ci siamo, potete anche ottenere i log dell'applicazione scrivendo meteor logs myApp.

La sintassi di Mongo è familiare, visto che utilizza un'interfaccia JavaScript. Non faremo altre manipolazioni di dati dalla shell di Mongo, ma potremo ritornarci di tanto in tanto giusto per assicurarci che i dati siano presenti.

Collezioni Lato Client

Le collezioni sono ancora più interessanti viste dal lato client. Quando dichiariamo Posts = new Meteor.Collection('posts'); sul client, stiamo creando una copia locale nella cache del browser della reale collezione in Mongo. Quando diciamo che una collezione lato client è una “cache”, vogliamo dire che contiene un sottoinsieme dei dati ai quali offre un accesso molto rapido.

È importante capire bene questo concetto perché risulta fondamentale al funzionamento di Meteor. In generale, una collezione lato client consiste in un sottoinsieme di tutti i documenti salvati all'interno della collezione Mongo (dopo tutto, generalmente non vogliamo mandare tutto il database al client).

Secondariamente, questi documenti sono salvati nella memoria del browser, il che significa che accedervi è praticamente istantaneo. Quando eseguiamo Posts.find() sul client per caricare i dati, non ci sono quindi comunicazioni lente con il server o il database, perché i dati sono già pre-caricati.

Introduzione a MiniMongo

L'implementazione Meteor di Mongo per il lato client viene chiamata MiniMongo. Non è ancora un'implementazione perfetta e, occasionalmente, potrete scoprire che alcune funzionalità di Mongo non funzionano in MiniMongo. Nonostante ciò, tutte le funzionalità che copriremo con questo libro funzionano allo stesso modo sia in Mongo che MiniMongo.

Comunicazione Client-Server

La chiave di volta di tutto ciò sta nel meccanismo che la collezione lato client utilizza per sincronizzare i suoi dati con la collezione lato server che ha lo stesso nome ( in questo caso 'posts').

Anziché spiegarlo nei dettagli, guardiamo semplicemente cosa succede.

Iniziamo aprendo due finestre del browser e accediamo alla console del browser in entrambe. Fatto questo, apriamo una shell di Mongo dalla riga di comando. A questo punto dovremmo vedere in tutti e tre i contesti il solo documento che abbiamo creato in precedenza.

> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
La Shell di Mongo
 Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
La console del primo browser

Creiamo un nuovo post. In una delle finestre del browser eseguiamo un inserimento:

 Posts.find().count();
1
 Posts.insert({title: "A second post"});
'xxx'
 Posts.find().count();
2
La console del primo browser

Come ci si poteva aspettare, il post è stato inserito nella collezione locale. Ora controlliamo Mongo:

❯ db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
La Shell di Mongo

Come possiamo vedere, il post ha percorso tutta la strada all'indietro fino al database Mongo, senza che sia stato necessario scrivere una singola linea di codice per agganciare il nostro client al server (beh, a dire il vero, una singola riga di codice l'abbiamo scritta: new Meteor.Collection('posts')). Ma questo non è tutto!

Passiamo alla seconda finestra del browser e digitiamo quanto segue nella console del browser:

 Posts.find().count();
2
La console del secondo browser

Il post è arrivato anche qua! Anche se non abbiamo mai ricaricato e nemmeno interagito con il secondo browser, e nemmeno abbiamo mai scritto codice per mandargli gli aggiornamenti. È successo tutto magicamente – e anche istantaneamente, anche se tutto ciò diventerà più ovvio nel seguito.

Quel che è successo è che la collezione lato server è stata informata da una collezione lato client dell'inserimento di un nuovo post, e si è quindi presa in carico la distribuzione di questo post sia all'interno del database Mongo che all'indietro verso tutte le altre collezioni post connesse.

Recuperare i post dalla console del browser non è così utile. Impareremo come scrivere questi dati dentro ai template, e allo stesso tempo trasformeremo il nostro semplice prototipo HTML in una applicazione web real-time funzionante.

Rendere tutto Real-time

Una cosa è guardare il contenuto delle Collezioni dalla console del browser, ma quello che ci piacerebbe fare veramente sarebbe visualizzare i dati, e le loro modifiche, sullo schermo. Nel fare questo trasformeremo la nostra app da una semplice pagina web che mostra contenuti statici, in una applicazione web real-time dai contenuti dinamici.

Scopriamo come.

Popolare il Database

Per prima cosa inseriremo un po’ di dati all'interno del database. E lo faremo con un file di inizializzazione che carica un insieme di dati strutturati dentro la collezione Posts al primo avvio del server.

Prima di tutto assicuriamoci che dentro al database non ci sia nulla. Useremo meteor reset, che cancella il database e resetta il progetto. Chiaramente bisogna stare molto attenti ad utilizzare questo comando, soprattutto quando si inizia a lavorare su progetti veri.

Fermiamo il server Meteor (premendo ctrl-c) e dopo, da riga di comando, eseguiamo:

$ meteor reset

Il comando reset svuota completamente il database Mongo. Durante la fase di sviluppo può risultare molto utile, visto che ci sono buone probabilità che il database cada in uno stato inconsistente.

Ora che il database è vuoto, possiamo aggiungere il seguente codice che carica tre post ogni volta che il server viene lanciato e la collezione Posts risulti vuota:

if (Posts.find().count() === 0) {
  Posts.insert({
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  });

  Posts.insert({
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  });

  Posts.insert({
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  });
}
server/fixtures.js

Commit 4-2

Added data to the posts collection.

Abbiamo posizionato il file dentro alla cartella server/, in modo che non venga mai caricato in nessun browser utente. Il codice verrà eseguito immediatamente all'avvio del server, eseguendo delle chiamate insert sul database per aggiungere tre semplici post all'interno della collezione Posts. Visto che finora non abbiamo definito nessuna politica di sicurezza dei dati, non c'è nessuna differenza nel fare questo all'interno di un file eseguito sul server o da un browser.

Ora lanciamo nuovamente il server con meteor: i tre post verranno inseriti nel database.

Collegare i dati all'HTML tramite helpers

Se ora apriamo una console del browser, vedremo tutti e tre i post caricati dentro a MiniMongo:

 Posts.find().fetch();
Console del Browser

Per vedere questi post renderizzati in HTML, possiamo utilizzare un template helper. Nel Capitolo 3 abbiamo visto come Meteor permetta di collegare un data context ai template di Spacebars per costruire viste HTML di semplici strutture dati. Possiamo anche collegare i dati delle collezioni esattamente nello stesso modo. Rimpiazzeremo semplicemente l'oggetto statico postsData di JavaScript con una collezione dinamica.

A proposito, a questo punto sentitevi liberi di cancellare il codice postsData. Questo è come ora dovrebbe diventare posts_list.js:

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});
client/views/posts/posts_list.js

Commit 4-3

Wired collection into `postsList` template.

Find & Fetch

In Meteor, find() restituisce un cursore che rappresenta una sorgente di dati reattiva. Quando vogliamo farne il log del contenuto, possiamo utilizzare fetch() sul cursore per trasformarlo in un vettore.

All'interno di un applicazione, Meteor è abbastanza intelligente da sapere come iterare sui cursori senza che si debba prima convertirli in vettori. Questo è il motivo per cui non vedremo fetch() molto spesso all'interno di codice Meteor (e motivo per cui non l'abbiamo usato nell'esempio sopra).

Ora, anziché caricare una lista di post come un vettore statico, restituiamo un cursore al nostro helper posts. Questo cosa comporta? Se torniamo al browser vediamo:

Utilizzare dati dinamici
Utilizzare dati dinamici

Possiamo quindi vedere chiaramente che il nostro helper {{#each}} ha iterato su tutti i Posts e li ha visualizzati sullo schermo. La collezione lato server server ha caricato i post da Mongo, li ha passati attraverso la connessione alla collezione lato client e l'helper di Spacebars li ha passati al template.

Ora facciamo un passo in più; aggiungiamo un altro post da console:

 Posts.insert({
  title: 'Meteor Docs',
  author: 'Tom Coleman',
  url: 'http://docs.meteor.com'
});
La console del Browser

Guardiamo di nuovo il browser – dovremmo vedere quanto segue:

Adding posts via the console
Adding posts via the console

Per la prima volta abbiamo appena visto la reattività in azione. Quando diciamo ad Spacebars di iterare sul cursore Posts.find(), sa anche come osservare quel cursore per accorgersi di cambiamenti di modo da poter modificare l'HTML nel modo più semplice possibile per visualizzare i dati corretti sullo schermo.

Ispezionare i cambiamenti del DOM

In questo caso, la modifica più semplice era aggiungere un altro <div class="post">...</div>. Se vogliamo verificare che questo è effettivamente ciò che è successo, apriamo il DOM inspector e selezioniamo il <div> corrispondente ad uno dei post esistenti.

Dopodiché, nella console JavaScript, inseriamo un altro post. Tornando al DOM inspector, vedremo un <div> in più, corrispondente al nuovo post, ma lo stesso <div> del post esistente sarà ancora selezionato. Questo è di solito un modo efficace per capire se alcuni elementi sono stati ridisegnati oppure se sono rimasti invariati.

Collegare le Collezioni: Pubblicazioni e Sottoscrizioni

Fino a questo punto abbiamo avuto il pacchetto autopublish abilitato, il che non è previsto per applicazioni in fase di produzione. Come indica il nome stesso, questo pacchetto fa sì che ogni collezione sia interamente condivisa con ogni client connesso. Visto che questo non è ciò che vogliamo, disabilitiamolo.

Apriamo una nuova finestra del terminale e scriviamo:

$ meteor remove autopublish

Questo ha un effetto istantaneo. Se ora guardiamo il browser, vedremo che tutti i post sono scomparsi! Questo perché stavamo sfruttando autopublish per assicurarci che la collezione lato client dei post fosse una copia esatta di tutti i post presenti nel database.

Alla fine dello sviluppo dovremo fare in modo che vengano trasferiti solo i post che l'utente deve vedere (anche considerando cose come la paginazione). Ma per ora faremo in modo che la collezione Posts venga interamente pubblicata.

Per farlo creiamo una semplice funzione publish() che restituisce un cursore su tutti i post:

Meteor.publish('posts', function() {
  return Posts.find();
});
server/publications.js

Nel client, dobbiamo sottoscrivere la pubblicazione con subscribe. Aggiungiamo semplicemente la riga seguente al file main.js:

Meteor.subscribe('posts');
client/main.js

Commit 4-4

Removed `autopublish` and set up a basic publication.

Se controlliamo nuovamente il browser, i post sono ricomparsi. Menomale!

Conclusione

Quindi, cosa abbiamo ottenuto? Beh, anche se non abbiamo ancora un'interfaccia utente, quello che abbiamo al momento è un'applicazione web funzionante. Potremmo fare il deploy di questa app su internet e (utilizzando la console del browser) iniziare ad inserire notizie per vederle apparire nei browser degli altri utenti di tutto il mondo.

Pubblicazioni e Sottoscrizioni

Sidebar 4.5

Pubblicazioni e sottoscrizioni sono uno dei fondamentali e più importanti concetti di Meteor, ma possono risultare complicati da capire all'inizio.

Questo ha portato a molte incomprensioni, come la convinzione che Meteor sia insicuro o che le applicazioni in Meteor non possono gestire grandi quantità di dati.

Buona parte dei motivi per cui le persone trovano questi concetti un po’ confusi inizialmente è la “magia” che Meteor fa per noi. Sebbene questa magia sia molto utile, può oscurare ciò che sta succedendo realmente dietro le scene (come la magia tende a fare). Proviamo a togliere gli strati di magia per capire ciò che sta accadendo.

I vecchi tempi

Guardiamo prima a ciò che accadeva nel 2011 quando Meteor non era ancora disponibile. Diciamo che state costruendo una semplice applicazione in Rails. Quando un utente raggiunge il vostro sito, il cliente (ad esempio il browser) manda una richiesta all'applicazione che risiede sul server.

Il primo lavoro dell'applicazione è quello di capire che dati l'utente vuole vedere. Potrebbe essere pagina 12 dei risultati di ricerca, le informazioni del profilo di Mary, i 20 ultimi tweet di Bob e così via. Potete figurarla come un commesso di una libreria che cerca negli scaffali il libro che avete richiesto.

Quando i dati corretti sono stati selezionati, il secondo lavoro dell'applicazione è quello di tradurre i dati in formato HTML leggibile da una persona (o JSON nel caso di un API).

Nella metafora della libreria, sarebbe il passaggio di impacchettare il libro che avete appena comprato e metterlo in una bella borsa. Questa è la parte “View” del famoso schema Model-View-Controller.

Infine, l'applicazione prende il codice HTML e lo restituisce al browser. Il lavoro è finito e ora che tutto è stato consegnato l'applicazione può rilassarsi con una birra e aspettare per la prossima richiesta.

Come funziona Meteor

Rivediamo cosa rende Meteor così speciale. Come abbiamo visto, l'innovazione di Meteor sta nel fatto che se un'applicazione Rails vive solo sul server, un'applicazione Meteor comprende anche una componente che risiede nel client (il browser).

Pushing a subset of the database to the client.
Pushing a subset of the database to the client.

Questo è come un commesso che non solo trova il giusto libro per voi, ma vi segue fino a casa per leggerlo di notte (che ammettiamo suona un po’ spaventoso).

Questa architettura fa in modo che Meteor faccia molte cose interessanti, prima fra tutte quello che Meteor chiama database ovunque. In parole semplici, Meteor prende una parte del database e lo copia nel client.

Ci sono due grandi implicazioni: primo, invece di mandare il codice HTML al client, un'applicazione Meteor invia i dati grezzi e lascia che sia il client ad occuparsene (dati sul filo). In secondo luogo, si è in grado di accedere ai dati istantaneamente senza dover aspettare un intero giro di scambio con il server (compensazione di latenza).

Pubblicare

Il database di un'applicazione può contenere decine di migliaia di documenti, alcuni dei quali possono essere dati privati o sensibili. Per questo motivo non dobbiamo ovviamente replicare tutto il database nel client, per ragioni di sicurezza e scalabilità.

Abbiamo bisogno di un modo per dire a Meteor quale sottoinsieme di dati può essere inviato al client e lo faremo attraverso una pubblicazione.

Torniamo a Microscope. Qui abbiamo tutti i post che si trovano nel database dell'applicazione:

Tutti i posts contenuti nel database.
Tutti i posts contenuti nel database.

Immaginiamo che alcuni dei nostri post siano stati segnalati per linguaggio inappropriato, anche se questa opzione non esiste al momento in Microscope. Anche se vogliamo tenere questi post nel database, non devono essere resi disponibili agli utenti (ad esempio inviati al client).

Il nostro primo compito è di dire a Meteor quali dati vogliamo mandare al client. Diciamo a Meteor che vogliamo pubblicare solo i post non segnalati:

Esclusione dei posts segnalati.
Esclusione dei posts segnalati.

Questo è il codice corrispondente, che risiede sul server:

// on the server
Meteor.publish('posts', function() {
  return Posts.find({flagged: false});
});

Questo assicura che non ci sia nessun modo possibile che un client possa accedere ai post segnalati. In questo modo si rende sicura l'applicazione Meteor: dovete assicurarvi che state pubblicando solo i dati a cui volete che il client acceda.

DDP

Fondamentalmente potete pensare a pubblicazione/sottoscrizione come ad un tubo che trasferisce i dati da una collezione sul server (sorgente) ad una collezione lato client (obiettivo).

Il protocollo che gestisce questo comunicazione è chiamato DDP (che sta per Distributed Data Protocol). Per saperne di più sul DPP potete guardare questo intervento alla Real-time Conference di Matt DeBergalis (uno dei fondatori di Meteor), o questo screencast di Chris Mather che vi mostra qualche dettaglio in più su questo concetto.

Sottoscrizioni

Anche se vogliamo che gli utenti vedano tutti i post non segnalati, non possiamo inviare migliaia di post alla volta. Abbiamo bisogno di un modo con cui i client possano specificare di quale sottoinsieme di dati hanno bisogno e lo facciamo attraverso le sottoscrizioni.

Ogni dato a cui si fa una sottoscrizione viene replicato sul client grazie a Minimongo, l'implementazione lato client, contenuto in Meteor, di MongoDB.

Ad esempio, stiamo navigando sul profilo di Bob Smith e vogliamo mostrare solo i suoi post.

Sottoscrivendosi ai posts di Bob, questi verranno replicati sul client.
Sottoscrivendosi ai posts di Bob, questi verranno replicati sul client.

Come prima cosa sistemiamo le nostre pubblicazioni in modo che accettino un parametro:

// on the server
Meteor.publish('posts', function(author) {
  return Posts.find({flagged: false, author: author});
});

Definiamo questo parametro quando facciamo sottoscrizione a quella pubblicazione nel codice lato client della nostra applicazione:

// on the client
Meteor.subscribe('posts', 'bob-smith');

Per rendere scalabile un'applicazione Meteor lato client, invece di sottoscrivere ogni dato disponibile, prendiamo e scegliamo le parti che sono necessarie. In questo modo evitiamo il sovraccarico della memoria del browser indipendentemente da quando è grande il database lato server.

Come trovare i dati

I post di Bob sono divisi in più categorie (per esempio: “JavaScript”, “Ruby” e “Phyton”). Potremmo voler ancora caricare tutti i post di Bob in memoria, ma vogliamo mostrare solo quelli della categoria “JavaScript”. Vediamo come trovarli.

Selezione di un sottoinsieme di documenti lato client.
Selezione di un sottoinsieme di documenti lato client.

Come abbiamo fatto sul server, useremo la funzione Posts.find() per selezionare un sottoinsieme dei nostri dati:

// on the client
Template.posts.helpers({
    posts: function(){
        return Posts.find({author: 'bob-smith', category: 'JavaScript'});
    }
});

Ora che abbiamo più informazioni sul ruolo di pubblicazioni e sottoscrizioni, andiamo più a fondo e osserviamo alcuni schemi comuni di implementazione.

Autopubblicazione

Se create un progetto Meteor da zero (ad esempio usando meteor create), verrà automaticamente abilitato il pacchetto autopublish. Come prima cosa vediamo esattemente cosa fa.

L'obiettivo di autopublish è di semplificare molto l'iniziare a scrivere codice in un'applicazione Meteor, replicando automaticamente tutti i dati dal server al client, prendendosi carico di pubblicazioni e sottoscrizioni al posto nostro.

Autopublish
Autopublish

Come funziona? Supponiamo di avere una collezione chiamata 'posts' sul server: autopublish invia automaticamente ogni post che trova nella collezione lato server in Mongo in una collezione chiamata 'posts' sul client (assumendo che ne esista una).

Se state usando autopublish, non dovete preoccuparvi delle pubblicazioni. I dati sono dappertutto e le cose sono semplici. Ci sono ovvie problematiche legate al fatto di avere una copia completa del database dell'applicazione nella cache della macchina di ogni utente.

Per questo motivo autopublish è utile solo quando state iniziando e non avete ancora pensato alle pubblicazioni.

Pubblicare intere collezioni

Dopo aver rimosso autopublish vi accorgerete presto che tutti i vostri dati sono spariti dal client. Un modo semplice per riverderli è di duplicare ciò che fa autopublish e pubblicare una collezione intera. Ad esempio:

Meteor.publish('allPosts', function(){
  return Posts.find();
});
Pubblicare una collezione intera
Pubblicare una collezione intera

Stiamo ancora pubblicando le intere collezioni ma abbiamo ora controllo su quali vogliamo mostrare. In questo caso stiamo pubblicando Posts ma non Comments.

Pubblicare collezioni parziali

Il prossimo livello di controllo è di pubblicare solo parte di una collection. Ad esempio solo i post che appartengono ad un certo autore:

Meteor.publish('somePosts', function(){
  return Posts.find({'author':'Tom'});
});
Pubblicare una collezione parziale.
Pubblicare una collezione parziale.

Dietro le quinte

Se avete letto la documentazione di Meteor sulle pubblicazioni, vi sarete forse preoccupati sentendo parlare di come usare added() e ready() per settare gli attributi dei record sul client e avrete faticato a capire come fare guardando le applicazioni di Meteor che non usano mai quei metodi.

Il motivo è che Meteor provvede una convezione molto importante: il metodo _publishCursor(). Non l'avete mai visto prima? Forse non direttamente ma se ritornate un cursore (ad esempio Posts.find({'author':'Tom'})) in una funzione di pubblicazione, è esattamente ciò che Meteor sta usando.

Quando Meteor vede che la pubblicazione somePosts ha ritornato un cursore, chiama _publishCursor() per – avete indovinato – pubblicare quel cursore automaticamente.

Questo è ciò che fa _publishCursor():

  • Controlla il nome della collezione lato server.
  • Prende tutti i documenti risultanti e li invia ad una collezione lato client con lo stesso nome (Usa added() per farlo).
  • Ogni volta che un documento viene aggiunto, rimosso o cambiato, invia questi cambiamenti alla collezione lato client (Usa .observe() sul cursore e .added(), .changed() e removed() per farlo).

In questo esempio possiamo essere sicuri di inviare all'utente solo i post a cui sono interessati (quelli scritti da Tom) disponibili nella cache lato client.

Pubblicare proprietà parziali

Abbiamo visto come pubblicare alcuni dei nostri post ma possiamo andare ancora più a fondo! Vediamo come pubblicare solo specifiche proprietà.

Come prima, usiamo find() per ritornare un cursore, ma questa volta escludiamo alcuni campi:

Meteor.publish('allPosts', function(){
  return Posts.find({}, {fields: {
    date: false
  }});
});
Pubblicare proprietà parziali.
Pubblicare proprietà parziali.

Possiamo combinare entrambe le tecniche. Ad esempio, se vogliamo ritornare tutti i post di Tom ma non la loro data di pubblicazione, scriviamo:

Meteor.publish('allPosts', function(){
  return Posts.find({'author':'Tom'}, {fields: {
    date: false
  }});
});

Riassumendo

Abbiamo visto come pubblicare ogni proprietà di ogni documento di ogni collezione (con autopublish) e come pubblicare solo alcune proprietà di alcuni documenti di alcune collezioni.

Questo copre le basi di quello che si può fare con le pubblicazioni in Meteor, e queste semplici tecniche bastano a coprire la maggioranza dei casi.

Qualche volta avrete bisogno di andare oltre combinando, collegando oppure unendo le pubblicazioni. Vedremo come fare in un capitolo successivo!

Routing

5

Ora che abbiamo una lista di post (che nella versione finale saranno inseriti dagli utenti), abbiamo bisogno di una pagina specifica per ogni post dove gli utenti possono discutere del singolo post.

Sarebbe bene rendere queste pagine accessibili tramite permalink, un URL nella forma http://myapp.com/posts/xyz (dove xyz è un identificatore _id di MongoDB).

Dunque abbiamo bisogno di un meccanismo di indirizzamento detto routing per leggere la barra dell'URL del browser e mostrare il contenuto corrispondente.

Aggiungere il pacchetto Iron Router

Iron Router è un pacchetto di gestione routing progettato appositamente per le applicazioni Meteor.

Non è utile solo per il routing (impostando il percorso), ma può gestire i filtri (assegnando azioni ad alcuni di questi percorsi) ed anche gestire le sottoscrizioni (controllando quale percorso ha accesso a quali dati). (Nota: Iron Router è sviluppato in parte da Tom Coleman, co-autore di Discover Meteor.)

Per prima cosa installiamo il package da Atmosphere:

$ mrt add iron-router
Terminal

Questo comando scarica ed installa il pacchetto iron-router nell'applicazione, pronto all'uso. Potrebbe essere necessario rilanciare l'applicazione Meteor (con ctrl+c per terminare il processo e poi mrt per riavviarlo) prima che un pacchetto sia utilizzabile.

Si noti che Iron Router è un pacchetto di terze parti, quindi serve Meteorite per installarlo (meteor add iron-router non funziona).

Vocabolario del Router

In questo capitolo vedremo molte caratteristiche del router. Se hai già esperienza con un framework tipo Rails, hai già familiarità con la maggior parte di questi concetti. Se no, ecco un veloce glossario per farti imparare rapidamente:

  • Route: Una route è il mattone base del routing. È il set di istruzioni che dice all'applicazione dove andare e cosa fare quando incontra un URL.
  • Path: Un path è il percorso indicato da un URL all'interno dell'applicazione. Può essere statico (/terms_of_service) o dinamico (/posts/xyz), e può anche includere parametri di ricerca (/search?keyword=meteor).
  • Segment: Le diverse parti di un path, delimitato da slashes (/).
  • Hook: Gli hook sono azioni che desideri fare prima, dopo, o anche durante il processo di routing. Un esempio tipico è il controllo dei diritti di accesso dell'utente prima di mostrare una determinata pagina.
  • Filter: I filtri sono semplicemente hook definiti globalmente per una o più route.
  • Route Template: Ogni route deve puntare ad un template. Se non ne specifichi uno, il router cercherà un template con lo stesso nome della route.
  • Layout: Puoi immaginare i layout come una di quelle cornici digitali. Contengono tutto il codice HTML che avvolge l'attuale template, e rimarrà invariato anche se il template cambia.
  • Controller: A volte potresti renderti conto che molti dei tuoi template riutilizzano gli stessi parametri. Piuttosto che duplicare il codice, puoi ereditare queste route da un unico routing controller che contiene tutta la logica di routing.

Per altre informazioni su Iron Router, consulta la documentazione completa su GitHub.

Routing: Associare gli URL ai Template

Fino a questo momento abbiamo costruito l'interfaccia usando elementi statici nei template (come {{> postsList}}). In questo modo sebbene il contenuto dell'applicazione possa cambiare, la struttura di base della pagina rimane sempre la stessa: una intestazione con una lista di post sotto.

Iron Router permette di superare questa staticità prendendo il controllo di ciò che viene mostrato nel tag HTML . Non definiremo noi il contenuto di questo tag, come faremmo con una normale pagina HTML. Faremo invece puntare il router ad uno speciale template di struttura che contiene un helper {{> yield}}.

Questo helper {{> yield}} definirà un'area dinamica speciale che mostrerà automaticamente il template corrispondente all'attuale route (per convenzione chiameremo questo template speciale “route template” d'ora in poi):

Layouts and templates.
Layouts and templates.

Iniziamo creando lo schema di pagina e aggiungendo l'helper {{> yield}}. Per prima cosa eliminiamo il tag da main.html e ne spostiamo il contenuto in un template layout.html.

Quindi il nostro main.html modificato appare così:

<head>
  <title>Microscope</title>
</head>
client/main.html

Il nuovo file layout.html conterrà invece lo schema generale di pagina:

<template name="layout">
  <div class="container">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="brand" href="/">Microscope</a>
    </div>
  </header>
  <div id="main" class="row-fluid">
    {{> yield}}
  </div>
  </div>
</template>
client/views/application/layout.html

Come puoi notare abbiamo sostituito l'inclusione del template postsList con una chiamata all'helper yield. Dopo questa modifica a video non apparirà più nulla. Succede perché non abbiamo detto al router cosa mostrare con l'URL / e quindi mostra semplicemente un template vuoto.

Per iniziare possiamo tornare alla situazione di prima mappando l'URL radice / sul template postsList. Creiamo una cartella /lib nella radice del nostro progetto ed all'interno il file router.js :

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

Abbiamo appena fatto due cose importanti. Primo, abbiamo detto al router di usare l'impaginazione appena creata come default per tutte le route. Secondo, abbiamo definito una nuova route chiamata postsList e mappata sul path /.

La cartella /lib

Tutto ciò che metti dentro la cartella /lib è garantito che sarà caricato prima di qualsiasi altra cosa nell'applicazione (con la sola potenziale eccezione dei pacchetti smart). È quindi il posto migliore dove mettere il codice degli helper che deve essere disponibile sempre.

Una piccola avvertenza: poiché la cartella /lib non si trova né dentro /client né dentro /server il suo contenuto sarà visibile in entrambi gli ambienti.

Nomi delle Route

Chiariamo un aspetto potenzialmente ambiguo. Abbiamo chiamato la nostra route postsList, ma abbiamo anche un template chiamato postsList. Dunque che significa?

L'impostazione predefinita di Iron Router è quella di cercare un template con lo stesso nome della route. A dire il vero cercherà anche un path basandosi sul nome della route, e quindi se non avessimo definito un path personalizzato (cosa che abbiamo fatto indicando un path nella definizione della route), il nostro template sarebbe comunque stato accessibile all'URL /postsList.

Potresti chiederti perché abbiamo bisogno di dare un nome alle route come prima cosa. Dar loro un nome ci permette di usare alcune funzionalità di Iron Router che rendono più semplice costruire i link all'interno dell'applicazione. La più utile è l'helper di Spacebars `{{pathFor}}, che contiene la parte di path dell'URL di ogni route.

Noi vogliamo che il link alla home principale punti alla lista di post, e quindi invece di indicare una URL statica /, possiamo usare l'helper di Spacebars. Il risultato finale sarà lo stesso, ma avremo maggiore flessibilità visto che l'helper fornirà sempre l'URL corretta anche se cambiassimo il path della route nel router.

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
  </div>
</header>

//...
client/views/application/layout.html

Commit 5-1

Very basic routing.

In Attesa dei Dati

Se installi ed esegui l'attuale versione dell'applicazione (o la lanci dal link qui sopra), potrai notare come la lista appaia vuota per alcuni istanti prima che appaiano i post. Succede perché quando la pagina viene caricata per la prima volta non ci sono post da mostrare finché la sottoscrizione posts non ha completato il recupero dei dati dal server.

Sarebbe molto meglio dal punto di vista dell'esperienza d'uso dare un feedback visuale che indica che qualcosa sta succedendo, e quindi l'utente deve aspettare un attimo.

Fortunatamente Iron Router fornisce un modo semplice per farlo – basta mettersi in attesa della sottoscrizione con waitOn:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});

Router.onBeforeAction('loading');
lib/router.js

Esaminiamo pezzo per pezzo. Per prima cosa abbiamo modificato il blocco Router.configure() per indicare il nome di un template (che creeremo tra un attimo) da mostrare durante il caricamento e l'attesa dei dati.

Secondo abbiamo aggiunto una funzione waitOn che ritorna la sottoscrizione ai nostri posts. Infine abbiamo abilitato l'hook loading integrato. Ciò significa che il router si assicurerà che la sottoscrizione posts sia completamente caricata prima di portare l'utente alla route richiesta.

Osserva che poiché stiamo definendo la nostra funzione waitOn globalmente a livello del router, questa sequenza avverrà solo una volta quando l'utente accede per la prima volta all'applicazione. Dopo i dati saranno già presenti nella memoria del browser ed il router non dovrà aspettarli di nuovo.

Inoltre poiché lasciamo che sia il router a gestire la sottoscrizione, la puoi tranquillamente rimuovere da main.js (che ora dovrebbe essere vuota).

Normalmente è una buona idea attendere il caricamento della sottoscrizione, non solo per l'esperienza d'uso, ma anche perché ti permette di contare sulla disponibilità dei dati all'interno del template. Così facendo infatti non dobbiamo più gestire la situazione in cui un template viene mostrato prima che i dati contenuti siano disponibili, con tutti i problemi che ne conseguono.

Aggiungeremo anche un filtro onBeforeAction per triggherare la funzionalità ‘loading’, predefinita in Iron Router, ed assicurarci di mostrare il template di caricamento mentre attendiamo.

L'ultimo pezzo del puzzle è il template di caricamento vero e proprio. Useremo il pacchetto spin per creare una simpatica animazione di attesa caricamento. Aggiungilo con mrt add spin e poi crea il template di caricamento così:

<template name="loading">
  {{> spinner}}
</template>
client/views/includes/loading.html

Nota che {{> spinner}} è un parziale contenuto nel pacchetto spin. Sebbene questo parziale arrivi “dall'esterno” dell'applicazione, possiamo includerlo come qualsiasi altro template.

Commit 5-2

Wait on the post subscription.

Un Primo Sguardo alla Reattività

La reattività è una delle caratteristiche principali di Meteor, e sebbene non l'abbiamo ancora veramente affrontata, il template di caricamento ci dà una prima idea di questo concetto.

Mostrare all'utente un template di caricamento se i dati non sono ancora disponibili è cosa buona e giusta, ma come fa il router a sapere quando mostrare all'utente la pagina completa quando i dati sono arrivati?

Per ora diciamo che è esattamente ciò che la reattività ci permette di fare, e fermiamoci qui. Ma non preoccuparti, ne saprai di più molto presto!

Routing ad un Post Specifico

Adesso che abbiamo visto come mostrare il template postsList, creiamo una route per mostrare i dettagli di un singolo post.

C'è solo una difficoltà: non possiamo pensare di creare una route per ogni post visto che potrebbero essercene centinaia. Quindi creiamo una sola route ma di tipo dinamico, e facciamo in modo che mostri qualsiasi post desideriamo.

Per iniziare creiamo un nuovo template che semplicemente mostra lo stesso template di singolo post che abbiamo usato prima nella lista dei post.

<template name="postPage">
  {{> postItem}}
</template>
client/views/posts/post_page.html

Aggiungeremo altri elementi a questo template più avanti (come ad esempio dei commenti), ma per ora ci servirà semplicemente come contenitore per il nostro {{> postItem}}.

Creiamo poi un'altra route a cui assegniamo un nome, questa volta mappando il path dell'URL nella forma /posts/<ID> sul template postPage:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id'
  });
});

lib/router.js

La sintassi speciale :_id dice al router due cose: primo di riconoscere ogni route nella forma /posts/xyz, dove “xyz” può essere qualsiasi cosa. Secondo di mettere qualunque cosa trovi al posto di “xyz” dentro una proprietà _id dell'array params del router.

Nota che stiamo usando _id solo per chiarezza. Il router non ha modo di sapere se stai passando un vero _id o solo una sequenza casuale di caratteri.

A questo punto stiamo instradando al template corretto, ma ci manca ancora qualcosa: il router conosce l’_id del post che vogliamo mostrare, ma il template no. Come risolviamo?

Fortunatamente il router ha una soluzione intelligente già integrata: permette di specificare il contesto dati (data context) del template. Puoi immaginare il contesto dati come la farcitura di una torta fatta di template e layout della pagina. Messa in maniera semplice è ciò con cui riempiamo il template:

The data context.
The data context.

Nel nostro caso possiamo ottenere il giusto contesto dati cercando il nostro post in base all’_id che abbiamo ricavato dall'URL:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });
});

lib/router.js

Così ogni volta che l'utente accede a questa route, noi troveremo il post corrispondente e lo passeremo al template. Ricorda che findOne ritorna un solo post che soddisfa la ricerca, e che fornire solo un _id come argomento è una abbreviazione di {_id: id}.

All'interno della funzione data di una route, this corrisponde alla route attuale e possiamo usare this.params per accedere alle proprietà della route (che abbiamo indicato usando il prefisso : all'interno del nostro path).

Approfondiamo i Contesti Dati

Impostando il contesto dati di un template puoi controllare il valore assunto da this negli helpers all'interno del template.

Normalmente lo si imposta implicitamente con l'iteratore {{#each}}, che imposta automaticamente il contesto dati all'elemento su cui stiamo iterando:

{{#each widgets}}
  {{> widgetItem}}
{{/each}}

Ma possiamo anche impostarlo esplicitamente usando {{#with}}, che dice semplicemente: “considera questo oggetto e applicalo al seguente template”. Ad esempio possiamo scrivere:

{{#with myWidget}}
  {{> widgetPage}}
{{/with}}

Si può ottenere lo stesso risultato passando il contesto come argomento nell'invocazione del template. In questo caso il precedente codice può essere riscritto così:

{{> widgetPage myWidget}}

Utilizzare un Helper nella Route Dinamica

Per concludere dobbiamo essere certi di puntare al giusto percorso ogni volta che desideriamo linkare un singolo post. Potremmo scrivere qualcosa del tipo <a href="/posts/{{_id}}">, ma usare un route helper è più affidabile.

Abbiamo chiamato la route del singolo post postPage, quindi possiamo utilizzare un helper {{pathFor 'postPage'}}:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Commit 5-3

Routing to a single post page.

Ma, un attimo! Come fa esattamente il router a sapere dove prendere la parte xyz in /posts/xyz? In fondo non stiamo passando nessun _id.

Scopriamo che Iron Router è sufficientemente intelligente da capirlo da solo. Infatti stiamo dicendo al router di usare la route postPage, ed il router sa già che questa route richiede un _id di qualche tipo (in quanto è così che abbiamo definito il path).

Quindi il router cercherà questo _id nel posto più logico: il contesto dati dell'helper {{pathFor 'postPage’}}, cioè this. Succede proprio che il nostro this corrisponde ad un post che (sorpresa!) possiede una proprietà _id.

In alternativa puoi anche dire esplicitamente al router dove vuoi che cerchi la proprietà _id passando un secondo argomento all'helper (es. {{pathFor 'postPage' unAltroPost}}). Nell'uso reale possiamo usare questa possibilità per avere un link al post precedente o successivo della lista, ad esempio.

Per vedere se funziona tutto correttamente, naviga alla lista di post e clicca sui link Discuss. Dovresti vedere qualcosa del genere:

A single post page.
A single post page.

HTML5 pushState

Una cosa da tener presente è che questa gestione degli URL avviene attraverso HTML5 pushState.

Il router intercetta i click sugli URL che sono interni al sito e impedisce al browser di gestirli come fossero link esterni, preoccupandosi invece di modificare opportunamente solo lo stato dell'applicazione.

Se tutto funziona a dovere la pagina dovrebbe modificarsi istantaneamente. Addirittura a volte le cose cambiano così rapidamente che potrebbe essere opportuno inserire delle transizioni di pagina. Pur essendo fuori dall'ambito di questo capitolo è un argomento molto interessante.

Le variabili in Session

Sidebar 5.5

Meteor è un framework reattivo. Ciò significa che al variare dei dati la nostra applicazione si aggiorna senza la necessità di fare nulla di specifico.

Abbiamo già visto all'opera questo meccanismo sui template che si aggiornano al variare dei dati e delle route.

Approfondiremo questo meccanismo nei prossimi capitoli, ma ora vogliamo introdurre alcune caratteristiche reattive di base usate spesso nelle applicazioni.

L'oggetto Session in Meteor

In questo momento in Microscope lo stato dell'applicazione è interamente contenuto nell'URL che l'utente sta consultando (e nel database).

In molti casi però hai bisogno di alcune informazioni transitorie sullo stato dell'applicazione, rilevanti solo per l'istanza corrente dell'applicazione di uno specifico utente (ad esempio se un elemento è mostrato o nascosto). L'oggetto Session è perfetto per questo.

L'oggetto Session è un contenitore dati globale e reattivo. È globale nel senso di un unico oggetto visibile ovunque: c'è una sola sessione ed è accessibile dall'intera applicazione. Le variabili globali sono normalmente mal viste ma in questo caso l'oggetto Session è usato come centro di comunicazione tra tutte le parti dell'applicazione.

Memorizzare dati nell'oggetto Session

L'oggetto sessione è visibile ovunque come Session. Per impostare un valore si invoca:

 Session.set('pageTitle', 'A different title');
Browser console

I dati memorizzati posso essere letti con Session.get('mySessionProperty');. È una sorgente dati reattiva, quindi se utilizzata all'interno di un helper vedremo l'output dell'helper modificarsi automaticamente al cambio dei dati.

Per fare una prova, aggiungi questo codice al template di layout:

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">{{pageTitle}}</a>
  </div>
</header>
client/views/application/layout.html
Template.layout.helpers({
  pageTitle: function() { return Session.get('pageTitle'); }
});
client/views/application/layout.js

Il meccanismo di ricaricamento automatico del codice di Meteor (noto come “hot code reload” o HCR) preserva lo stato delle variabili di sessione, e quindi ora dovremmo vedere “A different title” nella barra di navigazione. Se no occorre ridigitare il comando Session.set() di nuovo.

Inoltre se cambiamo ancora una volta il valore (nuovamente nella console del browser), vedremo il nuovo titolo:

 Session.set('pageTitle', 'A brand new title');
Browser console

L'oggetto Session è visibile globalmente, quindi le variazioni possono essere fatte ovunque nell'applicazione. Questo ci apre molte possibilità ma può anche trasformarsi in una trappola, se ne abusiamo.

Modifiche Identiche

Se modifichi una variabile della sessione con Session.set() reimpostando il valore già memorizzato, Meteor è sufficientemente intelligente da non eseguire alcun aggiornamento reattivo, evitando inutili elaborazioni.

Introduciamo Autorun

Abbiamo visto un esempio di sorgente dati reattiva e visto il suo impiego all'interno di un helper. Dobbiamo però tenere presente che sebbene alcuni contesti in Meteor siano intrinsecamente reattivi (come gli helper dei template), la maggior parte di una applicazione Meteor è puro e semplice JavaScript non reattivo.

Supponiamo di avere questo codice nell'applicazione:

helloWorld = function() {
  alert(Session.get('message'));
}

Anche se stiamo utilizzando una variabile di sessione, il contesto in cui viene usata non è reattivo, e dunque non saranno mostrati nuovi alert ogni volta che modifichiamo la variabile.

È in queste circostanze che torna utile Autorun. Come dice il nome stesso, il codice all'interno di un blocco autorun sarà eseguito e rieseguito ogni volta che le sorgenti dati reattive all'interno vengono modificate.

Scriviamo nella console del browser:

 Deps.autorun( function() { console.log('Value is: ' + Session.get('pageTitle')); } );
Value is: A brand new title
Browser console

Come ci saremmo aspettati il codice all'interno di autorun è stato eseguito mostrando l'output in console. Ora proviamo a modificare il titolo:

 Session.set('pageTitle', 'Yet another value');
Value is: Yet another value
Browser console

Magia! Al variare del valore di sessione autorun sapeva di dover rieseguire il codice, ristampando il nuovo valore nella console.

Dunque tornando al nostro esempio di prima, se vogliamo produrre un alert ogni volta che la variabile di sessione cambia dobbiamo inserire il codice in un blocco autorun:

Deps.autorun(function() {
  alert(Session.get('message'));
});

Come abbiamo appena visto l'autorun del codice è molto utile per monitorare i dati reattivi e agire al loro variare.

Hot Code Reload

Durante lo sviluppo di Microscope abbiamo usato una delle caratteristiche di Meteor che fanno risparmiare tempo: il ricaricamento a caldo del codice (hot code reload, HCR). Ogni volta che salviamo un file sorgente, Meteor se ne accorge e riavvia il server in maniera trasparente, informando ogni client che è necessario ricaricare la pagina.

Questo meccanismo è simile al ricaricamento automatico della pagina, ma con una importante differenza.

Per capire quale, azzeriamo la variabile di sessione che abbiamo usato finora:

 Session.set('pageTitle', 'A brand new title');
 Session.get('pageTitle');
'A brand new title'
Browser console

Se ricaricassimo la pagina nella finestra del browser manualmente, le variabili contenute nell'oggetto Session andrebbero perse (in quanto si creerebbe un'intera nuova sessione). Invece quando modifichiamo e salviamo un sorgente scatenando un ricaricamento a caldo del codice, la variabile di sessione non perde il suo valore. Proviamo!

 Session.get('pageTitle');
'A brand new title'
Browser console

Questo significa che se stiamo usando le variabili di sessione per tenere traccia di cosa sta facendo l'utente, il ricaricamento a caldo sarà trasparente per l'utente, perché il valore delle variabili di sessione rimane inalterato. Possiamo quindi rilasciare nuove versioni di una applicazione Meteor ed aspettarci che gli utenti ne saranno solo minimamente disturbati.

Pensaci un attimo. Se possiamo gestire tutto lo stato tramite URL e sessione, possiamo modificare in maniera trasparente il codice in esecuzione di ogni applicazione sotto il naso dell'utente mentre è in esecuzione e con problemi minimi.

Vediamo invece cosa succede se facciamo manualmente il refresh della pagina:

 Session.get('pageTitle');
null
Browser console

Al ricaricamento della pagina abbiamo perso la sessione. Durante un HCR, Meteor memorizza la sessione nel browser e la recupera dopo il reload. D'altro canto il normale comportamento al ricaricamento della pagina ha senso: se un utente ricarica esplicitamente una pagina è come se avesse raggiunto quell'URL per la prima volta, e quindi tutto deve apparire nello stesso stato iniziale che qualsiasi altro utente troverebbe raggiungendo quell'URL.

Queste sono le importanti lezioni di tutto questo:

  1. Memorizza sempre lo stato dell'utente nell'oggetto Session oppure nell'URL così da minimizzare i problemi degli utenti in caso di HCR.
  2. Memorizza qualsiasi informazione di stato che si desidera sia condivisibile con altri utenti dentro l'URL stessa.

Aggiungere Utenti

6

Finora si è lavorato alla creazione e visualizzazione di dati di esempio statici ed implementato un semplice prototipo.

È stato verificato come l'interfaccia utente sia reattiva a variazioni dei dati, con inserimenti e modifiche che vengono presentati immediatamente. Tuttavia, il sito è ancora penalizzato dal fatto che non è possibile inserire dati. E infatti ancora non vengono neanche gestiti gli utenti.

Vediamo come sia possibile sistemare la cosa.

Account: gestire utenti in modo semplice

In molti framework per il web, aggiungere profili utente è un problema ricorrente. Quasi ogni progetto necessita di profili utente, e purtroppo spesso è più complicato di quanto invece dovrebbe e potrebbe essere. Inoltre, non appena si renda necessario interagire con OAuth o altri schemi di autenticazione di terze parti, le cose tendono a complicarsi ulteriormente.

Fortunatamente, Meteor ha la soluzione. Grazie alla peculiarità dei package di Meteor di fornire codice sia lato server (JavaScript) che lato client (JavaScript, HTML e CSS), è possibile implementare un sistema di gestione degli account a costo quasi nullo.

È sufficiente utilizzare l'interfaccia utente per gli account integrata in Meteor (tramite mrt add accounts-ui), ma poiché l'intera applicazione utilizza Bootstrap, verrà invece utilizzato il package accounts-ui-bootstrap-dropdown (niente di cui preoccuparsi, la sola differenza riguarda il foglio di stile). Da linea di comando, scrivere:

$ mrt add accounts-ui-bootstrap-dropdown
$ mrt add accounts-password
Terminale

Questi due comandi provvedono ad installare gli appositi template per gli account; è possibile includerli nel sito utilizzando {{> loginButtons}}. Un consiglio molto utile: si può controllare su quale lato posizionare il menù a tendina utilizzando l'attributo align (ad esempio: {{>loginButtons align="right"}}).

È tempo di aggiungere i pulsanti all'header. Poiché l'header comincia a crescere oltremodo, è meglio concedergli un pò più di spazio spostandolo su un apposito template (che verrà salvato in client/views/includes/). Verrà anche utilizzato del markup aggiuntivo e alcune classi Bootstrap per migliorarlo dal punto di vista estetico:

<template name="layout">
  <div class="container">
    {{> header}}
    <div id="main" class="row-fluid">
      {{> yield}}
    </div>
  </div>
</template>
client/views/application/layout.html
<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav pull-right">
          <li>{{> loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Ora, accedendo all'applicazione, i pulsanti per l'autenticazione saranno visualizzati nell'angolo in alto a destra del sito.

Interfaccia utente per la gestione degli account integrata in Meteor
Interfaccia utente per la gestione degli account integrata in Meteor

È possibile utilizzarli per la registrazione, il login, per modificare la password, e tutto ciò che un semplice sito necessita per gli account basati su password.

Per abilitare l'autenticazione tramite nome utente nel sistema di gestione degli account, è sufficiente aggiungere un blocco di configurazione Accounts.ui in un nuovo file config.js, da salvare in client/helpers/:

Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY'
});
client/helpers/config.js

Commit 6-1

Added accounts and added template to the header

Creazione del primo utente

Procedendo e creando un nuovo account, il pulsante di registrazione “Sign in” cambierà per visualizzare il nome utente prescelto. Ciò conferma che un account utente è stato creato. Ma da dove provengono i dati relativi all'account utente?

Con l'aggiunta del package accounts, Meteor ha creato una nuova collezione speciale, alla quale si può accedere come Meteor.users. Per consultarne il contenuto, aprire la console del browser e digitare:

 Meteor.users.findOne();
Console del browser

La console dovrebbe restituire un oggetto che rappresenta l'utente sopra creato; ad una osservazione più attenta si nota che il nome utente è incluso, così come un campo _id che identifica in maniera univoca l'utente. Da notare che è possibile ottenere l'utente correntemente autenticato tramite Meteor.user().

Provando adesso ad uscire e registrandosi con un nuovo nome utente, Meteor.user() dovrebbe restituire il secondo utente. Tuttavia, se si prova ad eseguire:

 Meteor.users.find().count();
1
Console del browser

La console restituisce 1. Non dovrebbe invece essere 2? È stato eliminato il primo utente? Provando a loggarsi utilizzando il primo account, si verifica immediatamente che non è questo il caso.

È meglio assicurarsi controllando l'archivio dati predefinito, il database Mongo. Loggarsi in Mongo (meteor mongo da terminale) e verificare quanto segue:

> db.users.count()
2
Console di Mongo

Indubbiamente ci sono due utenti. Quindi perché solo uno per volta è visibile dal browser?

Una pubblicazione misteriosa!

Nel capitolo 4 si era parlato di come, disabilitando l’autopublishing, le collezioni non vengano più trasferite in maniera automatica dal server alla corrispondente versione locale della collezione, per ciascuno dei client collegati. Si è reso necessario create una coppia pubblicazione e sottoscrizione per veicolare i dati tra le parti.

Ma non è stata mai creata una pubblicazione per gli utenti. Per cui, come è possibile che i dati utente siano visibili?

La risposta sta nel fatto che il package degli account automaticamente esegue una “auto-pubblicazione” di un sottoinsieme dei dati relativi all'account dell'utente correntemente loggato. Se così non fosse, l'utente non sarebbe mai in grado di loggarsi sul sito!

Comunque il package degli account pubblica solamente l'utente corrente. Questo spiega il perché i dati degli altri utenti non sono visibili.

Quindi la pubblicazione è limitata ad un solo oggetto per utente loggato (e nessuno nel caso in cui non si è loggati).

Inoltre, i documenti nella collezione degli utenti non contengono gli stessi campi tra server e client. In Mongo, un documento utente è composto da svariati campi. Per verificare, è sufficiente digitare dal terminale Mongo:

> db.users.findOne()
{
    "createdAt" : 1365649830922,
    "_id" : "kYdBd9hr3fWPGPcii",
    "services" : {
        "password" : {
            "srp" : {
                "identity" : "qyFCnw4MmRbmGyBdN",
                "salt" : "YcBjRa7ArXn5tdCdE",
                "verifier" : "df2c001edadf4e475e703fa8cd093abd4b63afccbca48fad1d2a0986ff2bcfba920d3f122d358c4af0c287f8eaf9690a2c7e376d701ab2fe1acd53a5bc3e843905d5dcaf2f1c47c25bf5dd87764d1f58c8c01e4539872a9765d2b27c700dcdedadf5ac82521467356d3f91dbeaf9848158987c6d359c5423e6b9cabf34fa0b45"
            }
        },
        "resume" : {
            "loginTokens" : [
                {
                    "token" : "BMHipQqjfLoPz7gru",
                    "when" : 1365649830922
                }
            ]
        }
    },
    "username" : "tmeasday"
}
Console di Mongo

Al lato opposto, nel browser l'oggetto utente è molto più scarno, come è possibile verificare digitando il comando equivalente:

 Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}
Console del browser

Questo esempio evidenzia come una collezione locale possa essere, per ragioni di sicurezza, un sottoinsieme dei dati presenti lato server. L'utente correntemente loggato accede solo al minimo indispensabile per un preciso scopo (nel caso in questione, l'autenticazione). Questo è uno schema ricorrente da tenere in considerazione, come si vedrà in seguito.

Quando detto però non vuol dire che non sia possibile rendere pubblici più dati utente, se si vuole. Si rimanda alla documentazione Meteor per sapere come pubblicare campi aggiuntivi della collezione Meteor.users.

Reattività

Sidebar 6.5

Se le collezioni sono la caratteristica alla base di Meteor, possiamo dire che la reattività è l'involucro le rende davvero utili.

L'uso delle collezioni trasforma radicalmente il modo in cui la nostra applicazione gestisce le modifiche ai dati. Invece di controllare manualmente se ci sono state modifiche ai dati (es. attraverso una chiamata AJAX) e poi manipolare il codice HTML per riflettere queste modifiche, le modifiche ai dati possono avvenire in qualsiasi momento e Meteor si preoccuperà di modificare l'interfaccia utente di conseguenza.

Fermiamoci un attimo a pensarci: dietro le quinte Meteor è in grado di modificare qualsiasi parte dell'interfaccia utente quando la collezione di dati rappresentata cambia.

Lo stile imperativo per farlo sarebbe quello di usare .observe(), una funzione del nostro cursore dati che scatena delle callback quando i documenti che corripondono a quel cursore cambiano. A quel punto modifichiamo il DOM (cioè la rappresentazione dell'HTML nella pagina web) usando queste callback. Il codice sarebbe qualcosa del genere:

Posts.find().observe({
  added: function(post) {
    // when 'added' callback fires, add HTML element
    $('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
  },
  changed: function(post) {
    // when 'changed' callback fires, modify HTML element's text
    $('ul li#' + post._id).text(post.title);
  },
  removed: function(post) {
    // when 'removed' callback fires, remove HTML element
    $('ul li#' + post._id).remove();
  }
});

Penso si possa già vedere come questo codice tenderà a diventare rapidamente molto complesso. Immagina di dover gestire le modifiche ad ogni singolo attributo del post, dovendo di conseguenza cambiare del complesso codice HTML nei tag <li> del post. E non abbiamo ancora neanche preso in considerazione tutti i casi particolari che si verificherebbero quando rappresentiamo sorgenti dati multiple che possono cambiare contemporaneamente in tempo reale.

Quando Invece Dovremmo Usare observe()?

Lo schema di programmazione visto sopra a volte è necessario, specialmente se abbiamo a che fare con componenti di terze parti. Immaginiamo ad esempio di voler aggiungere o rimuovere punti su una mappa in tempo reale basandoci sui dai di una collezione (diciamo per mostrare la posizione degli utenti loggati).

In questo caso abbiamo bisogno di usare le callback di observe() per far “parlare” la mappa con la collezione di Meteor e sapere come reagire alle modifiche dei dati. Ad esempio faremmo affidamento a delle callback aggiunto e rimosso per invocare i metodi mettiPin() o togliPin dalle API della mappa.

Un Approccio Dichiarativo

Meteor sa farlo molto meglio: con la reattività, che è basata su un approccio dichiarativo. Con uno stile dichiarativo possiamo definire le relazioni tra gli oggetti una volta per tutte e aspettarci che tutto rimanga sincronizzato, invece di specificare cosa fare ad ogni cambiamento.

L'idea di fondo è estremamente potente, perché un sistema in tempo reale ha diversi input che possono modificarsi anche contemporaneamente in maniera non prevedibile. Definendo dichiarativamente come rappresentare il codice HTML in base a quali sorgenti dati reattive ci interessano, Meteor si preoccuperà di monitorare queste sorgenti dati è fare per noi il complesso lavoro di aggiornamento dell'interfaccia in maniera trasparente.

Tutto ciò per dire che invece di usare le callback con observe, Meteor ci permette di scrivere:

<template name="postsList">
  <ul>
    {{#each posts}}
      <li>{{title}}</li>
    {{/each}}
  </ul>
</template>

E poi ricavare la nostra lista di post con:

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});

Dietro le quinte Meteor collega ed esegue le callback di observe() per noi, e ridisegna la parte di HTML interessata quando le sorgenti dati reattive si modificano.

Gestione Delle Dipendenze in Meteor: le Computation

Sebbene Meteor sia un framework reattivo e real-time non tutto il codice di una applicazione è reattivo. Se così fosse l'intero codice dell'applicazione sarebbe rieseguito ogni volta che si verifica un cambiamento. Al contrario, la reattività è limitata a specifiche parti di codice, dette computation.

In altre parole una computation è un blocco di codice che viene eseguito ogni volta che cambia la sorgente dati collegata. Se abbiamo una sorgente dati reattiva (ad esempio una variabile dell'oggetto Session) e vogliamo reagire reattivamente alle sue variazioni, dobbiamo scrivere una computation che la contiene.

Normalmente non c'è bisogno di scriverla esplicitamente perché Meteor fornisce già ogni template la sua specifica computation (e quindi il codice negli helper dei template a nelle callback è sempre reattivo)

Tutte le sorgenti dati reattive tengono traccia delle computation che le usano in modo da informarle quando i valori cambiano. Per farlo invocano la funzione invalidate() della computation.

Le computation generalmente sono scritte in modo da rieseguire il proprio contenuto quando vengono invalidate, ed è ciò che accade alle computation dei template (oltre a questo usano dei trucchi per ridisegnare la pagina in maniera efficiente). Se ce ne fosse bisogno potremmo controllare più dettagliatamente il comportamento delle computation quando vengono invalidate, in pratica però nell'uso comune, faremo solo questo.

Come scrivere una Computation

Ora che conosciamo la teoria alla base delle computation, scriverne una sembrerà incredibilmente semplice. Usiamo semplicemente la funzione Deps.autorun per racchiudere un blocco di codice e renderlo reattivo, creando una computation:

Meteor.startup(function() {
  Deps.autorun(function() {
    console.log('There are ' + Posts.find().count() + ' posts');
  });
});

Notiamo che bisogna racchiudere il blocco Deps all'interno di un blocco Meteor.startup() in modo da assicurarci che venga eseguito solo dopo che Meteor ha finito di caricare la collezione Posts.

Dietro le quinte autorun crea una computation, e si preoccupa di rieseguirla ogni volta che cambia la sorgente dati collegata. Abbiamo scritto una computation che semplicemente scrive nella console il numero di post. Poichè Posts.find() è una sorgente dati reattiva, si preoccuperà di informare la computation che è necessaria una riesecuzione ogni volta che il numero di post cambia.

> Posts.insert({title: 'New Post'});
There are 4 posts.

Il risultato di tutto ciò è che possiamo scrivere codice che usa dati reattivi in maniera molto naturale, consapevoli che dietro le quinte un sistema di dipendenze si preoccuperà di rieseguirlo al momento opportuno.

Creare i post

7

Abbiamo visto come è facile creare dei post nella console, usando la richiesta per il database ‘Post.insert’, ma non possiamo aspettarci che gli utenti aprano la console per creare un nuovo post.

Eventualmente, dovremo costruire un'interfaccia utente che consenta agli utenti di inviare nuove storie sulla nostra applicazione.

Costruire la pagina 'Nuovo Post’

Iniziamo col definire un percorso ('route’) per la nuova pagina:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

Router.onBeforeAction('loading');
lib/router.js

Stiamo usando la funzione 'data’ per impostare il contesto dati del template 'postPage’. Ricorda che qualsiasi cosa inseriamo nel contesto 'data’, sarà disponibile come 'this’ negli helper del template.

Aggiungere un link nella testata

Adesso, avendo definito quel percorso, possiamo aggiungere un link nella nostra testata che colleghi alla pagina d'invio:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li><a href="{{pathFor 'postSubmit'}}">New</a></li>
        </ul>
        <ul class="nav pull-right">
          <li>{{> loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Impostare il nostro percorso significa che se l'utente accede all'URL ’/submit’, Meteor mostrerà il template 'postSubmit’. Scriviamo quindi il template:

<template name="postSubmit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="message">Message</label>
        <div class="controls">
            <textarea name="message" type="text" value=""></textarea>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary"/>
        </div>
    </div>
  </form>
</template>

client/views/posts/post_submit.html

Nota: qui c'è molta markup, ma è dovuto al fatto che stiamo utilizzando Twitter Bootstrap. Anche se l'unica cosa essenziale sono gli elementi del modulo, la markup aggiuntiva ci aiuterà a rendere la nostra applicazione più presentabile. Adesso dovrebbe apparire così:

The post submit form
The post submit form

Questo è un modulo semplice. Non dobbiamo preoccuparci di aggiungerci una funzione perché intercetteremo eventi d'invio nel modulo stesso ed aggiorneremo i dati via JavaScript. (Non ha senso procurare un non-JS fallback se consideri che un'applicazione Meteor non funzionerebbe senza JavaScript).

Creare i Post

Fissiamo un event handler all'evento 'submit’ del modulo. È preferibile intercettare questo evento, anziché l'evento 'click’ sul bottone, perché coprirà ogni possibile modo di invio, premendo 'invio’ nel form, ad esempio.

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    post._id = Posts.insert(post);
    Router.go('postPage', post);
  }
});
client/views/posts/post_submit.js

Commit 7-1

Added a submit post page and linked to it in the header.

Questa funzione usa jQuery per analizzare i valori dei vari campi modulo e riempire l'oggetto post con i risultati. Dobbiamo accertarci che 'preventDefault’ sia impostato sull'argomento 'event’ del nostro gestore (handler) così da evitare che il browser invii il modulo.

Finalmento possiamo fare rotta verso la pagina del nostro nuovo post. La funzione 'insert()’ su una collezione ritorna l’'id’ generato per l'oggeto che è stato inserito nel database e verrà utilizzata dalla funzione 'go()’ del Router per creare un URL a cui potremo accedere.

Il risultato è che l'utente preme invio, il post viene creato e l'utente viene diretto istantaneamente alla pagina di discussione relativa al nuovo post.

Aggiungere sicurezza

Creare i post va bene, ma non possiamo permettere che un qualsiasi visitatore lo faccia: vogliamo che siano connessi. Certo, potremmo iniziare col nascondere i moduli per un nuovo post agli utenti non connessi. Tuttavia, l'utente potrebbe presubimilmente creare un post nella console del browser senza essersi connesso e questo non possiamo permetterlo.

Fortunatamente la sicurezza dei dati è intrinseca alle collezioni di Meteor; è solo che è disattivata per impostazione predefinita quando crei un nuovo progetto. Ciò ti permette di fare i primi passi con facilità e di aggiungere strati alla tua applicazione lasciando gli aspetti più noiosi a dopo.

Adesso, la nostra applicazione non ha più bisogno delle rotelle di addestramento quindi leviamole! Rimuoviamo il pacchetto 'insecure’:

$ meteor remove insecure
Terminal

Fatto questo, vedrai che il modulo del post non funzionerà più. Il motivo è che senza il pacchetto 'insecure’, non sono più permessi insert, in nessuna collezione, lato client. O diamo degli ordini precisi a Meteor per quando permettere all'utente di inserire i post o li inseriamo lato server con dei metodi specifici, che vedremo in seguito.

Allowing Post Inserts

Per iniziare, vi dimostreremo come permettere inserimenti dal client per far nuovamente funzionare il nostro modulo. Come scopriremo più avanti, la tecnica utilizzata alla fine sarà un'altra, ma per il momento, quanto segue, farà funzionare le cose facilmente:

Posts = new Meteor.Collection('posts');

Posts.allow({
  insert: function(userId, doc) {
    // only allow posting if you are logged in
    return !! userId;
  }
});
collections/posts.js

Commit 7-2

Removed insecure, and allowed certain writes to posts.

'Posts.allow’ comunica a Meteor che “queste sono le circostanze sotto le quali i client possono agire sulla collezione 'Posts’”. In questo caso specifico stiamo dicendo “i client possono inserire i post purché abbiano un 'userId’ ”.

L’'userId’ dell'utente che sta apponendo la modifica viene passata alle chiamate 'allow’ e 'deny’ (o ritorna 'null’ se nessun utente è connesso) ed è quasi sempre utile. Dal momento che gli account degli utenti sono legati al nucleo di Meteor, possiamo confidare nel fatto che 'userId’ sia sempre corretta.

Siamo riusciti a garantire la necessità di essere connessi per creare un post. Proviamo a creare un post dopo aver effetuato un log out; dovremmo vedere questo:

Insert failed: Access denied
Insert failed: Access denied

Tuttavia, abbiamo ancora un paio di problemi da rsolvere:

  • Utenti non connessi possono ancora accedere al modulo per creare post.
  • Il post non è legato all'utente in nessun modo (e non c'è nemmeno codice sul server per farlo applicare)
  • È possibile creare post multipli che condividono lo stesso URL

Risolviamo questi problemi:

Garantire Accesso Al Modulo Per Il Nuovo Post

Iniziamo con l'impedire agli utenti non connessi di vedere il modulo d'invio del post. Per far questo accederemo al livello del router, definendo un route hook.

Un hook (gancio) intercetta il processo di routing e potenzialmente cambia l'azione che esegue il router. Puoi pensarla come una guardia di sicurezza che controlla i tuoi documenti prima di farti passare (o respingerti).

Dobbiamo controllare che l'utente sia connesso e se non lo fosse, attivare il template 'accesDenied’ invece del previsto 'postSubmit’ (poi fermiamo il router dal fare nient'altro). Modifichiamo router.js così:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});


var requireLogin = function(pause) {
  if (! Meteor.user()) {
    this.render('accessDenied');
    pause();
  }
}

Router.onBeforeAction('loading');
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
lib/router.js

Creiamo anche il template per la pagina 'accesso negato’:

<template name="accessDenied">
  <div class="alert alert-error">You can't get here! Please log in.</div>
</template>
client/views/includes/access_denied.html

Commit 7-3

Denied access to new posts page when not logged in.

Se adesso vai su http://localhost:3000/submit/ senza essere connesso, dovresti vedere questo:

The access denied template
The access denied template

Il bello dei routing hooks (ganci) è che sono reattivi. Ciò significa che possiamo essere dichiarativi e non dobbiamo preoccuparci di callbacks o simili quando l'utente si connette. Quando lo stato di registrazione dell'utente cambia, il template della pagina del Router cambia istantaneamente a sua volta da 'accessDenied’ a 'postSubmit’ senza dover scrivere una linea di codice per gestirlo.

Connettiti e prova a ricaricare la pagina. Può capitare di veder lampeggiare brevemente il template di accesso negato prima che la pagina d'invio post appaia. Ciò accade perché Meteor esegue il rendering dei template appena può, ancor prima di parlare con il server e verificare che l'utente (salvato nella memoria locale del browser) esista.

Per evitare questo problema (un problema molto comune che incontrerai spesso quando affronterai le complessità di latenza tra client e server), mostreremo una pagina di caricamento durante il breve istante necessario a verificare se l'utente ha accesso oppure no.

Dopo tutto a questo punto non sappiamo ancora se l'utente ha le corrette credenziali di registrazione e non possiamo mostrare i template di 'accesDenied’ o 'postSubmit’ prima di averlo confermato.

Modifichiamo quindi il nostro hook al fine di utilizzare il template di caricamento mentre 'Meteor.logginIn()’ è true??? :

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});


var requireLogin = function(pause) {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render(this.loadingTemplate);
    else
      this.render('accessDenied');

    pause();
  }
}

Router.onBeforeAction('loading');
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
lib/router.js

Commit 7-4

Show a loading screen while waiting to login.

Nascondere il Link

Il modo più semplice per prevenire che l'utente non connesso possa accedere a questa pagina per sbaglio è di nascondere il link. È molto facile:

<ul class="nav">
  {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
client/views/includes/header.html

Commit 7-5

Only show submit post link if logged in.

L'helper 'currentUser’ viene fornito dal pacchetto 'accounts’ ed è l'equivalente di 'Meteor.user()’ per Spacebars. Dal momento che è reattivo, il Link appare o scompare al momento della connessione o disconnessione dall'applicazione.

Meteor Method: Miglior sicurezza e astrazione

Siamo riusciti a garantire accesso alla pagina del nuovo post per utenti non connessi e negarli dal creare post nuovi anche se provassero ad imbrogliare usando la console. Detto questo, ci sono però ancora un paio di cose da aggiustare:

  • Data e ora per i post.
  • Garantire che lo stesso URL non sia postato più di una volta.
  • Aggiungere informazioni sull'autore del post (ID, nome utente, eccetera).

Potresti pensare di poter eseguire tutto questo all'interno del gestore di eventi 'submit’. In realtà, incorreremmo in diversi problemi di varia natura.

  • Per data e ora, dovremmo affidarci all'accuratezza dell'orologio nel computer dell'utente, ma non è sempre attendibile.
  • I clients non saranno sempre al corrente di tutti gli URL postati sul sito. Sapranno solo dei post che sono visibili in quel momento (più tardi vedremo come funziona esattamente) quindi non c'è modo di forzare l'unicità del'URL lato client.
  • Infine, anche se potessimo aggiungere informazioni sull'utente lato client, non potremmo imporne l'accuratezza perché potrebbe lasciare l'applicazione suscettibile ad attacchi provenienti dalla console del browser.

Per questi motivi sopra indicati, è consigliabile mantenere i gestori di eventi semplici e se vogliamo eseguire inserzioni più sofisticate o aggiornare collezioni, usiamo un Method.

Un Meteor Method è una funzione server-side chiamata dal client. Non ci sono totalmente sconosciuti, infatti, dietro le quinte, l’'insert’ di 'Collection’ e le funzioni 'update’, 'remove’ sono tutti Methods. Vediamo come creare il nostro.

Torniamo a 'post_submit.js’. Invece che inserirlo direttamente nella collezione 'Posts’, richiamiamo un Method chiamato 'post’:

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);

      Router.go('postPage', {_id: id});
    });
  }
});
client/views/posts/post_submit.js

La funzione 'Meteor.call’ richiama un Method nominato dal suo primo argomento. Puoi fornire argomenti alla chiamata (in questo caso l'oggetto 'post’ costruito dal modulo) ed infine allegare un callback che viene chiamato (execute) quando il Method del server è concluso. Qui stiamo semplicemente allertando l'utente nel caso ci fosse un problema oppure ridirigerlo alla pagina di discussione del post appena creato nel caso non ci fosse.

Definiamo poi il Method nel file 'collections/posts.js’. Rimuoviamo il blocco 'allow()’ da 'posts.js’ considerando che Metheor Methods li bypassano comunque. Ricordiamoci che i Methods sono eseguiti sul server quindi Meteor assume che siano attendibili.

Posts = new Meteor.Collection('posts');

Meteor.methods({
  post: function(postAttributes) {
    var user = Meteor.user(),
      postWithSameLink = Posts.findOne({url: postAttributes.url});

    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to post new stories");

    // ensure the post has a title
    if (!postAttributes.title)
      throw new Meteor.Error(422, 'Please fill in a headline');

    // check that there are no previous posts with the same link
    if (postAttributes.url && postWithSameLink) {
      throw new Meteor.Error(302,
        'This link has already been posted',
        postWithSameLink._id);
    }

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime()
    });

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Commit 7-6

Use a method to submit the post.

Questo Method è un po’ complicato ma dovresti riuscire a comprenderlo.

Prima di tutto definiamo la variabile 'user’ e controlliamo se c'è un altro post con lo stesso link. Verifichiamo che l'utente sia connesso e nel caso non lo fosse lanciamo un'errore (allertato con 'alert’). Eseguiamo anche una piccola convalida dell'oggetto post per assicurarsi che i nostri post abbiano un titolo.

Dopo, se ci fosse un altro post con lo stesso link, lanciamo un errore '302’ (che reindirizza) informando l'utente che dovrebbero andare a guardare il post creato precedentemente.

La classe 'Error’ di Meteor ha tre argomenti. Il primo ('error’) sarà il codice numerico '302’, il secondo ('reason’) è una breve spiegazione leggibile dell'errore e l'ultimo ('details’) può essere ogni utile informazione aggiuntiva.

Nel nostro caso, useremo il terzo argomento per passare l'ID del post che abbiamo appena trovato. Spoiler alert: lo useremo più tardi per reindirizzare l'utente al post preesistente.

Se tutte queste verifiche passano, prendiamo i campi che vogliamo inserire ed includiamo alcune informazioni sull'utente che sta pubblicando – inclusa l'ora attuale – nel post.

Infine, inseriamo il post e diamo l’'id’ del nuovo post all'utente.

Mettere i post in ordine

Ora che abbiamo una data per ogni post, ha senso garantire che siano messi in ordine con questo attributo. Per fare ciò, possiamo usare l'operatore 'sort’ di Mongo, che si aspetta di ricevere un oggetto composto dalle chiavi usate per riordinare ed un simbolo che indichi se siano ascendenti o discendenti.

Template.postsList.helpers({
  posts: function() {
    return Posts.find({}, {sort: {submitted: -1}});
  }
});
client/views/posts/posts_list.js

Commit 7-7

Sort posts by submitted timestamp.

C'è voluto un po’ di lavoro ma abbiamo finalmente un'interfaccia utente che permetterà agli utenti di inserire del contenuto nella nostra applicazione!

Detto questo, ogni applicazione che permetta all'utente di creare contenuti deve anche dargli modo di modificarli e cancellarli. Questo sarà oggetto del prossimo capitolo 'Modificare i Post’.

Compensazione della latenza

Sidebar 7.5

Nell'ultimo capitolo abbiamo introdotto un nuovo concetto nell'universo di Meteor: i Metodi.

Without latency compensation
Without latency compensation

Un Metodo di Meteor è un modo per eseguire una serie di comandi sul server in maniera strutturata. Nel nostro esempio abbiamo usato un Metodo perché vogliamo che i nuovi post siano taggati con il nome e l'id dell'autore e con l'orario corrente del server.

Tuttavia se Meteor eseguisse i Metodi nel modo più semplice avremmo un problema. Considerate questa sequenza di eventi (nota: i timestamp sono valori casuali presi per semplice scopo illustrativo):

  • +0ms: L'utente clicca su un bottone di submit e il browser scatena una chiamata al Metodo.
  • +200ms: Il server esegue le modifiche sul database di Mongo.
  • +500ms: Il client riceve le modifiche e aggiorna l'interfaccia utente per visualizzarle.

Se questo fosso il modo in cui funziona Meteor ci sarebbe un breve ritardo tra l'esecuzione di queste azioni e il risultato (il ritardo sarebbe più o meno sensibile, dipende da quanto vicini siete al server). Non è accettabile in una moderna applicazione web!

Compensazione di latenza

With latency compensation
With latency compensation

Per evitare questo problema, Meteor introduce un concetto chiamato Compensazione di latenza. Quando abbiamo definito il nostro Metodo post, l'abbiamo messo un un file nella cartella collections/. Questo significa che è disponibile sia sul server che sul client – e verrà eseguito su entrambi allo stesso tempo!

Quando fate una chiamata a un Metodo, il client invia la chiamata al server, ma contemporaneamente simula l'azione del Metodo sulle collezioni del client. Il processo ora è questo:

  • +0ms: L'utente clicca su un bottone di submit e il browser scatena una chiamata al Metodo.
  • +0ms: Il client simula l'azione della chiamata al Metodo sulle collezioni lato client e aggiorna l'interfaccia per visualizzarle.
  • +200ms: Il server esegue le modifiche sul database di Mongo.
  • +500ms: Il client riceve le modifiche e annulla le modifiche simulate rimpiazzandole con quelle del server (che sono di solito le stesse). L'interfaccia si modifica per visualizzarle.

Questo fa in modo che l'utente veda le modifiche istantaneamente. Quanto la risposta del server arriva qualche momento più tardi, possono esserci o meno delle modifiche sensibili ai documenti che il server invia. Una cosa da imparare è che dovete cercare di simulare i reali documenti il più fedelmente possibile.

Osservare la compensazione di latenza

Possiamo fare una piccola modifica alla chiamate del Metodo post per vederlo in azione. Per farlo dovremo fare un po’ di codice avanzato con il pacchetto npm futures per ritardare l'inserimento di oggetti nel nostro Metodo.

Useremo isSimulation per chiedere a Meteor se il Metodo è chiamato come un abbozzo. Un abbozzo è la simulazione di un Metodo che Meteor esegue sul client in parallelo, mentre il Metodo “reale” viene eseguito sul server.

In questo modo chiediamo a Meteor se il codice si sta eseguendo sul client. Se è così aggiungiamo la stringa (client) alla fine del titolo del post. Se non è così, aggiungiamo la stringa (server):

Meteor.methods({
  post: function(postAttributes) {
    // […]

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'message'), {
      title: postAttributes.title + (this.isSimulation ? '(client)' : '(server)'),
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime()
    });

    // wait for 5 seconds
    if (! this.isSimulation) {
      var Future = Npm.require('fibers/future');
      var future = new Future();
      Meteor.setTimeout(function() {
        future.return();
      }, 5 * 1000);
      future.wait();
    }

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Nota: in caso ve lo stiate chiedendo, il this in this.isSimlation è un oggetto di invocazione di Metodo che provvede accesso a svariate e utili variabili.

Come funziona esattamente Futures esula dagli argomenti di questo libro, ma abbiamo praticamente detto a Meteor di aspettare 5 secondi prima di inserire il documento nella collezione sul server.

Abbiamo fatto anche un reindirizzamento alla lista di post:

Template.postSubmit.events({
  'submit form': function(event) {
    event.preventDefault();

    var post = {
      url: $(event.target).find('[name=url]').val(),
      title: $(event.target).find('[name=title]').val(),
      message: $(event.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);
    });
    Router.go('postsList');
  }
});
client/views/posts/post_submit.js

Commit 7-5-1

Demonstrate the order that posts appear using a sleep.

Se creiamo un post ora vedremo la compensazione di latenza in azione. Prima viene inserito un post con (client) nel titolo (il primo post nella lista che rimanda a GitHub):

Our post as first stored in the client collection
Our post as first stored in the client collection

Cinque secondi più tardi, viene rimpiazzato dal domento reale che è stato inserito sul server:

Our post once the client receives the update from the server collection
Our post once the client receives the update from the server collection

Metodi delle collezioni sul client

Potete pensare che i Metodi siano complicati dopo di questo, ma in realtà possono essere abbastanza semplici. Abbiamo visto tre semplici Metodi finora: i Metodi dei cambiamenti in una collezione, insert, update e remove.

Quando definite una collezione sul server chiamata 'posts', state implicitamente definendo tre Metodi: posts/insert, posts/update and posts/delete. In altre parole, quando chiamate Posts.insert() sulla vostra collezione lato client, state eseguendo un Metodo con compensazione di latenza che fa due cose:

  1. Controlla che possiamo fare il cambiamento chiamando le callback allow e deny (questo non avviene durante la simulazione).
  2. Rende effettivi i cambiamenti nel sottostante archivio dati.

Metodi che chiamano Metodi

Se state riuscendo a seguire, potete aver notato che il nostro Metodo post sta chiamando un altro Metodo (posts/insert) quando inseriamo un nuovo post. Come funziona?

Quando la simulazione (la versione lato client del Metodo) viene eseguita, eseguiamo la simulazione di insert (in modo da inserire il documento nella collezione lato client), ma non chiamiamo la versione lato server di insert, perché ci aspettiamo che sia la versione server di post a farlo.

Di conseguenza quando il Metodo lato server post chiama insert non c'è bisogno di preoccuparsi della simulazione, e l'inserimento prosegue senza problemi.

Modificare i post

8

Il prossimo passo, ora che abbiamo creato dei messaggi, è essere in grado di modificarli e cancellarli. Mentre il codice per l'interfaccia utente (UI) è abbastanza semplice, questo sarebbe un momento buono per parlare di come Meteor gestisce i permessi utente.

Prima di tutto agganciamo il nostro router. Aggiungeremo una route per accedere alla pagina per modificare i messaggi e impostare il contesto dei dati (data context)

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function(pause) {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render('loading')
    else
      this.render('accessDenied');

    pause();
  }
}

Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
lib/router.js

Il template per la modifica messaggio

Ora possiamo concentrarci sul template. Il nostro template postEdit sarà un form abbastanza standard:

<template name="postEdit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="{{url}}" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="{{title}}" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary submit"/>
        </div>
    </div>
    <hr/>
    <div class="control-group">
        <div class="controls">
            <a class="btn btn-danger delete" href="#">Delete post</a>
        </div>
    </div>
  </form>
</template>
client/views/posts/post_edit.html

E qui c'è il gestore post_edit.js che và assieme:

Template.postEdit.events({
  'submit form': function(e) {
    e.preventDefault();

    var currentPostId = this._id;

    var postProperties = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    }

    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // display the error to the user
        alert(error.reason);
      } else {
        Router.go('postPage', {_id: currentPostId});
      }
    });
  },

  'click .delete': function(e) {
    e.preventDefault();

    if (confirm("Delete this post?")) {
      var currentPostId = this._id;
      Posts.remove(currentPostId);
      Router.go('postsList');
    }
  }
});
client/views/posts/post_edit.js

A questo punto la maggior parte di questo codice dovrebbe essere familiare. Prima di tutto, abbiamo il nostro template helper che prende il messaggio corrente e lo passa al template.

Abbiamo inoltre due callbacks per gli eventi di template: una per l'evento submit del form e una per l'evento click del link per cancellare.

La callback per cancellare è estremamente semplice: sopprimiamo l'evento click predefinito e chiediamo una conferma. Se la riceviamo, otteniamo l'ID del messaggio corrente dal contesto dei dati (data context) del template, lo cancelliamo, e dopodichè reindirizziamo l'utente alla pagina principale.

La callback di aggiornamento è un pò più lunga, ma non troppo complicata. Dopo aver soppresso l'evento predefinito e aver preso il messaggio corrente, prendiamo i nuovi valori del form dalla pagina e li memorizziamo in un oggetto postProperties.

A quel punto passiamo quest'oggetto al metodo Collection.update() di Meteor e usiamo una callback che o visualizza un errore nel caso l'aggiornamento fallisce, oppure manda indietro l'utente alla pagina del messaggio se l'aggiornamento riesce.

Aggiungere i links

Dovremmo anche aggiungere dei links di modifica ai nostri messaggi, così che gli utenti possano accedere alla pagina di modifica:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}}
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Ovviamente non vogliamo mostrare un link di modifica nel form di qualcun'altro. È qui che l'helper ownPost entra in scena:

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js
Form di modifica messaggio.
Form di modifica messaggio.

Commit 8-1

Added edit posts form.

Il nostro form di modifica sembra buono, ma in realtà non si può modificare niente. Che sta succedendo?

Impostare i permessi

Siccome precedentemente abbiamo rimosso il pacchetto (package) insecure, tutte le modifiche fatte sulla parte del client vengono negate.

Per risolvere questo problema, imposteremo alcune regole di permessi. Prima di tutto, crea un nuovo permissions.js file all'interno di lib. Questo carica la nostra logica dei permessi (ed è disponibile in entrambi gli ambienti):

// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
  return doc && doc.userId === userId;
}
lib/permissions.js

Nel capitolo Creare post abbiamo rimosso la funzione allow() in quanto le modifiche al database avvenivano solamente sul server tramite un metodo Meteor, e, in questi casi, le direttive .allow() e .deny() vengono ignorate.

Ora che, invece, editiamo contenuti sul client è necessario specificare chi ha i permessi di modificare gli elementi della collezione inserendo il metodo .allow() nel file posts.js.

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Meteor.methods({
  ...
collections/posts.js

Commit 8-2

Added basic permission to check the post’s owner.

Limitare le modifiche

Solo per il fatto che puoi modificare i tuoi messaggi, non vuol dire che puoi modificare ogni proprietà. Per esempio, non vogliamo che gli utenti possano creare un messaggio e assegnarlo a qualcun'altro.

deny() è la callback che in Meteor viene usata per assicurarsi che gli utenti possono modificare solamente certi campi.

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Posts.deny({
  update: function(userId, post, fieldNames) {
    // may only edit the following two fields:
    return (_.without(fieldNames, 'url', 'title').length > 0);
  }
});
collections/posts.js

Commit 8-3

Only allow changing certain fields of posts.

Stiamo parlando dell'array fieldNames che contiene una lista dei campi modificati, e usando il metodo without() di Underscore per ritornare un sotto-array contenente i campi che non sono url oppure title.

Se tutto è normale, l'array dovrebbe essere vuoto e la sua lunghezza dovrebbe essere 0. Se qualcuno sta cercando di fare qualcosa di bizzarro, la lunghezza dell'array sarà 1 oppure più grande e la callback ritornerà true (di conseguenza negando l'aggiornamento).

Chiamate dei metodi vs manipolazione dei dati sul lato client.

Per creare dei messaggi, stiamo usando un metodo post, mentre per modificarli e cancellarli stiamo chiamando update e remove direttamente sul client, limitando l'accesso tramite allow e deny.

Quando è opportuno uno ma non l'altro?

Quando le cose sono relativamente semplici e puoi esprimere adeguatamente le tue regole tramite allow e deny, è solitamente più semplice fare le cose direttamente sul client.

Manipolare i dati del database direttamente crea una percezione di immediatezza e permette di costruire una user experience migliore, fintanto che ci si ricorda di gestire i casi di fallimento in maniera graziosa (es. quando il server ritorna indietro che i cambiamenti alla fin fine non sono riusciti).

Però è probabilmente meglio usare un metodo non appena inizi ad esigere di dover fare cose che dovrebbero essere fatte senza il controllo dell'utente (ad esempio aggiungere un timestamp per un nuovo messaggio, oppure assegnarlo all'utente giusto).

Le chiamate ai metodi sono anche più appropriate in alcuni altri scenari:

  • Quando devi sapere o ritornare dei valori tramite callback anzichè aspettare per la reactivity (reattività) e sincronizzazione si propaghi.
  • Per delle funzioni di database pesanti che sarebbero troppo costose da inviare tramite una grande collection (collezione).
  • Per sommarizzare oppure aggregare dati (es. contare, fare la media, sommare).

Allow e Deny

Sidebar 8.5

Il sistema di sicurezza di Meteor ci permette di controllare le modifiche al database senza dover definire dei Metodi ogni volta che facciamo una modifica.

Dato che abbiamo bisogno di eseguire operazioni ausiliarie come decorare il post con proprietà aggiuntive a eseguire azioni speciali quando l'URL del post viene pubblicato, ha senso utilizzare uno specifico Metodo post quando creiamo un post.

D'altra parte non abbiamo bisogno di creare nuovi Metodi per modificare o eliminare i post. Dobbiamo solo controllare che l'utente abbia i permessi per eseguire queste azioni, e ciò avviene facilmente attraverso le callback allow e deny.

Usare queste callback ci permette di essere più dichiarativi riguardo alle modifiche al database e di poter dire quali tipi di modifiche possono essere effettuate. Il fatto che si integrino con il sistema di account è un bonus aggiuntivo.

Callback multiple

Possiamo definire multiple callback allow, l'importante è che almeno una di esse ritorni true per la modifica che sta eseguendo. In questo modo quando Posts.insert viene chiamata in un browser (non importa che sia dall'interfaccia client o dalla console), il server di conseguenza chiamerà ogni insert fino ache non ne trova uno che ritorna true. Se non ne trova, non da permesso all'inserimento e ritorna un errore 403 al client.

Allo stesso modo, possiamo definire più di una callback deny. Se una qualunque di esse ritorna true, le modifiche vengono cancellate e viene ritornato un errore 403. Questo significa che per rendere effettivo un insert , saranno chiamate una o più callback allow insert e ogni callback deny insert.

Note: n/e stands for Not Executed
Note: n/e stands for Not Executed

In altre parole, Meteor si muove nella lista di callback partendo da quelle di tipo deny, in seguito quelle di tipo allow, eseguendole fino a che una di esse non ritorna true.

Un esempio pratico di questo schema è quello di avere due callback allow(), una che controlla se un post appartiene all'utente corrente e una seconda che controlla se questo utente ha i diritti di amministrazione. Se l'utente è un amministratore potrà sicuramente modificare ogni post, dato che almeno una delle callback ritornerà true.

Compensazione dela latenza

Ricordate che i Metodi che modificano il database (come .update()) hanno compensazione di latenza, proprio come ogni altro Metodo. Per esempio, se provate ad eliminare un post di cui non avete i permessi attraverso la console del browser, vedrete il post sparire brevemente nel momento in cui viene cancellato nella collezione locale, ma poi lo vedrete riapparire quando il server informa che il documento non è stato cancellato.

Certamente questo comportamento non è un problema quando viene generato dalla console (se gli utenti provano a mettere mano ai dati tramite console, non è certo un vostro problema quello che succede nei loro browser). Tuttavia dovete assicurarvi che non succeda nell'interfaccia utente. Ad esempio dovete fare in modo che un bottoni “Elimina” non siano visibili agli utenti che non hanno i permessi per eliminare.

Fortunatamente, dato che potete condividere il codice dei permessi tra client e server (ad esempio potete scrivere una funzione in una libreria chiamata canDeletePost(user, post) e metterla nella cartella condivisa /lib), questo non richiede molto codice extra.

Permessi lato server

Ricordatevi che il sistema di permessi si applica solo alle modifiche del database inizializzate dal client. Sul server Meteor assume che tutte le operazioni siano permesse.

Questo significa che se scrivete un Metodo Meteor deletePost lato server che può essere chiamato dal client, ognuno è in grado di cancellare qualsiasi post. Probabilmente non volete che accada a meno che non abbiate verificato i permessi utente all'interno del Metodo.

Usare “deny” come callback

Infine, un trucco che potete fare con deny è di usarlo sulle callback “onX”. Per esempio potete ottenere un timestamp lastModified con il seguente codice:

Posts.deny({
  update: function(userId, doc, fields, modifier) {
    doc.lastModified = +(new Date());
    return false;
  },
  transform: null
});

Dato che le callback deny vengono eseguite per ogni update che ha successo, sappiamo che queste callback saranno eseguite e opereranno le modifiche in maniera strutturata.

In realtà questa tecnica è un hack, dato che potreste volere eseguire le modifiche usando un Metodo. Tuttavia è comunque utile saperlo, e in futuro possiamo sperare che diventi disponibile una callback beforeUpdate.

Errori

9

Usare una finestra di dialogo alert() per avvisare un utente che c'è stato un problema con l'inserimento di un post è abbastanza insoddisfacente e di sicuro non contribuisce ad un'ottimale user experience. Si può fare certamente di meglio.

Si può costruire un meccanismo più versatile per riportare gli errori che di sicuro è una scelta migliore che farlo bloccando il flusso di utilizzo del browser.

Collezioni Locali

Verrà implementato un semplice sistema che tiene traccia di quali errori un utente ha visto e mostra quelli nuovi in un'area “flash” del sito. Questo pattern è utile per informare l'utente che è successo qualcosa senza però infastidire la sua esperienza nell'uso dell'applicazione.

Quello che verrà creato è un sistema simile ai messaggi flash che si trovano spesso nelle applicazioni Ruby on Rails, ma è più ingegnoso per il fatto che è implementato lato client e riconosce quando l'utente visualizza il messaggio.

Per iniziare, si crea una collezione per contenere gli errori. Dato che gli errori sono rilevanti solo nella sessione corrente e non devono in alcun modo essere persistenti, si crea qualcosa di nuovo, una collezione locale. Ciò significa che la collezione Errors esisterà solo nel browser, e non tenterà di sincronizzarsi sul server.

Per fare ciò, si crea l'errore in un file disponibile solo al client, con in nome della collezione impostato a null. Grazie a una funzione throwError, con la quale gli errori vengono inseriti nella nuova collezione locale:

// Local (client-only) collection
Errors = new Meteor.Collection(null);
client/helpers/errors.js

Ora che la collezione è creata, si aggiunge una funzione throwError per inserirvi gli errori. Non bisogna preoccuparsi di allow, deny o altro, perché si tratta di una collezione locale e non verrà salvata sul database di Mongo.

throwError = function(message) {
  Errors.insert({message: message})
}
client/helpers/errors.js

Il vantaggio di usare una collezione locale per contenere gli errori è che, come tutte le collezioni, è un elemento reattivo – il che significa che si possono mostrare gli errori nello stesso modo in cui si mostrano i dati delle altre collezioni.

Mostrare gli errori

Gli errori verranno mostrati al di sopra del layout principale:

<template name="layout">
  <div class="container">
    {{> header}}
    {{> errors}}
    <div id="main" class="row-fluid">
      {{> yield}}
    </div>
  </div>
</template>
client/views/application/layout.html

Si creino ora i template errors e error nel file errors.html:

<template name="errors">
  <div class="errors row-fluid">
    {{#each errors}}
      {{> error}}
    {{/each}}
  </div>
</template>

<template name="error">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
client/views/includes/errors.html

Template gemelli

Si noti che sono stati inseriti due template in un solo file. Fino ad ora si è cercato di mantenere la convenzione “un file, un template”, ma per come è fatto Meteor è possibile mettere tutti i template in un file solo e non ci sarebbero problemi di funzionamento (anche se si avrebbe un file main.html molto confuso!).

In questo caso, dato che entrambi i template degli errori sono molto piccoli, si fa un'eccezione e si inseriscono nello stesso file per avere un repository un po’ più pulito.

Ora si deve solo creare l'helper dei template e si è pronti per proseguire!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});
client/views/includes/errors.js

Commit 9-1

Basic error reporting.

Creare gli errori

Ora che si sa come mostrare gli errori, si deve crearne qualcuno perché si possa vedere qualcosa. Gli errori spesso si generano quando un utente inserisce nuovi contenuti, per cui il controllo viene inserito nella callback della creazione di un post per mostrare un messaggio per ogni errori che viene generato.

In aggiunta, se ritorna un errore 302 (che significa che esiste già un post con lo stesso URL), l'utente verrà reindirizzato al post esistente. Si ottiene l’_id del post esistente da error.details (si ricordi che l’_id del post è passato come terzo argomento details nella classe Error nel capitolo 7).

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error) {
        // display the error to the user
        throwError(error.reason);

        if (error.error === 302)
          Router.go('postPage', {_id: error.details})
      } else {
        Router.go('postPage', {_id: id});
      }
    });
  }
});
client/views/posts/post_submit.js

Commit 9-2

Actually use the error reporting.

Facciamo una prova: creiamo un post inserendo come URL http://meteor.com. Dato che questo URL è già collegato ad un altro post, dovrebbe vedersi un errore:

Triggering an error
Triggering an error

Rimuovere gli errori

Se avete provato a cliccare sul bottone di chiusura dell'errore, avrete visto l'errore sparire perché il bottone di chiusura scatena l'evento JavaScript di Twitter Bootstrap.

Quello che succede è che Bootstrap rimuove l'elemento <div> dell'errore dal DOM, ma non rimuove l'oggetto dell'errore dalla collezione di Meteor. Facciamo quindi in modo di ripulire la nostra collezione locale dopo che il messaggio di errore ha fatto il suo lavoro.

Come prima cosa, modificheremo la funzione throwError per includere una proprietà seen (visto). Questo ci aiuterà in seguito a tenere traccia del fatto che un errore sia stato visto o meno da un utente.

Una volta fatto, si può scrivere una semplice funzione clearErrors che rimuova gli errori “visualizzati”:

// Local (client-only) collection
Errors = new Meteor.Collection(null);

throwError = function(message) {
  Errors.insert({message: message, seen: false})
}

clearErrors = function() {
  Errors.remove({seen: true});
}
client/helpers/errors.js

Ripuliamo ora gli errori nel router in modo che navigando su un'altra pagina questi errori spariscano per sempre:

// ...

Router.onBeforeAction(requireLogin, {only: 'postSubmit'})
Router.onBeforeAction(function() { clearErrors() });
lib/router.js

Per fare in modo che la funzione clearErrors() faccia il proprio lavoro, gli errori devono essere marcati come seen. Per farlo in maniera appropriata dobbiamo tenere conto di un caso limite: quando viene alzato un errore e l'utente viene poi reindirizzato ad un'altra pagina (ad esempio quando si tenta di inserire un link duplicato), il reindirizzamento avviene in maniera istantanea. Ciò significa che l'utente non ha mai avuto modo di vedere l'errore prima che venga cancellato.

Qui è dove la proprietà seen viene in aiuto. Dobbiamo assicurarci che sia impostata a true solo se l'utente ha realmente visto l'errore.

Per farlo useremo Meteor.defer(). Questa funzione dice a Meteor di eseguire la propria callback “appena dopo” qualsiasi cosa stia accadendo al momento. Se può aiutare, si può considerare che usare defer() è come dire al browser di aspettare un millisecondo prima di procedere.

Si sta dicendo a Meteor di impostare seen a true un millisecondo dopo che il template errors e stato renderizzato. Ricordate che abbiamo detto che il reindirizzamento accade in maniera istantanea? Questo significa che il reindirizzamento avverrà prima della callback defer, che quindi non sarà mai eseguita.

È esattamente ciò che si stava cercando: se non viene eseguita l'errore non sarà mai marcato come seen, il che significa che non sarà eliminato, e quindi apparirà sulla pagina alla quale l'utente è stato reindirizzato esattamente come desiderato!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});

Template.error.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.update(error._id, {$set: {seen: true}});
  });
};
client/views/includes/errors.js

Commit 9-3

Monitor which errors have been seen, and clear on routing.

La callback rendered viene chiamata quando i template è renderizzato nel browser. Dentro la callback, this si riferisce all'istanza corrente del template, e this.data permette di accedere ai dati dell'oggetto che è al momento renderizzato (in questo caso, un errore).

Wow! Abbiamo fatto un sacco di lavoro per qualcosa che si spera gli utenti non vedano mai!

Creare un pacchetto con Meteorite

Sidebar 9.5

Dopo che abbiamo creato uno schema riutilizzabile per la gestione dei nostri errori, possiamo creare un pacchetto e condividerlo con gli altri sviluppatori della comunità di Meteor.

Come prima cosa dobbiamo preparare la struttura del pacchetto. Lo inseriamo in una cartella chiamate packages/errors/, in questo modo si crea un pacchetto personalizzato che l'applicazione utilizzerà automaticamente. (È possibile che abbiate notato che Meteorite installa i pacchetto tramite symlinks nella cartella packages/).

In seguito, creiamo il file package.js, nel quale viene detto a Meteor come deve utilizzare il pacchetto e quali simboli vengono esportati, cioè quali oggetti vengono resi disponibili all'applicazione.

Package.describe({
  summary: "A pattern to display application errors to the user"
});

Package.on_use(function (api, where) {
  api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');

  api.add_files(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');

  if (api.export)
    api.export('Errors');
});
packages/errors/package.js

Aggiungiamo tre file al pacchetto, prendendoli direttamente da Microscope e aggiungendo solo un namespace appropriato e un API leggeremente più chiara:

Errors = {
  // Local (client-only) collection
  collection: new Meteor.Collection(null),

  throw: function(message) {
    Errors.collection.insert({message: message, seen: false})
  },
  clearSeen: function() {
    Errors.collection.remove({seen: true});
  }
};

packages/errors/errors.js
<template name="meteorErrors">
  {{#each errors}}
    {{> meteorError}}
  {{/each}}
</template>

<template name="meteorError">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
packages/errors/errors_list.html
Template.meteorErrors.helpers({
  errors: function() {
    return Errors.collection.find();
  }
});

Template.meteorError.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.collection.update(error._id, {$set: {seen: true}});
  });
};
packages/errors/errors_list.js

Testing the package out with Microscope

Ora testeremo le modifiche localmente per assicurarci che tutto funzioni. Per aggiungere il pacchetto all'applicazione, eseguiamo nel terminale meteor add errors ed eliminiamo i file esistenti che ora sono superflui:

$ rm client/helpers/errors.js
$ rm client/views/includes/errors.html
$ rm client/views/includes/errors.js
removing old files on the bash console

Dobbiamo fare qualche piccola modifica per utilizzare correttamente l'API:

Router.onBeforeAction(function() { Errors.clearSeen(); });
lib/router.js
  {{> header}}
  {{> meteorErrors}}
client/views/application/layout.html
Meteor.call('post', post, function(error, id) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);

client/views/posts/post_submit.js
Posts.update(currentPostId, {$set: postProperties}, function(error) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);
client/views/posts/post_edit.js

Commit 9-5-1

Created basic errors package and linked it in.

Una volta terminate queste modifiche, la situazione dovrebbe essere tornata a com'era prima della creazione del pacchetto.

Come scrivere dei test

Il primo passo nello sviluppo di un pacchetto è quello di testarlo in un'applicazione, ma quello successivo è di scrivere una serie di test per verificarne il corretto funzionamento. Meteor fornisce Tinytest, un sistema di test integrato, che semplifica l'esecuzione di questi test e ci aiuta a stare tranquillo al momento di condividere il nostro pacchetto con altri sviluppatori.

Creiamo ora un file nel quale, utilizzando Tinytest, faremo dei test sul codice creato per gestire gli errori:

Tinytest.add("Errors collection works", function(test) {
  test.equal(Errors.collection.find({}).count(), 0);

  Errors.throw('A new error!');
  test.equal(Errors.collection.find({}).count(), 1);

  Errors.collection.remove({});
});

Tinytest.addAsync("Errors template works", function(test, done) {
  Errors.throw('A new error!');
  test.equal(Errors.collection.find({seen: false}).count(), 1);

  // render the template
  UI.insert(UI.render(Template.meteorErrors), document.body);

  // wait a few milliseconds
  Meteor.setTimeout(function() {
    test.equal(Errors.collection.find({seen: false}).count(), 0);
    test.equal(Errors.collection.find({}).count(), 1);
    Errors.clearSeen();

    test.equal(Errors.collection.find({seen: true}).count(), 0);
    done();
  }, 500);
});
packages/errors/errors_tests.js

In questi test stiamo verificando le funzioni di base di Meteor.Errors e controllando che il codice renderizzato nel template funzioni ancora.

Non ci dilungheremo nella spiegazione di come scrivere i test per i pacchetti di Meteor (dato che l'API non è ancora definitiva), ma speriamo che si capisca sufficientemente come funziona il codice precedente.

Per dire a Meteor come eseguire i testi, aggiungiamo nel file package.js il seguente codice:

Package.on_test(function(api) {
  api.use('errors', 'client');
  api.use(['tinytest', 'test-helpers'], 'client');

  api.add_files('errors_tests.js', 'client');
});
packages/errors/package.js

Commit 9-5-2

Added tests to the package.

Possiamo poi eseguirli da terminale con:

$ meteor test-packages errors
Terminal
Passing all tests
Passing all tests

Come rilasciare il pacchetto

Rendiamo ora disponibile in nostro pacchetto al mondo intero attraverso Atmosphere.

Come prima cosa, aggiungiamo un file smart.json per passare a Meteorite e Atmosphere i dettagli più importanti del pacchetto:

{
  "name": "errors",
  "description": "A pattern to display application errors to the user",
  "homepage": "https://github.com/tmeasday/meteor-errors",
  "author": "Tom Coleman <tom@thesnail.org>",
  "version": "0.1.0",
  "git": "https://github.com/tmeasday/meteor-errors.git",
  "packages": {
  }
}
packages/errors/smart.json

Commit 9-5-3

Added a smart.json

Inseriamo dei metadati per segnalare informazioni sul pacchetto, fra i quali una descrizione per spiegare a cosa è utile, l'indirizzo di dove è posizionata la repository Git e un numero di versione iniziale. Se il nostro pacchetto dipendesse da altri presenti su Atmosphere, possiamo creare una sezione "packages" per descrivere le dipendenze.

Una volta sistemato tutto, il rilascio è molto semplice. Dobbiamo creare un repository Git, caricarlo su di un server Git remoto e mettere il link nel file smart.json.

Per fare questi passaggi con GitHub bisogna creare un nuovo repository e seguire il procedimento standard per caricare il codice del pacchetto in quella repository. Infine utilizzare il comando mrt release per pubblicarlo:

$ git init
$ git add -A
$ git commit -m "Created Errors Package"
$ git remote add origin https://github.com/tmeasday/meteor-errors.git
$ git push origin master
$ mrt release .
Done!
Terminal (run from within `packages/errors`)

Nota: i nomi dei pacchetti devono essere unici. Se avete seguito il procedimento parola per parola e utilizzato lo stesso nome per il pacchetto, verrà segnalato un conflitto e il rilascio non avrà successo. Tuttavia nel prossimo futuro i pacchetti Atmosphere saranno preceduti da quello dell'autore in modo che questo non si verifichi.

Seconda Nota: dovrete crearvi un utente su http://atmosphere.meteor.com, perché all'esecuzione di mrt release . vi verranno richiesti username e password.

Ora che abbiamo rilasciato il pacchetto, possiamo eliminarlo dal progetto e installarlo di nuovo usando Meteorite:

$ rm -r packages/errors
$ mrt add errors
Terminal (run from the top level of the app)

Commit 9-5-4

Removed package from development tree.

Meteorite ora dovrebbe scaricare il nostro pacchetto per la prima volta, ottimo lavoro!

Commenti

10

Lo scopo di un sito di social news è creare una comunità di utenti, e sarebbe davvero impossibile farlo se questi utenti non avessero la possibilità di parlare tra di loro. Dunque, in questo capitolo, aggiungiamo i commenti!

Iniziamo col creare una nuova collezione dove salvare i commenti, e aggiungendo un pò di dati di esempio a questa collezione.

Comments = new Meteor.Collection('comments');
collections/comments.js
// Fixture data
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // create two users
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000
  });
}
server/fixtures.js

Non dimentichiamoci di pubblicare e sottoscrivere la collezione che abbiamo appena creato:

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function() {
  return Comments.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() {
    return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
  }
});
lib/router.js

Commit 10-1

Added comments collection, pub/sub and fixtures.

È bene osservare che, perché il codice sopra sia eseguito, devi prima lanciare meteor reset per svuotare il tuo database. Dopo aver eseguito il reset, non dimenticare di creare un nuovo utente ed effettuare di nuovo il login!

Prima di tutto, abbiamo creato un paio di (finti) utenti, li abbiamo inseriti nel database e abbiamo usato i loro ids per selezionarli dal database stesso subito dopo. Dopodichè, abbiamo aggiunto dei commenti al primo post, uno per ciascuno degli utenti, collegando il commento al post (con postId) e all'utente (con userId). Abbiamo anche aggiunto una data di inserimento e un testo per ciascun commento, insieme a un valore per author, un campo non normalizzato.

Inoltre, abbiamo modificato in nostro router per attendere sulle collezioni comments e posts.

Visualizzare i commenti

È già un risultato l'aver inserito i commenti nel database, ma dobbiamo anche mostrarli nella pagina di discussione. La procedura dovrebbe ormai esserti familiare, e ti sei già fatto un'idea di quali sono i passi necessari:

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>
</template>
client/views/posts/post_page.html
Template.postPage.helpers({
  comments: function() {
    return Comments.find({postId: this._id});
  }
});
client/views/posts/post_page.js

Inseriamo il blocco {{#each comments}} dentro al template post, così this si riferisce a un post dentro all'helper comments. Per selezionare quali commenti mostrare, filtriamo la collezione selezionando quelli collegati allo specifico post tramite l'attributo postId.

Considerato quanto abbiamo già imparato riguardo agli helpers e ad Spacebars, visualizzare un commento è abbastanza semplice. Creiamo una nuova cartella comments dentro a views per salvare il template del dettaglio commenti:

<template name="comment">
  <li>
    <h4>
      <span class="author">{{author}}</span>
      <span class="date">on {{submittedText}}</span>
    </h4>
    <p>{{body}}</p>
  </li>
</template>
client/views/comments/comment.html

Creiamo un template helper per formattare la data (campo submitted) in modo che sia leggibile (a meno che tu non sia una di quelle persone che riesce a leggere con naturalezza timestamp UNIX e codici colore esadecimali?)

Template.comment.helpers({
  submittedText: function() {
    return new Date(this.submitted).toString();
  }
});
client/views/comments/comment.js

Mostriamo anche il numero totale di commenti per ciascun post:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

E, per fare questo, aggiungiamo un helper chiamato commentsCount al template manager postItem:

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  },
  commentsCount: function() {
    return Comments.find({postId: this._id}).count();
  }
});
client/views/posts/post_item.js

Commit 10-2

Display comments on `postPage`.

Dovresti essere ora in grado di visualizzare i commenti di esempio, qualcosa del genere:

Visualizzazione dei commenti
Visualizzazione dei commenti

Inserire commenti

Ora inseriamo la possibilità per i nostri utenti di creare nuovi commenti. La procedura che seguiremo sarà abbastanza simile a quella che abbiamo usato per la funzionalità di inserimento di nuovi post.

Incominciamo con il creare il modulo di inserimento alla fine di ciascun post:

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>

  {{#if currentUser}}
    {{> commentSubmit}}
  {{else}}
    <p>Please log in to leave a comment.</p>
  {{/if}}
</template>
client/views/posts/post_page.html

E poi create il template del modulo di inserimento:

<template name="commentSubmit">
  <form name="comment" class="comment-form">
    <div class="control-group">
        <div class="controls">
            <label for="body">Comment on this post</label>
            <textarea name="body"></textarea>
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button type="submit" class="btn">Add Comment</button>
        </div>
    </div>
  </form>
</template>
client/views/comments/comment_submit.html
Il modulo di inserimento di commenti
Il modulo di inserimento di commenti

Per inserire il commento, chiameremo un metodo comment nel manager commentSubmit che funziona in maniera simile a quello nel manager postSubmit:

Template.commentSubmit.events({
  'submit form': function(e, template) {
    e.preventDefault();

    var $body = $(e.target).find('[name=body]');
    var comment = {
      body: $body.val(),
      postId: template.data._id
    };

    Meteor.call('comment', comment, function(error, commentId) {
      if (error){
        throwError(error.reason);
      } else {
        $body.val('');
      }
    });
  }
});
client/views/comments/comment_submit.js

Nello stesso modo in cui abbiamo precedentemente creato un Method meteor lato server chiamato post, ora ne creiamo uno chiamato comment per creare i commenti; controlliamo che tutto sia in regola, e inseriamo il nuovo commento nella collezione comments.

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {
    var user = Meteor.user();
    var post = Posts.findOne(commentAttributes.postId);
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to make comments");

    if (!commentAttributes.body)
      throw new Meteor.Error(422, 'Please write some content');

    if (!post)
      throw new Meteor.Error(422, 'You must comment on a post');

    comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime()
    });

    return Comments.insert(comment);
  }
});
collections/comments.js

Commit 10-3

Created a form to submit comments.

Non stiamo facendo nessun controllo particolare, solo verificando che l'utente sia autorizzato, che il commento non sia vuoto, e che sia collegato a un post.

Ottimizzare la sottoscrizione Comments

Allo stato attuale, Meteor manda tutti i commenti di tutti i post a tutti gli utenti collegati; ci sembra uno spreco, perché alla fin fine, ciascun utente usa solo una piccola parte di tutti questi dati (i commenti del post che sta visualizzando). Ottimizziamo la pubblicazione e la sottoscrizione così da controllare meglio quali commenti sono inviati.

A pensarci bene, l'applicazione ha bisogno di sottoscrivere la pubblicazione comments solo quando un utente accede alla pagina che visualizza un post, e ha bisogno di caricare solo i commenti relativi a quel particolare post.

Il primo passo sarà cambiare il modo in cui effettuiamo la sottoscrizione alla collezione comments. Finora, l'abbiamo effettuata a livello di router, il che significa che carichiamo tutti i dati quando il router viene inizializzato.

Ma ora vogliamo che la nostra sottoscrizione dipenda dal particolare indirizzo della pagina che stiamo visualizzando, e tale indirizzo può ovviamente cambiare. Dunque dobbiamo spostare il codice che effettua la sottoscrizione dal livello di router a quello di route.

Questo ha un'altra conseguenza: invece di caricare tutti i dati quando inizializziamo l'applicazione per la prima volta, questi verranno caricati quando la particolare route viene chiamata; questo significa che ora l'applicazione sarà più lenta in fase di navigazione (perché i dati vengono caricati man mano che servono), ma è inevitabile, a meno che non si vogliano precaricare tutti i dati all'avvio.

Prima di tutto, eviteremo di precaricare tutti i commenti all'interno del blocco configure rimuovendo Meteor.subscribe('comments'):

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() {
    return Meteor.subscribe('posts');
  }
});
lib/router.js

E aggiungeremo una nuova funzione waitOn a livello di route:

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return Meteor.subscribe('comments', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  //...

});
lib/router.js

Noterete che stiamo passando this.params._id come parametro della pubblicazione. Usiamo questo dato per far sì che i dati scaricati siano solo quelli riferiti al post corrente:

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});
server/publications.js

Commit 10-4

Made a simple publication/subscription for comments.

Osserviamo ora un ultimo problema: quando torniamo alla pagina principale, l'applicazione dice che il nostro post ha 0 commenti:

Tutti i commenti sono spariti!
Tutti i commenti sono spariti!

Contare i commenti

La ragione di questo comportamento è semplice: noi carichiamo i commenti solo quando entriamo nella pagina di visualizzazione del post, dunque, quando viene eseguita l'istruzione Comments.find({postId: this._id}) nell'helper commentsCount del manager post_item, Meteor non riesce a trovare il dato da mostrare.

La maniera migliore per risolvere il problema è denormalizzare, salvando il numero dei commenti dentro al post stesso (se non sei del tutto sicuro cosa significhi, non preoccuparti: questo argomento viene spiegato nel prossimo approfondimento). Anche se, come vedremo, il nostro codice diventerà un po’ più complesso, questo aumento di complessità sarà giustificato dall'aumento delle prestazioni dell'applicazione, per non dover pubblicare tutti i commenti già nella pagina che mostra i post.

Dobbiamo dunque aggiungere una proprietà commentsCount alla struttura dati post. Per cominciare, aggiorniamo i nostri dati di esempio (e ricordiamoci di lanciare meteor reset - non dimenticare anche di ricreare il tuo utente, dopo!):

var telescopeId = Posts.insert({
  title: 'Introducing Telescope',
  ..
  commentsCount: 2
});

Posts.insert({
  title: 'Meteor',
  ...
  commentsCount: 0
});

Posts.insert({
  title: 'The Meteor Book',
  ...
  commentsCount: 0
});
server/fixtures.js

Dopodichè, ogni post deve partire da 0 commenti:

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id,
  author: user.username,
  submitted: new Date().getTime(),
  commentsCount: 0
});

var postId = Posts.insert(post);
collections/posts.js

Ed incrementiamo il valore di commentsCount quando un utente aggiunge un nuovo commento utilizzando l'operatore $inc di Mongo (che serve a incrementare il valore di un campo numerico):

// update the post with the number of comments
Posts.update(comment.postId, {$inc: {commentsCount: 1}});

return Comments.insert(comment);
collections/comments.js

Possiamo dunque rimuovere l'helper commentsCount dal file client/views/posts/post_item.js, dal momento che il campo è disponibile nel post stesso.

Commit 10-5

Denormalized the number of comments into the post.

Ora che gli utenti possono parlare l'un l'altro, sarebbe un peccato se la pagina non fosse aggiornata con i nuovi commenti via via che questi vengono inseriti; il prossimo capitolo ci spiega come utilizzare le notifiche per questo scopo.

Denormalizzazione

Sidebar 10.5

Denormalizzare i dati significa non archiviare i data in forma “normale”. In altre parole, la denormalizzazione significa avere più copie dello stesso dato in più punti del database.

Nell'ultimo capitolo abbiamo denormalizzato il conteggio del numero di commenti all'interno dell'oggetto ‘post’ per evitare di dover caricare tutti i commenti ogni volta. Dal punto di vista di una modelizzazione dati questo è ridondante, perché potremmo contare ogni volta il giusto insieme di commenti per ottenere quel dato (tralasciando le considerazioni sulla performance).

Spesso denormalizzare significa più lavoro per lo sviluppatore. Nel nostro esempio, ogni volta che aggiungiamo o rimuoviamo un commento, dobbiamo ricordarci di aggiornare il post collegato per assicurarci che il campo commentsCount resti corretto. Questo è esattamente il motivo per cui i database relazionali come MySQL disapprovano questo approccio.

Tuttavia l'approccio normale ha i suoi contro: senza una proprietà commentsCount, dovremmo inviare al client tutti i commenti ogni volta solo per poterli contare, che è quello che facevamo all'inizio. Denormalizzare ci permette di evitarlo del tutto.

Una pubblicazione speciale

Potrebbe essere possibile creare una pubblicazione speciale che invia solo il conteggio dei commenti a cui siamo interessato (ad esempio il conteggio dei commenti dei post che possiamo attualmente vedere, attraverso query di aggregazione sul server).

Conviene considerare se la complessità del codice di tale pubblicazione non superi di peso le difficoltà create dalla denormalizzazione…

Ovviamente, queste considerazioni sono specifiche per ogni applicazione: se state scrivendo del codice dove l'integrità dei dati è di primaria importanza, allora evitare l'inconsistenza dei dati è molto più importante e di priorità più elevata dei guadagni in performance.

Incorporare documenti o usare collezioni multiple

Se avete dell'esperienza con Mongo, potreste essere stupiti dal fatto che abbiamo creato una seconda collezione solo per i commenti: perché non incorporare i commenti in una lista all'interno del documento del post?

Si deve al fatto che gli strumenti che Meteor ci fornisce funzionano molto meglio quando intervengono a livello di collezione. Per esempio:

  1. L'helper {{#each}} è molto efficiente quando si esegue un'iterazione su di un cursore (il risultato di collection.find()). Non si può dire lo stesso quando si esegue un'iterazione su un array di oggetti all'interno di un documento più grande.
  2. I metodi allow e deny operano a livello di documento, il che rende facile assicurarsi che ogni modifica a un singolo commento sia corretta in un modo che sarebbe più complesso se operassimo a livello di post.
  3. Il protocollo DDP opera al livello degli attributi di primo livello di un documento – questo significa che se comments fosse un proprietà di post, ogni volta che un commento viene creato su un post, il server invierebbe l'intera lista lista dei commenti del post aggiornata a ogni client connesso.
  4. Le pubblicazioni e le sottoscrizioni sono molto più semplici da controllare a livello dei documenti. Per esempio, se volessimo paginare i commenti di un post, troveremmo difficile farlo se i commenti non fossero in una collezione separata.

Mongo suggerisce di incorporare i documenti per ridurre il numero di richieste dispendiose per recuperare i documenti. Tuttavia, questa non è una problematica quando teniamo conto dell'architettura di Meteor: il più delle volte stiamo interrogando i commenti sul client, dove l'accesso al database è praticamente nullo.

Gli aspetti negativi della denormalizzazione

C'è un interessante discussione sul quando non dovreste denormalizzare i dati. Per una buona lettura su questa tesi, raccomandiamo Why You Should Never Use MongoDB di Sarah Mei.

Notifiche

11

Ora che gli utenti possono commentare sui rispettivi post, sarebbe bene far loro sapere che una conversazione è iniziata.

Per farlo, informeremo il proprietario del post che c'è stato un commento al proprio post, e forniremo un link per visualizzare quel commento.

Questo è il tipo di caratteristica in cui Meteor davvero eccelle: poichè Meteor è in realtime di default, mostreremo quelle notifiche istantaneamente. Non abbiamo bisogno di aspettare che l'utente aggiorni la pagina o che la controlli, possiamo semplicemente far apparire le notifiche senza dover mai scrivere codice specifico.

Creare le notifiche

Creeremo una notifica quando qualcuno commenta sui tuoi post. In futuro, le notifiche potrebbero essere estese per coprire molti altri scenari, ma per ora questo basterà per tenere gli utenti informati su quello che sta succedendo.

Creiamo la nostra collezione Notifications, così come una funzione createCommentNotification che inserirà una notifica in corrispondenza di ogni nuovo commento su uno dei tuoi post:

Notifications = new Meteor.Collection('notifications');

Notifications.allow({
  update: ownsDocument
});

createCommentNotification = function(comment) {
  var post = Posts.findOne(comment.postId);
  if (comment.userId !== post.userId) {
    Notifications.insert({
      userId: post.userId,
      postId: post._id,
      commentId: comment._id,
      commenterName: comment.author,
      read: false
    });
  }
};
collections/notifications.js

Così come per i post e i commenti, questa collezione Notifications sarà condivisa tra client e server. Così come abbiamo bisogno di aggiornare le notifiche, una volta che un utente le ha viste, dobbiamo anche abilitare gli aggiornamenti, assicurando come al solito di limitare le autorizzazioni di aggiornamento ai soli dati in possesso dell'utente.

Abbiamo anche creato una semplice funzione che osserva il post che l'utente sta commentando, trova chi dovrebbe essere notificato, e inserisce una nuova notifica.

Stiamo già creando i commenti con un metodo lato server, quindi possiamo solo estendere tale metodo in modo da chiamare la nostra funzione. Sostituiremo return Comments.insert(comment); con comment._id = Comments.insert(comment), al fine di salvare l’_id del commento appena creato in una variabile, quindi chiamare la nostra funzione createCommentNotification:

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {

    // [...]

    // create the comment, save the id
    comment._id = Comments.insert(comment);

    // now create a notification, informing the user that there's been a comment
    createCommentNotification(comment);

    return comment._id;
  }
});
collections/comments.js

Pubblichiamo anche le notifiche e abboniamoci sul client:

// [...]

Meteor.publish('notifications', function() {
  return Notifications.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() {
    return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
  }
});
lib/router.js

Commit 11-1

Added basic notifications collection.

Visualizzare le notifiche

Ora possiamo andare avanti e aggiungere una lista di notifiche nell’ header.

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{> loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

E creiamo i template notifications e notification (entrambi saranno sul file notifications.html):

<template name="notifications">
  <a href="#" class="dropdown-toggle" data-toggle="dropdown">
    Notifications
    {{#if notificationCount}}
      <span class="badge badge-inverse">{{notificationCount}}</span>
    {{/if}}
    <b class="caret"></b>
  </a>
  <ul class="notification dropdown-menu">
    {{#if notificationCount}}
      {{#each notifications}}
        {{> notification}}
      {{/each}}
    {{else}}
      <li><span>No Notifications</span></li>
    {{/if}}
  </ul>
</template>

<template name="notification">
  <li>
    <a href="{{notificationPostPath}}">
      <strong>{{commenterName}}</strong> commented on your post
    </a>
  </li>
</template>
client/views/notifications/notifications.html

Possiamo vedere che il piano è per ogni notifica di contenere un link al post che è stato commentato e il nome dell'utente che l'ha commentato.

Poi, dobbiamo essere sicuri di selezionare nel nostro manager la giusta lista di notifiche e aggiornare le notifiche come “lette” quando un utente clicca sul link alle quale puntano.

Template.notifications.helpers({
  notifications: function() {
    return Notifications.find({userId: Meteor.userId(), read: false});
  },
  notificationCount: function(){
    return Notifications.find({userId: Meteor.userId(), read: false}).count();
  }
});

Template.notification.helpers({
  notificationPostPath: function() {
    return Router.routes.postPage.path({_id: this.postId});
  }
});

Template.notification.events({
  'click a': function() {
    Notifications.update(this._id, {$set: {read: true}});
  }
});
client/views/notifications/notifications.js

Commit 11-2

Display notifications in the header.

Si può pensare che le notifiche siano non troppo differenti dagli errori, ed è vero la loro struttura è molto simile. C'é solo una differenza importante: abbiamo creato una classica collezione sincronizzata client-server. Questo significa che le nostre notifiche sono persistenti e, fin quando usiamo lo stesso account utente, esisteranno ricaricamento della pagina e su differenti dispositivi.

Facciamo una prova: apri un altro browser (diciamo Firefox), creiamo un altro utente, e commentiamo sul post creato con l'account principale (quello che hai lasciato aperto in Chrome). Dovresti vedere qualcosa tipo questo:

Visualizzazione delle notifiche.
Visualizzazione delle notifiche.

Controllo dell'accesso alle notifiche

Le notifiche stanno funzionando bene. Tuttavia c'è solo un piccolo problema: le nostre notifiche sono pubbliche.

Se avete ancora il secondo browser aperto, provate ad eseguire il seguente codice nella console del browser:

 Notifications.find().count();
1
Browser console

Questo nuovo utente (quello che ha commentato) non dovrebbe ricevere alcuna notifica. La notifica che si può vedere nella collezione Notifications in realtà appartiene al nostro utente originario.

Oltre a potenziali problemi di privacy, non possiamo permetterci di avere le notifiche di ogni utente caricate nel browser di ogni altro utente. In un sito abbastanza grande, questo potrebbe sovraccaricare la memoria disponibile del browser e iniziare a causare seri problemi di prestazioni.

Risolviamo questo problema con le pubblicazioni. Possiamo usare le nostre pubblicazioni per specificare esattamente quale parte di una collezione vogliamo condividere con ciascun browser.

Per fare questo, abbiamo bisogno di restituire nella nostra pubblicazione un cursore diverso da Notifications.find(). Vale a dire, vogliamo restituire un cursore che corrisponde alle notifiche dell'utente corrente.

Farlo è abbastanza semplice, visto che la funzione publish dispone dell’_id dell'utente corrente in this.userId:

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Commit 11-3

Only sync notifications that are relevant to the user.

Ora se controlliamo le nostre due finestre del browser, dovremmo vedere due diverse collezioni per le notifiche:

 Notifications.find().count();
1
Browser console (user 1)
 Notifications.find().count();
0
Browser console (user 2)

In realtà, l'elenco delle notifiche dovrebbe anche cambiare a seconda che si sia autenticati o meno. Questo perché le pubblicazioni automaticamente ri-pubblicano ogni volta che l'account utente cambia.

La nostra applicazione sta diventando sempre più funzionale, e via via che gli utenti si registrano e iniziano a postare link si rischia di finire con una homepage senza fine. Affronteremo questo problema nel prossimo capitolo implementando la paginazione.

Reattività avanzata

Sidebar 11.5

È raro aver bisogno di scrivere da soli il codice per risolvere le dipendenze, ma è certamente utile per capire come avviene il flusso di risoluzione delle dipendenze.

Immaginate di voler monitorare il numero di amici di Facebook dell'utente corrente che hanno messo “Mi piace” su ogni post su Microscope. Supponiamo che abbiamo già definito i dettagli su come autenticare l'utente con Facebook, le opportune chiamate alle API, e estrarre i dati rilevanti. Ora abbiamo una funzione asincrona lato client che restituisce il numero di mi piace, getFacebookLikeCount(utente, url, callback).

La cosa importante da ricordare su questa funzione è che è non-reattiva e non-realtime. Farà una richiesta HTTP a Facebook per recuperare dei dati, e li renderà disponibili per l'applicazione in una richiamata asincrona, ma la funzione non si ri-eseguirà da sola quando il contatore cambia su Facebook, e la nostra interfaccia non cambierà quando i dati sottostanti lo faranno.

Per risolvere questo problema, possiamo iniziare utilizzando setInterval per chiamare la nostra funzione ogni pochi secondi:

currentLikeCount = 0;
Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId),
      function(err, count) {
        if (!err)
          currentLikeCount = count;
      });
  }
}, 5 * 1000);

Ogni volta che controlliamo la variabile currentLikeCount, possiamo aspettarci di ottenere il numero corretto con un margine di errore di cinque secondi. Possiamo ora utilizzare questa variabile in un helper in questo modo:

Template.postItem.likeCount = function() {
  return currentLikeCount;
}

Tuttavia, nulla ancora dice al nostro template di ri-disegnare quando currentLikeCount cambia. Anche se la variabile è ora pseudo-realtime, nel senso che cambia da sola, non è reattiva così ancora non può comunicare correttamente con il resto dell'ecosistema Meteor.

Tracciare la reattività: Computation

La reattività di Meteor è mediata dalle dipendenze, strutture di dati che tracciano una serie di calcoli.

Come abbiamo visto nel precedente approfondimento reattività, una computation è una sezione di codice che utilizza i dati reattivi. Nel nostro caso, c'è una computation che è stata creata in modo implicito per il modello PostItem. Ogni helper sul gestore del template sta lavorando all'interno di tale computation.

Si può pensare alla computation, come la sezione di codice che “si interessa” dei dati reattivi. Quando i dati cambiano, sarà questa computation che sarà informata (via invalidate ()), ed è la computation che decide se c'é bisogno di fare qualcosa.

Trasformare una variabile in una funzione reattiva

Per trasformare la nostra variabile currentLikeCount variabile in una fonte di dati reattiva, abbiamo bisogno di tenere traccia di tutte le computation che la utilizzano in una dipendenza. Ciò richiede di trasformarla da variabile a funzione (che restituirà un valore):

var _currentLikeCount = 0;
var _currentLikeCountListeners = new Deps.Dependency();

currentLikeCount = function() {
  _currentLikeCountListeners.depend();
  return _currentLikeCount;
}

Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId),
      function(err, count) {
        if (!err && count !== _currentLikeCount) {
          _currentLikeCount = count;
          _currentLikeCountListeners.changed();
        }
      });
  }
}, 5 * 1000);

Quello che abbiamo fatto è impostare una dipendenza _currentLikeCountListeners, che tiene traccia di tutte le computation nelle quali currentLikeCount() è stato usato. Quando il valore _currentLikeCount cambia, chiamiamo la funzione changed() su tale dipendenza, che invalida tutte le computation tracciate.

Queste computation possono quindi andare avanti e gestire il cambiamento, caso per caso.

Comparare Deps ad Angular

Angular è una libreria solo client-side di rendering reattivo, ed è sviluppata dai bravi ragazzi di Google. È indicativo confrontare l'approccio di Meteor per il tracciamento delle dipendenze con quello di Angular, visto che gli approcci sono molto diversi.

Abbiamo visto che il modello di Meteor utilizza i blocchi di codice chiamati computation. Queste computation sono monitorate da speciali sorgenti di dati “reattive” (funzioni) che si prendono cura loro di invalidare al momento opportuno. Così la sorgente di dati esplicitamente informa tutte le sue dipendenze quando hanno bisogno di chiamare invalidate(). Si noti che anche se questo accade generalmente quando i dati sono cambiati, la sorgente dei dati potrebbe potenzialmente anche decidere di attivare invalidazione per altri motivi.

Inoltre, anche se la computation di solito solo riesegue quando invalidata, è possibile impostarla fino a comportarsi nel modo desiderato. Tutto questo ci dà un elevato livello di controllo sulla reattività.

In Angular, la reattività è mediata dall'oggetto scope. Uno scope può essere pensato come semplice oggetto JavaScript con un paio di metodi speciali.

Quando si vuole dipendere reattivamente su un valore in uno scope, si chiama scope.$watch, fornendo l'espressione alla quale si è interessati (vale a dire quali parti dello scope vi interessano) e una funzione listener che verrà eseguita ogni volta tale espressione cambia. Così si dichiara esplicitamente esattamente quello che si vuole fare ogni volta che il valore dell'espressione cambia.

Tornando al nostro esempio Facebook, scriveremo:

$rootScope.$watch('currentLikeCount', function(likeCount) {
  console.log('Current like count is ' + likeCount);
});

Naturalmente, proprio come raramente si imposta una computation in Meteor, neanche $watch viene chiamato spesso esplicitamente in Angular come direttiva ng-model e {{expressions}} imposta automaticamente il tracciamento che poi si prende cura del re-rendering ad ogni cambiamento.

Quando un certo valore reattivo è cambiato, scope.$apply() deve poi essere chiamato. Questo rivaluta ogni osservatore del campo dello scope, ma chiama solo la funzione listener di watcher il cui valore di espressione è cambiato.

Quindi scope.$apply() è simile a dependency.changed(), tranne che agisce a livello del campo dello scope, piuttosto che dare il comando di dire che proprio gli ascoltatori dovrebbero essere rivalutato. Detto questo, questa leggera mancanza di controllo dà ad Angular la capacità di essere molto intelligente ed efficiente nel modo in cui determina con precisione quali listener devono essere rivalutati.

Con Angular, il codice della nostra funzione getFacebookLikeCount() codice sarebbe assomigliato a qualcosa di simile:

Meteor.setInterval(function() {
  getFacebookLikeCount(Meteor.user(), Posts.find(postId),
    function(err, count) {
      if (!err) {
        $rootScope.currentLikeCount = count;
        $rootScope.$apply();
      }
    });
}, 5 * 1000);

Certo, Meteor si occupa della maggior parte del lavoro pesante per noi e ci permette di beneficiare della reattività senza molto lavoro da parte nostra. Ma si spera che impararando questi pattern possa tornare utile se ci sarà mai bisogno di dover andare oltre.

Paginazione

12

Le cose stanno andando alla grande con Microscope e dovremmo aspettarci un ottimo riscontro quando verrà finalmente rilasciato.

Dovremmo quindi pensare a quali implicazioni ci saranno sulla performance dato il numero di nuovi post che verranno inseriti appena il sito prenderà il volo!

Abbiamo parlato di come una collezione lato client può contenere solo una parte dei dati presenti sul server, e abbiamo già utilizzato questa particolarità con le collezioni di notifiche e commenti.

Al momento stiamo ancora pubblicando tutti i post insieme, a tutti gli utenti connessi e se ci saranno migliaia di post pubblicati, questo potrebbe diventare un problema. Per risolverlo dobbiamo paginare i nostri post.

Aggiungere Post

Prima di tutto, nei nostri dati di esempio, carichiamo abbastanza post perché la paginazione abbia senso:

// Fixture data
if (Posts.find().count() === 0) {

  //...

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0
    });
  }
}
server/fixtures.js

Dopo aver eseguito meteor reset, dovreste vedere un risultato simile a questo:

Displaying dummy data.
Displaying dummy data.

Commit 12-1

Added enough posts that pagination is necessary.

Paginazione infinita

Implementeremo un paginazione “infinita”, il che significa che caricheremo inizialmente 10 post, con un bottone “carica più post” in fondo alla pagina. Cliccando sul bottone verranno aggiunti altri 10 post e così via, ad infinitum. In questo modo possiamo controllare tutto il sistema di paginazione con un solo parametro, che rappresenta il numero di post da visualizzare sullo schermo.

Ora dobbiamo trovare un modo per passare al server questo parametro così che sappia quanti post deve inviare al client. Dato che abbiamo già fatto una sottoscrizione alla pubblicazione posts nel router, ne trarremo vantaggio lasciando che sia il router a gestire la nostra paginazione.

Il metodo più semplice è quello di aggiungere il parametro del limite di post direttamente nel path, con URL di questo tipo http://localhost:3000/25. Un vantaggio di usare questa tecnica è che se già si stanno visualizzando 25 post e capita di ricaricare la finestra del browser per errore, verranno visualizzati 25 post anche quando la pagina si ricarica.

Per farlo in maniera appropriata, dobbiamo cambiare il modo in cui eseguiamo la sottoscrizione ai post. Come abbiamo fatto nel capitolo Commenti, dobbiamo spostare il codice della nostra sottoscrizione dal livello del router al livello della route.

Potrebbe sembrare un gran lavoro da fare tutto in una volta, ma diventerà tutto chiaro procedendo col codice.

Come prima cosa, fermiamo la sottoscrizione alla pubblicazione posts nel blocco Router.configure(). Eliminiamo Meteor.subscribe('posts'), lasciando solo la sottoscrizione notifications:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() {
    return [Meteor.subscribe('notifications')]
  }
});
lib/router.js

Aggiugiamo un parametro postsLimit al percorso della route. Aggiungendo un ? dopo il nome del parametro stiamo indicando che è opzionale. In questo modo la route non solo riconoscerà un url come http://localhost:3000/50, ma anche http://localhost:3000.

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?'
  });
});
lib/router.js

È importante contare che un percorso come /:parameter? combacerà con ogni possibile percorso. Siccome ogni route viene analizzata in maniera successiva per vedere se corrisponde al percorso attuale, dobbiamo organizzare le nostre route in ordine di specificità decrescente.

In altre parole, le route più specifiche come /posts/:_id devono essere messe per prime, e la route postsList dev'essere posizionata alla fine del file dato che si combina praticamente con tutti i percorsi.

È ora di gestire il problema più complesso di sottoscrivere e trovare i dati corretti. Dobbiamo gestire il caso in cui il parametro postsLimit non sia presente, assegnando un valore di default. Useremo come limite “5”, che ci da la possibilità di giocare abbastanza con la paginazione.

Router.map(function() {
  //..

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var postsLimit = parseInt(this.params.postsLimit) || 5;
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: postsLimit});
    }
  });
});
lib/router.js

Notate che stiamo passando un oggetto JavaScript ({limit: postsLimit}) insieme al nome della nostra pubblicazione posts. Questo oggetto ci servirà come parametro options per l'asserzione lato sevrer Posts.find(). Passiamo al codice lato server per implementarlo:

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Passare Parametri

Il codice delle nostre pubblicazioni sta dicendo al server che può fidarsi di ogni oggetto JavaScript che gli venga inviato dal client (nel nostro caso, {limit: postsLimit}) per passarlo all'asserzione find() come parametor options. Questo rende possibile agli utenti di inviare qualsiasi opzione tramite la console del browser.

Nel nostro caso si tratta di una cosa relativamente senza pericoli dato che tutto quello che può fare un utente è di riordinare i post in maniera diversa o cambiare il limite (che è quello che volevamo abilitare inizialmente).

Non dovete usare questo schema quando archiviate dati privati in campi non pubblicati, dato che l'utente può modificare l'opzione fields per accedervi, e dovete probabilmente evitare di usarla come argomento di selezione dell'asserzione find() per le stesse ragioni di sicurezza.

Uno schema più sicuro è quello di passare i parametri individualmente al posto dell'intero oggetto, per essere sicuri di avere completo controllo sui dati:

Meteor.publish('posts', function(sort, limit) {
  return Posts.find({}, {sort: sort, limit: limit});
});

Ora che stiamo eseguendo la sottoscrizione al livello della route, dobbiamo spostare il contesto dei dati nello stesso posto. Facciamo una deviazione dal nostro precedente schema e facciamo in modo che la funzione data ritorni un oggetto JavaScript invece di un cursore. Questo ci permette di creare un contesto dati nominato, che chiameremo posts.

Questo significa che invece di essere implicitamente disponibile come this all'interno del template, il nostro contesto dati sarà disponibile come posts. Oltre a questa piccola modifica, il codice dovrebbe risultare familiare:

Router.map(function() {
  //..

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5;
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5;
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });
});
lib/router.js

Ora che stiamo spostando il contesto dati al livello del router, possiamo con sicurezza eliminare l'helper del template posts all'interno del file posts_list.js e siccome abbiamo chiamato il contesto dati posts (come l'helper), non dobbiamo nemmeno toccare il template postsList!

Facciamo il punto della situazione. Il codice modificato di router.js dovrebbe essere così:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() {
    return [Meteor.subscribe('notifications')]
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5;
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5;
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });
});
lib/router.js

Commit 12-2

Augmented the postsList route to take a limit.

Proviamo il nostro nuovo sistema di paginazione. Abbiamo ora la possibilità di visualizzare un numero arbitrario di post semplicemente cambiando un parametro nell'URL. Ad esempio, proviamo ad accedere a http://localhost:3000/3, dovremmo vedere qualcosa di simile:

Controlling the number of posts on the homepage.
Controlling the number of posts on the homepage.

Perché non usare delle pagine?

Perché stiamo usando un approccio a “paginazione infinita” invece di mostare pagine di 10 post ciascuna, come fa Google con i risultati di ricerca? Questo è dovuto al paradigma di un'applicazione in tempo reale utilizzato da Meteor.

Immaginiamo di star paginando la nostra collezione Posts utilizzando lo schema di paginazione dei risultati di Google e che siamo al momento in pagina 2, che mostra i post da 10 a 20. Cosa succede se uno degli utenti cancella un post dei precedenti 10?

Dato che la nostra applicazione è in tempo reale, la nostra base dati cambierebbe. Il post numero 10 diventerebbe ora il post numero 9 e sarebbe eliminato dalla nostra vista, mentre il post numero 11 resterebbe nell'intervallo. Il risultato finale sarebbe che l'utente vedrebbe i post cambiare all'improvviso senza motivo!

Anche se tollerassimo questo malfunzionamento nell'esperienza utente, la paginazione tradizionale è anche difficile da implementare per ragioni tecniche.

Torniamo indietro al nostro esempio precedente. Stiamo pubblicando i post da 10 a 20 della collezione Posts, ma come troviamo questi post sul client. Non si possono prendere i post da 10 a 20 dato che ci sono solo 10 post al momento nei dati lato client.

Una soluzione sarebbe di pubblicare quei 10 post sul server, e poi fare un Posts.find() lato client per prendere tutti i post pubblicati.

Questo funziona se avete una sola sottoscrizione. Ma cosa accede se iniziamo ad avere più di una sottoscrizione ai post come capiterà a breve?

Diciamo che una sottoscrizione chiede i post da 10 a 20 e un'altra quelli da 30 a 40. Abbiamo ora 20 post pubblicati lato client in totale, senza modo di sapere quali appartengono a quale sottoscrizione.

Per tutte queste ragioni, la paginazione tradizionale non ha molto senso quando lavoriamo con Meteor.

Creare un Controller per una route

Potete notare che abbiamo ripetuto due volte la riga var limit = parseInt(this.params.postsLimit) || 5;. Inoltre, non è una buona scelta inserire il valore “5” direttamente nel codice. Non è certo la fine del mondo, ma dato che è sempre meglio seguire il principio DRY (Don’t Repeat Yourself - Non ripeterti) quando possibile, vediamo come possiamo riscrivere il nostro codice.

Introduciamo ora un nuovo aspetto dell'Iron Router, i Controller delle Route. Un controller di route è semplicemente un modo per raggruppare alcune funzionalità di routing in un pacchetto riutilizzabile, dal quale ogni route può ereditare queste funzionalità. Per ora lo useremo per una sola route, ma vedrete nel prossimo capitolo come questo aspetto diventerà utile.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5,
  limit: function() {
    return parseInt(this.params.postsLimit) || this.increment;
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  data: function() {
    return {posts: Posts.find({}, this.findOptions())};
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    controller: PostsListController
  });
});
lib/router.js

Seguiamo i passaggi: come prima cosa, abbiamo creato il controller estendendo RouteController. Poi dichiariamo la proprietà template come abbiamo fatto prima e poi la nuova proprietà ìncrement`.

Definiamo poi un nuova funzione limit che resituisce il limite corrente, e una funzione findOptions che restituisce un oggetto di opzioni. Questo può sembrare un passaggio in più, ma ci verrà utile più avanti.

Definiamo ora le funzioni waitOn e data come abbiamo fatto prima, eccetto per il fatto che ora utilizziamo in esse la nuova funzione findOptions.

L'ultima cosa da fare è di dire alla route postsList di puntare al nuovo controller, tramite la proprietà controller.

Commit 12-3

Refactored postsLists route into a RouteController.

Aggiungere un link ‘Carica più post’

Abbiamo ora una paginazione funzionante e il nostro codice ha una buona struttura. C'è solo un problema: non c'è ancora un modo per usare la paginazione se non cambiando manualmente l'URL. Questo di certo non rende pratica l'esperienza dell'utente, vediamo come poterla sistemare.

Ciò che vogliamo fare è abbastanza semplice. Aggiungiamo un bottone “Carica più post” alla fine della nostra lista di post, che incrementerà di 5 il numero di post visualizzati ogni volta che viene cliccato. Se sto visitando l'URL http://localhost:3000/5, cliccando su “Carica più post” dovrebbe portarmi a http://localhost:3000/10. Se siete riusciti ad arrivare fino a questo punto del libro, crediamo che possiate cavarvela con un po’ di aritmetica!

Come prima, aggiungiamo la logica della paginazione nella nostra route. Ricordate quando abbiamo esplicitamente dato un nome al contesto dati piuttosto che utilizzare un cursore anonimo? Non c'è nessuna regola che dice che la funzione data può solo ritornare cursori, così useremo la stessa tecnica per generare l'URL del bottone “Carica più post”.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5,
  limit: function() {
    return parseInt(this.params.postsLimit) || this.increment;
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().count() === this.limit();
    var nextPath = this.route.path({postsLimit: this.limit() + this.increment});
    return {
      posts: this.posts(),
      nextPath: hasMore ? nextPath : null
    };
  }
});
lib/router.js

Diamo uno sguardo più approfondito a questa piccola magia del router. Ricordate che la route postsList (che eredita dal controller PostsListController su cui stiamo attualmente lavorando) accetta un parametro postsLimit.

In questo modo quando passiamo {postsLimit: this.limit() + this.increment} alla funzione this.route.path(), stiamo dicendo alla route postsList di costruire il proprio percorso usando questo oggetto JavaScript come contesto di dati.

In altre parole, questa è identico ad utilizzare l'helper di Spacebars {{pathFor 'postsList'}}, escluso che stiamo rimpiazzando il this implicito con il nostro contesto di dati personalizzato.

Stiamo prendendo quel percorso e lo stiamo aggiungendo al contesto di dati per il nostro template, ma solo se ci sono più post da mostrare. Il modo in cui lo facciamo è un po’ complesso.

Sappiamo che this.limit() ritorna il numero corrente di post che vogliamo mostrare, che può essere sia il valore dell'URL corrente, o del nostro valore di default (5) se l'URL non contiene nessun parametro.

D'altra parte, this.posts si riferisce al cursore corrente, così this.posts.count() si riferisce al numero di post che sono attualmente nel cursore.

Quello che stiamo dicendo qui è che se chiediamo n post e ne otteniamo n, continuiamo a mostrare il bottone “Carica di più”. Ma se chiediamo n post e otteniamo meno di n, significa che abbiamo raggiunto il limite e dobbiamo smettere di mostrare quel bottone.

Detto questo, il nostro sistema ha un problema quando il numero di elemente nel database è esattamente n. Quando accade il client richiede n post, ottiene n post e continua a mostrare il bottone “Carica di più”, non sapendo che non sono rimasti elementi.

Sfortunatamente non ci sono sistemazioni semplici per questo problema, perciò al momento ci accontentiamo di questa implementazione non proprio perfetta.

Quel che rimane da fare è aggiungere il link “Carica di più” in fondo alla lista di post, facendo in modo che si visualizzi solo se abbiamo più post da caricare:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
client/views/posts/posts_list.html

Questo è quello che dovremmo vedere ora:

The “load more” button.
The “load more” button.

Commit 12-4

Added nextPath() to the controller and use it to step thr…

Una miglior barra di progresso

La nostra paginazione sta ora funzionando bene, ma soffre di un problema noioso: ogni volta che clicchiamo su “Carica di più” e il router richiede più post, veniamo indirizzati al template loading mentre aspettiamo che i nuovi dati arrivino. Il risultato è che veniamo rimandati all'inizio della pagina ogni volta e bisogna scrollare fino in fondo per riprendere la nostra navigazione.

Sarebbe molto meglio se potessimo stare sulla stessa pagina durante l'intera operazione, indicando comunque che i dati si stanno caricando. Fortunatamente è quello che fa il pacchetto iron-router-progress.

Come Safari per iOS o siti come Medium e YouTube, iron-router-progress aggiunge una sottile barra di caricamento nella parte alta dello schermo. Implementarlo è semplice come aggiungere il pacchetto alla nostra applicazione:

mrt add iron-router-progress
bash console

Attraverso la magia dei pacchetti, la nostra nuova barra di progresso funziona perfettamente appena installata! La barra di progresoo si attiverà per ogni route e si completerà automaticamente appena la route avrà caricato i dati richiesti.

Facciamo solo una modifica. Disabilitiamo iron-router-progress per la route postSubmit dato che non deve aspettare per i dati da nessuna sottoscrizione (dopo tutto è solo un form vuoto):

Router.map(function() {

  //...

  this.route('postSubmit', {
    path: '/submit',
    disableProgress: true
  });
});
lib/router.js

Commit 12-5

Use the iron-router-progress package to make pagination n…

Accedere ad ogni post

Stiamo attualmente caricando i cinque post più recenti come impostazione predefinita, ma cosa accade quando si naviga alla pagina di un post?

An empty template.
An empty template.

Se ci provate, troverete il template di un post vuoto. Questo è corretto: abbiamo detto al router di sottoscrivere la pubblicazione posts quando carica la route postsList, ma non abbiamo detto cosa vogliamo fare con la route postPage.

Finora tutto quello che sappiamo è come sottoscrivere a una lista dei n post più recenti. Come facciamo a chiedere al server un post specifico? Vi sveliamo un piccolo segreto: potete avere più di una pubblicazione per ogni collezione!

Per riavere indietro i post mancanti, creiamo semplicemente una nuova pubblicazione singlePost che pubblica solo un post, identificato tramite _id.

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('singlePost', function(id) {
  return id && Posts.find(id);
});
server/publications.js

Ora, facciamo la sottoscrizione lato client ai post corretti. Stiamo già sottoscrivendo alla ppubblicazione comments nella funzion waitOn della route postPage, così possiamo semplicemente aaggiungere qui la sottoscrizione. Non dimentichiamo di aggiungere la sottoscrizione anche alla route postEdit dato che necessita degli stessi dati:

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return [
        Meteor.subscribe('singlePost', this.params._id),
        Meteor.subscribe('comments', this.params._id)
      ];
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    waitOn: function() {
      return Meteor.subscribe('singlePost', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  /...

});
lib/router.js

Commit 12-6

Use a single post subscription to ensure that we can alwa…

Terminata la paginazione, la nostra applicazione non soffre più di problemi di scalabilità, e gli utenti sono sicuri di poter contribuire con molti più link di prima. Non sarebbe carino avere un sistema per poter dare un voto a questi link? È esattamente l'argomento del prossimo capitolo, Votare.

Votazioni

13

Adesso che il sito sta guadagnando in popolarità, trovare i link migliori sta cominciando a diventando complicato. È necessario un qualche sistema ordinamento per i post.

Si potrebbe implementare un complesso sistema di ordinamento basato su karma, decadimento del punteggio a tempo, e svariati altri parametri (la maggior parte dei quali implementati in (Telescope)[http://telesc.pe/], il fratello maggiore di Microscope). Nella nostra applicazione invece si punta alla semplicità, per cui i post saranno ordinati in base al numero di voti ricevuti.

Per cominciare, verrà fornita agli utenti la possibilità di votare un post.

Modello dei Dati

Per determinare se visualizzare agli utenti il pulsante di upvote o meno, per ciascun post verrà salvata la lista degli utenti che lo hanno votato, che consente anche di evitare che un utente voti più di una volta.

Riservatezza dei Dati & Pubblicazioni

Le liste dei votanti saranno disponibili a tutti gli utenti, il che renderà i dati automaticamente e pubblicamente consultabili tramite la console del browser.

Questo è il tipico problema di riservatezza dei dati causato dal modo in cui le collezioni funzionano. Ad esempio, si vuole consentire agli utenti la ricerca degli utenti che hanno votato i loro post? Nel caso in questione rendere tali informazioni pubbliche non ha reali conseguenze, ma è importante almeno avere consapevolezza del problema.

Da notare che se si volesse limitare l'accesso ad alcune di queste informazioni, ci si dovrebbe assicurare che lato client non venga consentito smanettare con le opzioni dei campi fields della pubblicazione, rimuovendo la proprietà lato server, oppure evitando di passare per intero le opzioni dal client al server.

Inoltre, il totale dei voti per ciascun post sarà denormalizzato, al fine di ottenere in maniera semplice tale contatore. Verranno quindi aggiunti due attributi ai post, upvoters e votes. Per cominciare, i suddetti campi verranno aggiunti ai dati di esempio:

// Fixture data
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // create two users
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000,
    commentsCount: 2,
    upvoters: [], votes: 0
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0,
      upvoters: [], votes: 0
    });
  }
}
server/fixtures.js

Al solito, fermare l'applicazione, eseguire meteor reset, riavviare l'app, quindi creare un nuovo account. È opportuno assicurarsi che i due campi vengano inizializzati al momento della creazione dei post:

//...

// check that there are no previous posts with the same link
if (postAttributes.url && postWithSameLink) {
  throw new Meteor.Error(302,
    'This link has already been posted',
    postWithSameLink._id);
}

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id,
  author: user.username,
  submitted: new Date().getTime(),
  commentsCount: 0,
  upvoters: [],
  votes: 0
});

var postId = Posts.insert(post);

return postId;

//...
collections/posts.js

Implementazione dei template per le votazioni

Per prima cosa, verrà aggiunto un pulsante al template del post:

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn"></a>
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        {{votes}} Votes,
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html
Il pulsante per votare
Il pulsante per votare

Verrà quindi chiamato un metodo lato server per eseguire l'upvote quando l'utente clicca sul pulsante:

//...

Template.postItem.events({
  'click .upvote': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

Infine, verrà aggiunto un metodo lato server in collections/posts.js che eseguirà la registrazione del voto:

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    var post = Posts.findOne(postId);
    if (!post)
      throw new Meteor.Error(422, 'Post not found');

    if (_.include(post.upvoters, user._id))
      throw new Meteor.Error(422, 'Already upvoted this post');

    Posts.update(post._id, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Commit 13-1

Added basic upvoting algorithm.

Questo methodo è sufficientemente autoesplicativo. Si eseguono alcuni controlli preventivi per assicurarsi che l'utente sia loggato e che il post esista. Quindi si controlla che l'utente non abbia già espresso il suo voto per il post, quindi si incrementa il contatore dei voti e si aggiunge l'utente alla lista dei votanti.

Questa ultima operazione si rivela interessante, in quanto vengono utilizzato due operatori speciali di Mongo. Ce ne sono molti altri da imparare, ma questi due sono estremamente utili: $addToSet aggiunge un elemento ad un array se non già incluso, mentre $inc semplicemente incrementa il valore di un campo di tipo intero.

Ritocchi all'Interfaccia Utente

Se l'utente non è loggato, oppure ha già votato, non deve essere in grado di votare nuovamente. Per evidenziare questo caso sull'interfaccia, verrà utilizzata una funzione di supporto che aggiunge, ove necessario, la classe CSS disabled al pulsante per la votazione.

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn {{upvotedClass}}"></a>
    <div class="post-content">
      //...
  </div>
</template>
client/views/posts/post_item.html
Template.postItem.helpers({
  ownPost: function() {
    //...
  },
  domain: function() {
    //...
  },
  upvotedClass: function() {
    var userId = Meteor.userId();
    if (userId && !_.include(this.upvoters, userId)) {
      return 'btn-primary upvotable';
    } else {
      return 'disabled';
    }
  }
});

Template.postItem.events({
  'click .upvotable': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

La classe viene cambiata da .upvote a .upvotable, quindi è opportuno non tralasciare di modificare il gestore dell'evento click.

Disabilitazione del pulsante per la votazione.
Disabilitazione del pulsante per la votazione.

Commit 13-2

Grey out upvote link when not logged in / already voted.

Ora, si può notare che i post con un singolo voto sono etichettati come “1 votes”, vediamo quindi come formattare il plurale (in Inglese) in modo appropriato. Mettere in forma plurale può essere un processo complicato, ma per adesso verrà affrontato in modo semplicistico. Si implementerà una funzione Spacebars generica, che può essere riutilizzata altrove.

UI.registerHelper('pluralize', function(n, thing) {
  // fairly stupid pluralizer
  if (n === 1) {
    return '1 ' + thing;
  } else {
    return n + ' ' + thing + 's';
  }
});
client/helpers/Spacebars.js

Le funzioni che sono state create sono legate ai relativi manager e template. Ma utilizzando Spacebars.registerHelper viene creata una funzione di supporto globale che può essere utilizzata in qualunque template:

<template name="postItem">
//...
<p>
  {{pluralize votes "Vote"}},
  submitted by {{author}},
  <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
  {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>
client/views/posts/post_item.html
Perfezionamento della forma plurale
Perfezionamento della forma plurale

Commit 13-3

Added pluralize helper to format text better.

Adesso si dovrebbe vedere 1 vote.

Miglioramento dell'Algoritmo di Votazione

Il codice che implementa la votazione dei post sembra sufficientemente buono, ma può essere ulteriormente migliorato. Nel metodo upvote vengono eseguite 2 chiamate a Mongo: la prima per ottenere il post, l'altra per modificarlo.

Ci sono due problemi in questa scelta implementativa. Per prima cosa, è inefficiente accedere al database per 2 volte. Ma ben più importante, introduce una corsa critica. L'algoritmo implementato è il seguente:

  1. Legge il post dal database
  2. Verifica se l'utente ha votato
  3. In caso negativo, esegue il voto

Cosa succede se lo stesso utente vota nuovamente lo stesso post durante i passi 1 e 3? Questa versione consente all'utente di votare 2 volte per lo stesso post. Fortunatamente, Mongo fornisce un metodo più efficace combinando i passi 1-3 in un singolo comando:

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    Posts.update({
      _id: postId,
      upvoters: {$ne: user._id}
    }, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Commit 13-4

Better upvoting algorithm.

Il codice si traduce in: “trova tutti i post con questo id per cui l'utente non ha ancora votato, e aggiornali in questo modo”. Se l'utente non ha ancora votato, il post con quell’id sarà ovviamente trovato. D'altro canto, se l'utente ha già votato, allora la ricerca non produrrà alcun risultato, e di conseguenza non succederà niente.

Il solo problema adesso adesso è che non si può informare l'utente riguardo la votazione già eseguita per il post (poiché è stata eliminata la chiamata al database che eseguiva la verifica). Ad ogni modo, l'utente dovrebbe esserne a conoscenza dato che il pulsante “upvote” nell'interfaccia utente è disabilitato.

Compensazione della Latenza

Supponiamo che si provi a barare spostando uno dei propri post in cima alla lista, modificando manualmente il numero dei voti:

> Posts.update(postId, {$set: {votes: 10000}});
Console del browser

(Dove postId è l'id di uno dei propri post).

Questo tentativo di ingannare il sistema verrebbe intercettato dal callback deny() (in collections/posts.js) ed immediatamente annullato.

Ma esaminando attentamente, è possibile che si veda in azione la compensazione di latenza. Può durare un instante, ma il post sarà temporaneamente spostato in cima alla lista prima di essere rispedito alla sua posizione originaria.

Cosa è successo? Nella collezione locale Posts, l’update è stato eseguito senza problemi. Ciò accade istantaneamente, per cui il post salta in cima alla lista. Al contempo, sul server l’update viene negato. Quindi qualche istante dopo (misurato in millisecondi se Meteor è in esecuzione sul proprio computer), il server restituisce l'errore, istruendo la collezione locale a ripristinare la modifica.

Risultato finale: durante l'attesa di una risposta da parte del server, l'interfaccia utente non può far altro che considerare attendibile la collezione locale. Non appena il server nega la modifica, l'interfaccia utente si adatta di conseguenza.

Classifica dei post

Ora che ciascun post ha un punteggio dipendente dal numero dei voti, si può visualizzare una lista dei migliori post. Per far ciò, si vedrà come sia possibile gestire due sottoscrizioni separate operanti sulla collezione dei post, e rendere maggiormente generico il template postsList.

Per cominciare, sono necessarie due sottoscrizioni, una per ciascun ordinamento. Il trucco sta nel fatto che entrambe sranno sottoscritte alla stessa pubblicazione posts, solo con parametri differenti!

Verranno inoltre create due nuove route chiamate newPosts e bestPosts, accedibili tramite le URL rispettivamente /new e /best (ovviamente insieme a /new/5 e /best/5 per la paginazione).

Per realizzare quanto detto, verrà esteso PostsListController in due distinti controller NewPostsListController e BestPostsListController. Questo consentirà il riuso delle medesime opzioni di routing sia per la route home che per newPosts, a partire da un singolo NewPostsListController da cui ereditare. Tutti ciò dimostra quanto Iron Router possa essere flessibile.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5,
  limit: function() {
    return parseInt(this.params.postsLimit) || this.increment;
  },
  findOptions: function() {
    return {sort: this.sort, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().count() === this.limit();
    return {
      posts: this.posts(),
      nextPath: hasMore ? this.nextPath() : null
    };
  }
});

NewPostsListController = PostsListController.extend({
  sort: {submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.newPosts.path({postsLimit: this.limit() + this.increment})
  }
});

BestPostsListController = PostsListController.extend({
  sort: {votes: -1, submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.bestPosts.path({postsLimit: this.limit() + this.increment})
  }
});

Router.map(function() {
  this.route('home', {
    path: '/',
    controller: NewPostsListController
  });

  this.route('newPosts', {
    path: '/new/:postsLimit?',
    controller: NewPostsListController
  });

  this.route('bestPosts', {
    path: '/best/:postsLimit?',
    controller: BestPostsListController
  });
  // ..
});
lib/router.js

Da notare che avendo adesso più di una route, la logica dietro nextPath è stata spostata da PostsListController a NewPostsListController BestPostsListController, poiché il percorso sarà diverso in entrambi i casi.

Inoltre, quando si ordina per votes si ha un parametro di ordinamento aggiuntivo per timestamp di submit, per assicurare che l'ordinamento sia corretto.

Dopo aver introdotto i nuovi controller, possiamo ora rimuovere senza problemi la route postsList precedentemente definita. Cancelliamo qunidi il seguente codice:

 this.route('postsList', {
  path: '/:postsLimit?',
  controller: PostsListController
 })
lib/router.js

Aggiungeremo anche i collegamenti nell'intestazione:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li>
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li>
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{> loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Dobbiamo anche aggiornare il gestore dell'evento di cancellazione dei post:

  'click .delete': function(e) {
    e.preventDefault();

    if (confirm("Delete this post?")) {
      var currentPostId = this._id;
      Posts.remove(currentPostId);
      Router.go('home');
    }
  }
client/views/posts_edit.js

Fatto ciò, si ottiene la lista dei migliori post:

Ordinamento per punteggio
Ordinamento per punteggio

Commit 13-5

Added routes for post lists, and pages to display them.

Migliorare l'Header

Avendo due pagine che elencano i post, non è chiaro quale delle due liste è correntemente visualizzata. È necessario quindi modificare l'header per rendere la cosa esplicita. Viene creato un gestore header.js e una funzione di supporto che usa il percorso corrente e una o più route per aggiungere una classe attiva alle voci di nagivatione.

La ragione per cui si vuole supportare più route è che sia home che newPosts (a cui corrispondono le URL rispettivamente / e /new) puntano allo stesso template. Quindi activeRouteClass deve essere sufficientemente intelligente da rendere il tag <li>attivo in entrambi i casi.

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li class="{{activeRouteClass 'home' 'newPosts'}}">
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li class="{{activeRouteClass 'bestPosts'}}">
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li class="{{activeRouteClass 'postSubmit'}}">
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{> loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html
Template.header.helpers({
  activeRouteClass: function(/* route names */) {
    var args = Array.prototype.slice.call(arguments, 0);
    args.pop();

    var active = _.any(args, function(name) {
      return Router.current() && Router.current().route.name === name
    });

    return active && 'active';
  }
});
client/views/includes/header.js
Evidenziazione della pagina attiva
Evidenziazione della pagina attiva

Parametri dell'Helper

Non è stato usato finora questo specifico pattern, ma come ogni tag Spacebars, i tag del template helper possono ricevere parametri.

E se da un lato è possibile passare parametri con nome alla funzione, è anche possibile passare un numero non specificato di parametri anonimi, acceduti tramite l'oggetto arguments dall'interno della funzione.

In quest'ultimo caso, può risultare conveniente convertire l'oggetto arguments in un array JavaScript, e quindi utilizzare il metodo pop() al fine di liberarsi dell'hash aggiunto da Spacebars.

Per ciascuna voce di navigazione, l'helper activeRouteClass accetta una lista di nomi di route, e quindi usa la funzione any() di Underscore per verificare se il route passa il test (ovvero se la URL corrispondente coincide con il percorso corrente).

Se una qualunque delle route coincide con il path corrente, any() restituisce true. Per concludere, si utilizza il pattern Javascript boolean && string, dove false && myString restituisce false, ma true && myString restituisce invece myString.

Commit 13-6

Added active classes to the header.

Ora che gli utenti possono votare post in tempo reale, è possibile vedere i post muoversi su e giù nella home page in risposta a cambiamenti nel loro punteggio. Non sarebbe meglio se esistesse il modo di rendere il tutto più fluido con alcune animazioni?

Pubblicazioni avanzate

Sidebar 13.5

A questo punto dovresti avere una buona comprensione di come pubblicazioni e sottoscrizioni interagiscono. Cerchiamo quindi di sbarazzarci delle ruote di supporto ed esaminiamo un paio di scenari avanzati.

Pubblicare una collezione più volte

Nel nostro primo approfondimento sulle pubblicazioni, abbiamo visto alcune delle tecniche più comuni su pubblicazioni e sottoscrizioni, ora abbiamo imparato come la funzione _publishCursor le ha rese molto facili da implementare sui nostri siti.

Iniziamo ricordando cosa _publishCursor fa per noi esattamente: prende tutti i documenti che corrispondono ad un certo cursore e li invia alla collezione con lo stesso nome sul client. Da notare che il nome della pubblicazione non è coinvolto.

Questo significa che possiamo avere più di una pubblicazione di collegamento tra le collezioni sul client e sul server.

Abbiamo già incontrato questo pattern nel nostro capitolo sulla paginazione, quando abbiamo pubblicato un sottoinsieme paginato di tutti i post in aggiunta al post corrente visualizzato.

Un altro simile caso d'uso è quello di pubblicare una panoramica di un grande set di documenti, come anche tutti i dettagli di un singolo elemento:

Pubblicare una collezione due volte
Pubblicare una collezione due volte
Meteor.publish('allPosts', function() {
  return Posts.find({}, {fields: {title: true, author: true}});
});

Meteor.publish('postDetail', function(postId) {
  return Posts.find(postId);
});

Ora, quando il client sottoscrive queste due pubblicazioni (utilizzando autorun per garantire che il giusto postID sia stato inviato alla sottoscrizione postDetail), la sua collezione 'posts' viene popolata da due sorgenti: un elenco di titoli, i nomi degli autori della prima sottoscrizione e tutti i dettagli di un post dalla seconda.

Si potrebbe notare che il post pubblicato da postDetail viene anche pubblicato da allPosts (anche se con un solo sottoinsieme delle sue proprietà). Tuttavia, Meteor si prende cura della sovrapposizione unendo i campi e garantendo che nessun post sia duplicato.

Questa è una bella cosa, perché ora quando renderizziamo l'elenco sommario dei post, gestiamo degli oggetti di dati che hanno sufficienti dati per mostrare quello che ci serve. Quando renderizziamo la pagina per un singolo post, quindi, abbiamo tutto quello che serve per mostrarlo. Certo, dovremo verificare che sul client tutti i campi siano disponibili su tutti i post - questa è una situazione comune!

Facciamo presente che non si è limitati al solo variare le proprietà del documento, si potrebbero benissimo pubblicare le stesse proprietà di entrambe le pubblicazioni, ma ordinare gli elementi in modo diverso.

Meteor.publish('newPosts', function(limit) {
  return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});

Meteor.publish('bestPosts', function(limit) {
  return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
server/publications.js

Sottoscrivere una pubblicazione più volte

Abbiamo appena visto come sia possibile pubblicare una singola collezione più di una volta. Allo stesso modo si può realizzare un risultato molto simile con un altro pattern: la creazione di una singola pubblicazione, ma sottoscrivendoci più volte.

In Microscope, sottoscriviamo la pubblicazione posts più volte, ma Iron Router attiva e disattiva ogni sottoscrizione per noi. Non c'è alcun motivo per cui non ci si possa sottoscrivere più volte contemporaneamente.

Ad esempio, diciamo che abbiamo voluto caricare sia i più recenti che i migliori post in memoria allo stesso tempo:

Sottoscriversi due volte a una pubblicazione
Sottoscriversi due volte a una pubblicazione

Stiamo impostando una singola pubblicazione:

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

E poi sottoscriviamo questa pubblicazione più volte. In realtà questo è più o meno quello che stiamo facendo in Microscope:

Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});

Quindi cosa sta succedendo esattamente? Ogni browser sta aprendo due sottoscrizioni differenti, ciascuna che si connette alla stessa pubblicazione sul server.

Ogni sottoscrizione fornisce diversi argomenti per tale pubblicazione ma, fondamentalmente, ogni volta un (diverso) set di documenti viene raccolto dalla collezione posts e trasferito sulla collezione client-side.

Collezioni multiple in una singola sottoscrizione

A differenza dei database relazionali più tradizionali come MySQL, che fanno uso di join, i database NoSQL come Mongo sono tutti su denormalizzazione e incorporamento (embedding). Vediamo come funziona nel contesto di Meteor.

Diamo uno sguardo ad un esempio concreto. Abbiamo aggiunto commenti ai nostri post e, finora, siamo stati felici di pubblicare solo i commenti sul singolo post che l'utente sta guardando.

Tuttavia, supponiamo di voler mostrare i commenti su tutti i post in prima pagina (tenendo presente che questi post cambieranno nel momento in cui verranno impaginati, essendo solo quelli in prima pagina a dover mostrare i commenti). Questo caso d'uso rappresenta un buon motivo per l'incorporamento dei commenti nei post, ed in effetti è ciò che ci ha spinto a denormalizzare il contatore dei commenti.

Naturalmente potremmo sempre solo incorporare i commenti nei post, sbarazzandoci della collezione Comments. Ma come abbiamo visto nel capitolo Denormalizzazione, così facendo perderemmo alcuni vantaggi supplementari di lavorare con una collezione separata.

Scopriamo ora un trucco che coinvolge le sottoscrizioni e che permette di inserire i nostri commenti preservando le collezioni separate.

Supponiamo che, insieme con la nostra lista di post in prima pagina, vogliamo sottoscriverci ad una lista dei primi 2 commenti per ogni post.

Sarebbe difficile realizzare questo con una pubblicazione commenti indipendente, soprattutto se l'elenco dei post fosse limitato in qualche modo (per esempio, gli ultimi 10). Dovremmo scrivere una pubblicazione che assomiglierebbe a qualcosa di simile:

Due collezioni in una sottoscrizione
Due collezioni in una sottoscrizione
Meteor.publish('topComments', function(topPostIds) {
  return Comments.find({postId: topPostIds});
});

Questo sarebbe un problema dal punto di vista delle prestazioni, visto che la pubblicazione dovrebbe essere abbattuta e ristabilita ogni volta l'elenco dei topPostIds cambia.

C'è però un modo per aggirare questo ostacolo. Abbiamo appena sfruttato il non poter avere più di una pubblicazione per collezione, ma possiamo anche avere più di una collezione per pubblicazione:

Meteor.publish('topPosts', function(limit) {
  var sub = this, commentHandles = [], postHandle = null;

  // send over the top two comments attached to a single post
  function publishPostComments(postId) {
    var commentsCursor = Comments.find({postId: postId}, {limit: 2});
    commentHandles[post._id] =
      Meteor.Collection._publishCursor(commentsCursor, sub, 'comments');
  }

  postHandle = Posts.find({}, {limit: limit}).observeChanges({
    added: function(id, post) {
      publishPostComments(post._id);
      sub.added('posts', id, post);
    },
    changed: function(id, fields) {
      sub.changed('posts', id, fields);
    },
    removed: function(id) {
      // stop observing changes on the post's comments
      commentHandles[id] && commentHandles[id].stop();
      // delete the post
      sub.removed('posts', id);
    }
  });

  sub.ready();

  // make sure we clean everything up (note `_publishCursor`
  //   does this for us with the comment observers)
  sub.onStop(function() { postsHandle.stop(); });
});

Possiamo notare che non stiamo restituendo nulla in questa pubblicazione, visto che trasmettiamo manualmente messaggi alla nostra sub (via .added() e simili). Non abbiamo quindi bisogno di chiedere a _publishCursor di farlo per noi restituendo un cursore.

Ora, ogni volta che pubblichiamo un post, pubblichiamo anche automaticamente i primi due commenti ad esso collegati, il tutto con una sola chiamata di sottoscrizione!

Sebbene Meteor non renda ancora questo approccio molto semplice, puoi anche guardare nel pacchetto publish-with-relations su Atmosphere, che mira a rendere questo pattern più facile da usare.

Collegare collezioni differenti

Che altro può darci la nostra conoscenza ritrovata sulla flessibilità delle sottoscrizioni? Beh, se non usiamo _publishCursor, non abbiamo bisogno di seguire il vincolo che la collezione sorgente sul server deve avere lo stesso nome della collezione di destinazione sul client.

Una collezione per due sottoscrizioni
Una collezione per due sottoscrizioni

Una ragione per cui vorremmo fare questo è l’ ereditarietà su singola tabella (Single Table Inheritance).

Supponiamo che abbiamo voluto fare riferimento a vari tipi di oggetti da parte dei nostri post, ognuno dei quali memorizza campi comuni, ma anche che differivano leggermente nel contenuto. Ad esempio, potremmo costruire un motore di blogging Tumblr-like, dove ogni post ha la solita ID, timestamp e titolo; ma in più potrebbe essere caratterizzato da un'immagine, un video, un collegamento o semplicemente del testo.

Potremmo memorizzare tutti questi oggetti in una singola collezione 'risorse', utilizzando un attributo tipo per indicare quale tipo di oggetto che sono. (video, immagine, link, ecc.)

Se avessimo una singola collezione risorse sul server, potremmo trasformare questa collezione unica in multiple (Video, Immagini, ecc) collezioni sul client col seguente pizzico di magia:

  Meteor.publish('videos', function() {
    var sub = this;

    var videosCursor = Resources.find({type: 'video'});
    Meteor.Collection._publishCursor(videosCursor, sub, 'videos');

    // _publishCursor doesn't call this for us in case we do this more than once.
    sub.ready();
  });

Stiamo dicendo a _publishCursor di pubblicare i nostri video come il cursore farebbe, ma invece di pubblicare la collezione di risorse sul client, pubblichiamo da “‘risorse’” a 'video'.

È una buona idea farlo? Non è nostro compito giudicare. In ogni caso, è bene sapere che è possibile farlo per poter scegliere il metodo migliore per la nostra applicazione, utilizzando Meteor al massimo!

Animazioni

14

A questo punto abbiamo un sistema di votazione, conto punteggi e valutazione in tempo reale. Questo tuttavia risulta in un'esperienza erratica ed irritante, con messaggi che saltano da una parte all'altra della homepage. Per migliorare questa situazione useremo le animazioni.

Meteor & il DOM

Prima di iniziare la parte divertente (fare sì che le cose si muovano), dobbiamo capire come Meteor interagisce con il DOM (Document Object Model – la collezione di elementi HTML che costituiscono i contenuti di una pagina).

È cruciale ricordarsi che gli elementi non possono essere spostati. Possono solamente essere eliminati e creati (nota che questa è una limitazione del DOM, non di Meteor). Quindi per dare l'illusione che gli elementi A e B cambino di posto, Meteor cancellerà l'elemento B e inserirà una copia nuova di zecca (B’) prima dell'elemento A.

Questo rende l'animazione difficoltosa, siccome non puoi semplicemente animare B per muoverlo in una nuova posizione, perché B sarà scomparso non appena Meteor renderizza la pagina (che come sappiamo accade instantaneamente, grazie alla reactivity). Dovrai invece animare B’ mentre si muove dalla vecchia posizione di B verso la sua nuova posizione prima di A.

Per scambiare i messaggi A e B (posizionati rispettivamente nelle posizioni p1 e p2), dobbiamo seguire i seguenti punti:

  1. Elimina B
  2. Nel DOM, prima di A crea B’
  3. Muovi B’ verso p2
  4. Muovi A verso p1
  5. Anima A verso p2
  6. Anima B’ verso p1

Questi punti sono esposti in dettaglio nel seguente diagramma:

Scambiare due messaggi
Scambiare due messaggi

Nota che nei punti 3 e 4 non stiamo animando A e B’ verso le loro posizioni, ma li stiamo “teleportando” istantaneamente. Questo darà l'illusione che B non è mai stato cancellato e posizionerà entrambi gli elementi così che possano essere animati verso la loro nuova posizione.

Fortunatamente Meteor si prende cura dei punti 1 & 2, quindi ci dobbiamo preoccupare solamente dei punti da 3 a 6.

Nei punti 5 e 6 inoltre, stiamo semplicemente spostando gli elementi nelle loro giuste posizioni. Quindi le uniche parti di cui ci dobbiamo veramente preoccupare sono i punti 3 e 4, cioè mandare gli elementi verso il punto iniziale dell'animazione.

Tempismo giusto

Fino ad ora abbiamo parlato di come animare i nostri messaggi, ma non di dove animarli.

Per i punti 3 e 4, la risposta sta nella template callback rendered all'interno del gestore post_item.js, che è scatenato ogni volta che cambia la proprietà di un messaggio (nel nostro caso il punteggio).

I punti 5 e 6 sono un pò più complessi. Pensaci su: se tu dicessi ad un automata di correre verso nord per 5 minuti, dopodichè di correre verso sud per 5 minuti, probabilmente l'automata dedurrà che siccome finirà nello stesso posto, potrebbe risparmiarsi le sue forze e non correre per niente.

Quindi se vuoi assicurarti che il tuo automata corra per tutti e 10 i minuti, devi aspettare fino a che non ha corso i primi 5 minuti, e dopo dirgli di tornare indietro.

Il browser funziona in una simile maniera: se simultaneamente gli diamo entrambe le istruzioni, le nuove coordinate semplicemente sostituirebbero quelle vecchie e non accadrebbe nulla. In altre parole, il browser ha bisogno di registrare i cambiamenti di posizione come punti separati nel tempo, altrimenti non sarà in grado di animarli.

Meteor non fornisce una callback justAfterRendered, ma può imitarla usando Meteor.defer(), che semplicemente prende una funzione e pospone la sua esecuzione appena in tempo per registrarsi come un evento diverso.

Posizionamento con i CSS

Per animare i messaggi che si stanno riordinando per la pagina, dovremo avventurarci nel mondo dei CSS. È quindi d'ordine un breve ripasso sul posizionamento on i CSS.

Gli elementi di una pagina sono predefiniti per avere un posizionamento statico. Gli elementi posizionati staticamente si adattano al flusso della pagina e le loro coordinate sullo schermo non possono essere cambiate o animate.

Un posizionamento relativo dall'altro canto invece significa che l'elemento anche si adatta al flusso della pagina, ma può essere posizionato relativamente alla sua posizione originale*.

Un posizionamento assoluto va un passo più in avanti e ti permette di specificare delle coordinate x/y relative al documento oppure al primo elemento padre posizionato relativamente o assolutamente.

Per animare i nostri messaggi useremo un posizionamento relativo.

.post{
  position:relative;
  transition:all 300ms 0ms ease-in;
}
client/stylesheets/style.css

Questo permette di fare facilmente i punti 5 e 6: dobbiamo semplicemente impostare top a 0px (il suo valore predefinito) così i nostri messaggi scorreranno indietro verso la loro posizione “normale”.

Questo vuol dire che la nostra unica sfida è quella di calcolare da dove animarli (punti 3 e 4), relativamente alla loro nuova posizione. In altre parole, di quanto compensarli. Ma anche questo non è molto difficile: la giusta compensazione è semplicemente la posizione del messaggio precedente meno quella del nuovo.

Position:absolute

Per posizionare i nostri elementi potremmo anche usare position:absolute con un padre relativo. Ma un gran svantaggio degli elementi posizionati assolutamente è che sono completamente rimossi dal flusso della pagina, causando il collasso del loro contenitore padre come se fosse vuoto.

A sua volta questo significa che dovremmo impostare l'altezza del contenitore artificialmente con Javascript, invece di lasciare che il browser aggiusti naturalmente gli elementi. Di conseguenza, ogni qual volta sia possibile è meglio rimanere con il posizionamento relativo.

Richiamo totale

Tuttavia abbiamo ancora un problema. Mentre l'elemento A persiste nel DOM e quindi può “ricordare” la sua posizione precedente, l'elemento B viene reincarnato e riprende vita sotto forma di B’, con la memoria cancellata.

Meteor fortunatamente viene alla riscossa dandoci accesso all'oggetto istanza di template nella callback rendered. La documentazione di Meteor illustra:

Nel body della callback, this è un oggetto istanza di template che è unico a questa occorrenza del template ed è persistente tra diversi renderings.

Quello che faremo quindi, è trovare la posizione corrente di un messaggio nella pagina e salvare la posizione nell'oggetto istanza di template. In questa maniera, saremo in grado di sapere da dove animare il messaggio, anche quando viene eliminato e ricreato.

Le istanze di template ci permetteno anche di accedere alla collezione di dati tramite la proprietà data. Questo ci tornerà utile per prendere il punteggio di un messaggio.

Punteggio dei messaggi

Abbiamo parlato del punteggio dei messaggi, ma questo “valore” effettivamente non esiste come una proprietà del messaggio, sicome è semplicemente una conseguenza dell'ordine dei messaggi che sono elencati nella nostra collezione. Dovremo in qualche maniera trovare un modo per far apparire questa proprietà dall'aria se vogliamo essere in grado di animare i messaggi a seconda del loro punteggio.

Nota che siccome il punteggio è una proprietà relativa che dipende da come ordini i messaggi (un messaggio può essere valutato primo mentre si ordina per data, ma terzo quando si ordina per punteggio), non possiamo mettere questa proprietà rank nel database stesso.

Idealmente metteremmo la proprietà nelle nostre collezioni newPosts e topPosts, ma Meteor al momento non offre un meccanismo conveniente per farlo.

Inseriremo invece rank all'ultimo momento possibile, nel postList template manager:

Template.postsList.helpers({
  postsWithRank: function() {
    this.posts.rewind();
    return this.posts.map(function(post, index, cursor) {
      post._rank = index;
      return post;
    });
  }
});
/client/views/posts/posts_list.js

Invee di ritornare il cursore Posts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()}) come nei nostri posts helpers precedenti, postsWithRank prende il cursore e aggiunge la proprietà _rank per ognuno dei suoi documenti.

E non dimenticarti di aggiornare il template postsList:

<template name="postsList">
  <div class="posts">
    {{#each postsWithRank}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
/client/views/posts/posts_list.html

Sii cortese, riavvolgi

Meteor è uno dei web frameworks più progressisti e all'avanguardia che ci siano. Ma una delle sue funzionalità, la funzione rewind(), sembra essere un ritorno ai giorni passati delle video cassette e dei VCRs.

Ogni volta che usi un cursore con forEach(), map(), oppure fetch(), dovrai riavvolgerlo prima di poterlo usare di nuovo.

E in alcuni casi è meglio stare sul sicuro e riavvolgere il cursore in maniera preventiva piuttosto che rischiare un errore.

Mettere tutto insieme

Ora possiamo mettere tutto insieme usando la template callback rendered del manager in post_item.js per la logica della nostra animazione:

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // anima questo messaggio dalla posizione precedente a quella nuova
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // se l'elemento ha una currentPosition (non è il primo rendering)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // calcola la differenza tra vecchia e nuova posizione ed invia lì l'elemento
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  }

  // lascia che venga disegnato nella vecchia posizione, dopodichè...
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // porta l'elemento indietro alla sua posizione originale
    $this.css("top",  "0px");
  });
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Commit 14-1

Added post reordering animation.

Seguire non dovrebbe essere troppo difficile se fai riferimento al diagramma precedente.

Nota che siccome abbiamo impostato la proprietà currentPosition dell'istanza di template nella callback defer, questa proprietà non esisterà al primo rendering del frammento di template. Ma non è un problema siccome in ogni modo non siamo interessati ad animare il primo rendering.

Apri il sito e prova a votare. Dovresti vedere i messaggi, che con grazia da ballerine, si spostano gentilmente su e giù!

Animare nuovi messaggi

Ora i nostri messaggi si riordinano correttamente, ma non abbiamo ancora un'animazione per un “nuovo messaggio”. Invece di far spuntare immediatamente i nuovi messaggi, facciamoli apparire gradualmente.

In verità questo è più complesso di quel che sembra. Il problema è che la callback rendered di Meteor viene scatenata in due casi separati:

  1. Quando un nuovo template viene inserito nel DOM
  2. Quando i dati del template vengono cambiati

Solamente il primo caso dovrebbe essere animato, a meno che tu non voglia un'interfaccia utente che si illumina come un albero di natale ogni volta che i tuoi dati cambiano.

Assicuriamoci quindi di animare solamente i messaggi che sono veramente nuovi e non quelli che vengono ri-renderizzati perché i loro dati sono cambiati. Stiamo già testando per la presenza di una variabile d'istanza (che è impostata solamente dopo il primo render), quindi dobbiamo solo tornare indietro alla nostra callback rendered e aggiungere un blocco else:

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // anima questo messaggio dalla posizione precedente a quella nuova
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // se l'elemento ha una currentPosition (non è il primo rendering)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // calcola la differenza tra vecchia e nuova posizione ed invia lì l'elemento
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  } else {
    // è il primo evento render in assoluto, quindi nascondi l'elemento
    $this.addClass("invisible");
  }

  // lascia che venga disegnato nella vecchia posizione, dopodichè...
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // porta l'elemento indietro alla sua posizione originale
    $this.css("top",  "0px").removeClass("invisible");
  });
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Commit 14-2

Fade items in when they are drawn.

Nota che la removeClass("invisible") che abbiamo aggiunto nella funzione defer() verrà eseguita ad ogni rendering. Ma farà qualcosa solamente se la classe .invisible è presente sull'elemento, il che è vero solamente la prima volta che è renderizzato.

CSS & JavaScript

Avrai notato che stiamo usando una classe CSS .invisible per scatenare l'animazione invece di animare direttamente la proprietà CSS opacity come abbiamo fatto per top. Questo è perché per top avevamo bisogno di animare la proprietà usando un valore specifico che dipende dai dati dell'istanza.

Dall'altro canto qui vogliamo solo mostrare e nascondere un elemento, indipendentemente dai suoi dati. Siccome è una buona idea lasciare il più possibile CSS fuori da Javascript, qui aggiungeremo e toglieremo solamente la classe e specificheremo i dettagli dell'animazione nel nostro stylesheet.

Finalmente dovremmo avere l'animazione che volevamo! Lancia la tua app e provala! Puoi anche divertirti con le classi .post e .post.invisible per vedere se riesci a trovare altri modi di animare. Suggerimento: CSS easing functions è un buon punto per cominciare!

Prossimi passi

14.5

Gli scorsi capitoli vi hanno dato una buona idea di come si costruisce un applicazione in Meteor. Come possiamo continuare ora?

Capitoli extra

Come prima cosa potete acquistare l'edizione Full o Premium per sbloccare l'accesso ai capitoli extra. Questi capitoli vi guideranno attraverso scenari reali come costruire un API per la vostra applicazione, integrarla con servizi di terze parti e migrare i vostri dati.

Evented Mind

Se volete dare uno sguardo più approfondito agli aspetti più intricati di Meteor, raccomandiamo fortemente di visitare Evented Mind di Chris Mather, una piattaforma di lezioni video, con più di 50 video su Meteor (e nuovi video aggiunti ogni settimana).

MeteorHacks

Uno dei modi migliori per tenersi al passo con le informazioni su Meteor è quello di iscriversi alla newsletter settimanale di Arunoda Susiripala, MeteorHacks. Il blog di MeteorHacks è una grande risorsa di suggerimenti avanzati per Meteor.

Atmosphere

Atmosphere, l'archivio non ufficiale di pacchetto Meteor, è un altro grande posto dove imparare: potete scoprire nuovi pacchetti e dare un occhio al loro codice per vedere quali schemi vengono usati.

Attenzione: Atmosphere è mantenuto in parte da Tom Coleman, uno degli autori di questo libro.

Meteorpedia

Meteorpedia è una wiki per tutto ciò che riguarda Meteor. E ovviamente è fatta con Meteor!

Il Podcast di Meteor

Josh e Ry del negozio Meteor Differential registrano il Podcast di Meteor ogni settimana, un altro modo per tenersi aggiornati su cosa accade nella comunità di Meteor.

Altre Risorse

Stephan Hochhaus ha compilato una lista abbastanza esauriente di risorse per Meteor.

Il blog di Manuel Schoebel è un altra buona sorgente di post su Meteor così il blog Gentlenode.

Ottenere Aiuto

Se vi trovate bloccati, il miglior posto per chiedere è Stack Overflow. Assicuratevi di taggare la domanda con il tag meteor.

Comunità

Infine, il miglior modo per restare aggiornati è di essere attivi nella comunità. Raccomandiamo di registrarsi alla mailing list di Meteor, di seguire i Google Groups Meteor Core e Meteor Talk, e di creare un account sul forum di Meteor Crater.io.

Vocabolario Meteor

Sidebar 14.5

In questo libro leggerete alcune parole che potrebbero sembrarvi nuove, o almeno usate in maniera differente se riferite a Meteor. Useremo questo capitolo per definirle.

Client

Quando parliamo di Client, ci riferiamo al codice eseguito nel browser dell'utente, sia che si tratti di un browser tradizionale come Firefox o Safari, o qualcosa di più complesso, come una UIWebView di un'applicazione nativa per iPhone.

Collezione

In Meteor una collezione è un contenitore di dati che si sincronizzano automaticamente tra client e server. Le collezioni hanno un nome (ad esempio ‘articoli’), e in generale sono presenti sia sul client che sul server. Anche se si comportano in maniera differente, hanno un API comune basata sull'API di Mongo.

Computazione

Una computazione è un blocco di codice che viene eseguito ogni volta che una delle fonti di dati reattivi dalle quali dipende subisce un cambiamento. Se avete un sorgente di dati reattivi (ad esempio, una variabile di sessione), e volete rispondere in maniera reattiva, dove predisporre su di essa una computazione.

Cursore

Un cursore è il risultato di effettuare una query su una collezione di Mongo. Lato client, un cursore non è solamente un'array di risultati, ma un oggetto reattivo che può essere rilevato quando vengono aggiunti, eliminati e modificati gli oggetti nella collezione.

DDP

DDP è il Protocollo di Dati Distribuiti di Meteor, il protocollo di rete usato per sincronizzare le collezioni ed eseguire chiamate ai metodi. DDP va inteso come un protocollo generico, che prende il posto dell'HTTP per applicazioni in tempo reale che fanno uso intensivo di dati.

Deps

Deps è il sistema di reattività di Meteor. Deps è usato dietro le quinte per tenere l'HTML automaticamente sincronizzato con il sottostante modello di dati.

Documento

Mongo è un database che si basa sui documenti, ogni oggetto che fa parte di una collezioni si chiama infatti “documento”. Sono semplici oggetti Javascript (anche se non possono contenere funzioni) con una speciale proprietà, l’_id, che Meteor utilizza per tener traccia delle loro proprietà attraverso il protocollo DDP.

Helpers

Quando un template deve renderizzare qualcosa di più complesso della proprietà di un documento può chiamare un helper (aiutante), cioè una funzione che può aiutare nella renderizzazione.

Compensazione di Latenza

È la tecnica che permette la simulazione delle chiamate dei metodi lato client per evitare ritardi in attesa che il server risponda.

Method

Un metodo di Meteor è le chiamata ad una procedura remota dal client al server, con un po’ di logica speciale per tener traccia dei cambiamenti delle collezioni e permettere la compensazione di latenza.

MiniMongo

La collezione lato client è un archivio dati in memoria che offre un API simile a quella di Mongo. La libreria che permette questo comportamento si chiama “MinoMongo”, per indicare che è una versione ridotta di Mongo che viene eseguita interamente in memoria.

Pacchetti

Un pacchetto di Meteor può essere 1. Codice Javascript eseguito sul server. 2. Codice Javascript eseguito sul client. 3. Istruzioni su come processare le risorse (come SASS in CSS). 4. Risorse che devono essere processate.

Un pacchetto è come una libreria super potenziata. Meteor include già una larga serie di pacchetti di base. È disponibile anche Atmosphere, che è una raccolta di pacchetti di terze parti gestita dalla comunità.

Pubblicazione

Una pubblicazione è un insieme di dati con un nome specifico, personalizzato per ogni utente che vi fa una sottoscrizione. Le pubblicazioni sono impostate sul server.

Server

Il server di Meteor è un server HTTP e DDP che funziona tramite node.js. Consiste in tutte le librerie di Meteor e nel vostro codice Javascript lato server. Quando avviate il server di Meteor, esso si connette a un database Mongo (che si auto avvia in fase di sviluppo).

Sessione

La Sessione in Meteor si riferisce alla fonte di dati reattivi presente sul client che l'applicazione utilizza per tenere traccia dello stato in cui si trova l'utente.

Sottoscrizione

Una sottoscrizione è una connessione ad una pubblicazione per uno specifico client. La sottoscrizione è il codice che viene eseguito nel browser che comunica con una pubblicazione sul server e mantiene sincronizzati i dati.

Template

Un template è un modo per generare HTML tramite Javascript. Per impostazione predefinita, Meteor supporta Spacebars, un sistema di templating logic-less, cioè privo di istruzioni if o cicli for, anche se si prevede di supportarne altri in futuro.

Contesto dei dati di un Template

Quando un template viene renderizzato, fa riferimento ad un oggetto Javascript che fornisce dati specifici per quella particolare renderizzazione. Nella maggior parte dei casi si tratta di semplici oggetti Javascript (POJOs, plain-old-JavaScript-objects), di solito documenti da una collezione, ma a volte possono essere anche più complessi e contenere all'interno delle funzioni.