Routing

5

Percentuale Tradotta

In questo capitolo imparerai:

  • Il routing in Meteor.
  • Creazione di pagine di discussione dei post, con URL unici.
  • Come linkare correttamente questi URL.
  • 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.