Esercizio - Creare un'applicazione Web di base

Completato

Per il momento, nella macchina virtuale Ubuntu sono installati MongoDB e Node.js. A questo punto, è ora di creare un'applicazione Web di base per vedere tutto in azione. Lungo il percorso, si capirà anche il ruolo di AngularJS ed Express.

Un modo eccellente per imparare è seguire un esempio. L'applicazione Web che verrà creata implementerà un database di libri di base. L'applicazione Web consente di visualizzare informazioni sui libri, aggiungere nuovi libri ed eliminare libri esistenti.

Inoltre, l'applicazione Web utilizzata di seguito illustra molti concetti che si applicano alla maggior parte delle applicazioni Web dello stack MEAN. In base alle proprie esigenze e interessi, è possibile esplorare le funzionalità necessarie per le proprie applicazioni dello stack MEAN.

Ecco che aspetto avrà l'applicazione Web Books.

Screenshot of a web page with a form and submission button.

Ecco il compito di ogni componente dello stack MEAN.

  • In MongoDB si archiviano le informazioni sui libri.
  • Express.jsi esegue il routing di ogni richiesta HTTP al gestore appropriato.
  • Con AngularJS si connette l'interfaccia utente con la logica di business del programma.
  • In Node.js si ospita l'applicazione lato server.

Importante

Ai fini dell'apprendimento, in questo esercizio si creerà un'applicazione Web di base. Il suo scopo è testare lo stack MEAN per ricavarne un'idea del funzionamento. L'applicazione non è sufficientemente sicura né pronta per essere usata in un ambiente di produzione.

Informazioni su Express

Finora, nella macchina virtuale sono stati installati MongoDB e Node.js. Che cos'è Express.js, la E nell'acronimo MEAN?

Express.js è un framework di server Web creato per Node.js che semplifica il processo di creazione delle applicazioni Web.

Lo scopo principale di Express è di gestire il routing delle richieste. Per routing si intende il modo in cui l'applicazione risponde a una richiesta proveniente da un endpoint specifico. Un endpoint è costituito da un percorso, o URI, e da un metodo di richiesta, ad esempio GET o POST. Ad esempio, si può rispondere a una richiesta GET all'endpoint /book specificando l'elenco di tutti i libri presenti nel database. Si può rispondere a una richiesta POST allo stesso endpoint aggiungendo una voce al database sulla base dei campi che l'utente ha immesso in un modulo Web.

Nell'applicazione Web che verrà creata a breve si userà Express per il routing delle richieste HTTP e per la restituzione del contenuto Web all'utente. Express facilita anche l'uso delle applicazioni Web con i cookie HTTP, oltre che l'elaborazione delle stringhe di query.

Express è un pacchetto Node.js. Per installare e gestire i pacchetti Node.js si usa l'utilità npm. Più avanti in questa unità, si creerà un file denominato package.json per definire Express e altre dipendenze, quindi si eseguirà il comando npm install per installare tali dipendenze.

Informazioni su AngularJS

Al pari di Express, neanche AngularJS, la lettera A dell'acronimo MEAN, è stato ancora installato.

AngularJS facilita la scrittura e il test delle applicazioni Web in quanto consente di separare meglio l'aspetto della pagina Web, vale a dire il codice HTML, dal suo comportamento. Se si ha familiarità con il modello model-view-controller (MVC) o con il concetto di data binding, AngularJS non riserverà sorprese.

AngularJS è ciò che viene definito un framework JavaScript front-end, pertanto è sufficiente che sia disponibile nel client che accede all'applicazione. In altre parole, AngularJS viene eseguito nel browser Web dell'utente, non nel server Web. Poiché AngularJS è un linguaggio JavaScript, è possibile usarlo per recuperare facilmente dal server Web i dati da visualizzare nella pagina.

AngularJS non viene installato. Un riferimento al file JavaScript viene invece aggiunto alla pagina HTML, proprio come accade con altre librerie JavaScript. È possibile includere AngularJS nelle pagine Web in diversi modi. In questo caso si caricherà AngularJS da una rete per la distribuzione di contenuti o rete CDN. Una rete CDN è un modo per distribuire geograficamente immagini, video e altri contenuti migliorando la velocità di download.

Per il momento, non aggiungere questo codice. Nell'esempio riportato di seguito AngularJS viene caricato da una rete CDN. Il codice viene in genere aggiunto alla sezione <head> della pagina HTML.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.2/angular.min.js"></script>

Nota

AngularJS non va confuso con Angular. Anche se in entrambi molti concetti sono simili, AngularJS è il predecessore di Angular. AngularJS viene ancora comunemente usato per la compilazione di applicazioni Web. Mentre AngularJS si basa su JavaScript, Angular si basa su TypeScript, un linguaggio di programmazione che semplifica la scrittura di programmi JavaScript.

Come si compila l'applicazione?

In questo esempio si userà un processo di base. Si scriverà il codice dell'applicazione di Cloud Shell e quindi si userà SCP, ovvero il protocollo per la copia di sicurezza, per copiare i file nella macchina virtuale. L'applicazione Node.js viene quindi avviata e i risultati vengono visualizzati nel browser.

In pratica, l'applicazione Web viene scritta e testata in un ambiente più locale, ad esempio un portatile o una macchina virtuale eseguita localmente. È quindi possibile archiviare il codice in un sistema di controllo della versione, ad esempio Git, e usare un sistema di integrazione continua e recapito continuo (CI/CD), ad esempio Azure DevOps, per testare le modifiche e caricarle nella macchina virtuale. Altre risorse utili sono indicate alla fine di questo modulo.

Creare l'applicazione Web Books

In questa unità si creerà tutto il codice, gli script e i file HTML che costituiscono l'applicazione Web. Per brevità, le parti importanti di ogni file sono evidenziate, ma non vengono incluse informazioni dettagliate complete.

Se si è ancora connessi alla macchina virtuale con SSH, eseguire exit per chiudere la sessione SSH e tornare a Cloud Shell.

exit

Ora si è di nuovo nella sessione Cloud Shell.

Creare i file

  1. In Cloud Shell eseguire questi comandi per creare le cartelle e i file per l'applicazione Web:

    cd ~
    mkdir Books
    touch Books/server.js
    touch Books/package.json
    mkdir Books/app
    touch Books/app/model.js
    touch Books/app/routes.js
    mkdir Books/public
    touch Books/public/script.js
    touch Books/public/index.html
    

    Sono inclusi gli elementi seguenti:

    • Books è la directory radice del progetto.
      • server.js definisce il punto di ingresso all'applicazione Web. Carica i pacchetti Node.js necessari, specifica la porta di ascolto e inizia l'ascolto del traffico HTTP in ingresso.
      • package.json include informazioni sull'applicazione, tra cui il nome, la descrizione e i pacchetti Node.js che l'applicazione deve eseguire.
    • Books/app contiene codice che viene eseguito nel server.
      • model.js definisce la connessione di database e lo schema. Può essere considerato come il modello di dati per l'applicazione.
      • routes.js gestisce il routing delle richieste. Definisce ad esempio le richieste GET all'endpoint /book specificando l'elenco di tutti i libri presenti nel database.
    • Books/public contiene i file serviti direttamente al browser del client.
      • index.html contiene la pagina di indice. Contiene un modulo Web che consente all'utente di inviare informazioni relative ai libri. Visualizza anche tutti i libri presenti nel database e consente di eliminare le voci dal database.
      • script.js contiene il codice JavaScript eseguito nel browser dell'utente. Può inviare richieste al server per ottenere l'elenco dei libri, aggiungere libri al database ed eliminare libri dal database.
  2. Eseguire il comando code per aprire i file tramite l'editor di Cloud Shell.

    code Books
    

Creare il modello di dati

  1. Aprire app/model.js dall'editor e aggiungere quanto segue:

    var mongoose = require('mongoose');
    var dbHost = 'mongodb://localhost:27017/Books';
    mongoose.connect(dbHost, { useNewUrlParser: true } );
    mongoose.connection;
    mongoose.set('debug', true);
    var bookSchema = mongoose.Schema( {
        name: String,
        isbn: {type: String, index: true},
        author: String,
        pages: Number
    });
    var Book = mongoose.model('Book', bookSchema);
    module.exports = Book;
    

    Importante

    Ogni volta che si incolla o si modifica il codice in un file nell'editor, assicurarsi di salvare le modifiche tramite il menu "..." o il tasto di scelta rapida (CTRL+S in Windows e Linux o Comando+S in macOS).

    Il codice usa Mongoose per semplificare il processo di trasferimento dei dati da e verso MongoDB. Mongoose è un sistema basato su schema per la modellazione dei dati. Il codice definisce un documento di database denominato "Book" con lo schema specificato. Lo schema definisce quattro campi che descrivono un singolo libro:

    • Nome o titolo del libro
    • Codice ISBN (International Standard Book Number) che identifica il libro in modo univoco
    • Autore
    • Numero di pagine contenute

    Creare quindi i gestori HTTP che eseguono il mapping delle richieste GET, POST e DELETE alle operazioni del database.

Creare le route di Express.js che gestiscono le richieste HTTP

  1. Aprire app/routes.js dall'editor e aggiungere il codice seguente:

    var path = require('path');
    var Book = require('./model');
    var routes = function(app) {
        app.get('/book', function(req, res) {
            Book.find({}, function(err, result) {
                if ( err ) throw err;
                res.json(result);
            });
        });
        app.post('/book', function(req, res) {
            var book = new Book( {
                name:req.body.name,
                isbn:req.body.isbn,
                author:req.body.author,
                pages:req.body.pages
            });
            book.save(function(err, result) {
                if ( err ) throw err;
                res.json( {
                    message:"Successfully added book",
                    book:result
                });
            });
        });
        app.delete("/book/:isbn", function(req, res) {
            Book.findOneAndRemove(req.query, function(err, result) {
                if ( err ) throw err;
                res.json( {
                    message: "Successfully deleted the book",
                    book: result
                });
            });
        });
        app.get('*', function(req, res) {
            res.sendFile(path.join(__dirname + '/public', 'index.html'));
        });
    };
    module.exports = routes;
    

    Il codice crea quattro route per l'applicazione. Di seguito viene offerta una breve panoramica di ognuna.

    Verbo HTTP Endpoint Descrizione
    GET /book Recupera tutti i libri dal database.
    POST /book Crea un oggetto Book che si basa sui campi specificati dall'utente nel modulo Web e lo scrive nel database.
    DELETE /book/:isbn Elimina il libro identificato dal codice ISBN dal database.
    GET * Restituisce la pagina di indice quando non si trova una corrispondenza con altre route.

    Express.js può gestire le risposte HTTP direttamente nel codice di gestione delle route oppure può gestire il contenuto statico proveniente dai file. In questo codice sono illustrati entrambi. Le prime tre route restituiscono dati JSON per le richieste API relative ai libri. La quarta route (il caso predefinito) restituisce il contenuto del file di indice, index.html.

Creare l'applicazione JavaScript lato client

  1. Aprire public/script.js dall'editor e aggiungere il codice seguente:

    var app = angular.module('myApp', []);
    app.controller('myCtrl', function($scope, $http) {
        var getData = function() {
            return $http( {
                method: 'GET',
                url: '/book'
            }).then(function successCallback(response) {
                $scope.books = response.data;
            }, function errorCallback(response) {
                console.log('Error: ' + response);
            });
        };
        getData();
        $scope.del_book = function(book) {
            $http( {
                method: 'DELETE',
                url: '/book/:isbn',
                params: {'isbn': book.isbn}
            }).then(function successCallback(response) {
                console.log(response);
                return getData();
            }, function errorCallback(response) {
                console.log('Error: ' + response);
            });
        };
        $scope.add_book = function() {
            var body = '{ "name": "' + $scope.Name +
            '", "isbn": "' + $scope.Isbn +
            '", "author": "' + $scope.Author +
            '", "pages": "' + $scope.Pages + '" }';
            $http({
                method: 'POST',
                url: '/book',
                data: body
            }).then(function successCallback(response) {
                console.log(response);
                return getData();
            }, function errorCallback(response) {
                console.log('Error: ' + response);
            });
        };
    });
    

    Si noti come il codice definisca un modulo denominato "myApp" e un controller denominato "myCtrl". Il funzionamento di moduli e controller non verrà illustrato in dettaglio in questa sede, ma questi nomi verranno usati nel passaggio successivo per associare l'interfaccia utente (codice HTML) alla logica di business dell'applicazione.

    In precedenza sono state create quattro route che gestiscono varie operazioni GET, POST e DELETE nel server. Questo codice è simile a quelle stesse operazioni, ma sul lato client, ovvero il Web browser dell'utente.

    La funzione getData, ad esempio, invia una richiesta GET all'endpoint /book. È importante ricordare che il server gestisce la richiesta recuperando dal database le informazioni relative a tutti i libri e restituendo queste informazioni sotto forma di dati JSON. Si noti come i dati JSON risultanti vengano assegnati alla variabile $scope.books. Nel prossimo passaggio si apprenderà come ciò influisca su quanto viene visualizzato dall'utente nella pagina Web.

    Al caricamento della pagina, il codice chiama la funzione getData. È possibile esaminare le funzioni del_book e add_book per avere un'idea del relativo funzionamento. Per la corrispondenza con il gestore predefinito del server non è necessario alcun codice lato client perché il gestore predefinito restituisce la pagina di indice, non dati JSON.

Creare l'interfaccia utente

  1. Aprire public/index.html dall'editor e aggiungere il codice seguente:

    <!doctype html>
    <html ng-app="myApp" ng-controller="myCtrl">
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.2/angular.min.js"></script>
        <script src="script.js"></script>
    </head>
    <body>
        <div>
        <table>
            <tr>
            <td>Name:</td>
            <td><input type="text" ng-model="Name"></td>
            </tr>
            <tr>
            <td>Isbn:</td>
            <td><input type="text" ng-model="Isbn"></td>
            </tr>
            <tr>
            <td>Author:</td>
            <td><input type="text" ng-model="Author"></td>
            </tr>
            <tr>
            <td>Pages:</td>
            <td><input type="number" ng-model="Pages"></td>
            </tr>
        </table>
        <button ng-click="add_book()">Add</button>
        </div>
        <hr>
        <div>
        <table>
            <tr>
            <th>Name</th>
            <th>Isbn</th>
            <th>Author</th>
            <th>Pages</th>
            </tr>
            <tr ng-repeat="book in books">
            <td><input type="button" value="Delete" data-ng-click="del_book(book)"></td>
            <td>{{book.name}}</td>
            <td>{{book.isbn}}</td>
            <td>{{book.author}}</td>
            <td>{{book.pages}}</td>
            </tr>
        </table>
        </div>
    </body>
    </html>
    

    Il codice crea un modulo HTML di base con quattro campi per inviare i dati relativi ai libri e una tabella in cui sono visualizzati tutti i libri archiviati nel database.

    Anche se si tratta di codice HTML standard, gli attributi HTML ng- possono essere poco noti. Questi attributi HTML collegano il codice AngularJS all'interfaccia utente. Ad esempio, quando si seleziona Aggiungi, AngularJS chiama la funzione add_book che invia i dati del modulo al server.

    È possibile esaminare il codice per avere un'idea di come ognuno degli attributi ng- si colleghi alla logica di business dell'applicazione.

Creare il server Express.js per ospitare l'applicazione

  1. Aprire server.js dall'editor e aggiungere il codice seguente:

    var express = require('express');
    var bodyParser = require('body-parser');
    var app = express();
    app.use(express.static(__dirname + '/public'));
    app.use(bodyParser.json());
    require('./app/routes')(app);
    app.set('port', 80);
    app.listen(app.get('port'), function() {
        console.log('Server up: http://localhost:' + app.get('port'));
    });
    

    Questo codice crea l'applicazione Web vera e propria. Rende disponibili i file statici dalla directory public e usa le route definite in precedenza per gestire le richieste.

Definire informazioni e dipendenze del pacchetto

Tenere presente che package.json offre informazioni sull'applicazione, tra cui il nome, la descrizione e i pacchetti Node.js che l'applicazione deve eseguire.

  1. Aprire package.json dall'editor e aggiungere il codice seguente:

    {
      "name": "books",
      "description": "Sample web app that manages book information.",
      "license": "MIT",
      "repository": {
        "type": "git",
        "url": "https://github.com/MicrosoftDocs/mslearn-build-a-web-app-with-mean-on-a-linux-vm"
      },
      "main": "server.js",
      "dependencies": {
        "express": "~4.16",
        "mongoose": "~5.3",
        "body-parser": "~1.18"
      }
    }
    

Verranno visualizzati informazioni o metadati riguardanti l'applicazione, tra cui il nome, la descrizione e la licenza.

Il campo repository specifica il punto in cui è conservato il codice. Per informazioni di riferimento, è poi possibile esaminare il codice su GitHub all'URL indicato qui.

Il campo main definisce il punto di ingresso dell'applicazione. Viene specificato qui per motivi di completezza, ma non si tratta di un'informazione importante in quanto non è prevista la pubblicazione dell'applicazione come pacchetto Node.js che altri utenti possono scaricare e usare.

Il campo dependencies è invece importante. Definisce i pacchetti Node.js necessari per l'applicazione. A breve si verrà connessi alla macchina virtuale una seconda volta e si eseguirà il comando npm install per installare questi pacchetti.

I pacchetti Node usano generalmente lo schema di controllo delle versioni Schema di controllo delle versioni. Il numero di versione contiene tre componenti: versione principale, versione secondaria e patch. La notazione ~ tilde indica a npm di installare la versione patch più recente nelle versione principale e in quella secondaria specificate. Le versioni visualizzate si riferiscono alle versioni più recenti usate per testare il modulo. In pratica, è possibile incrementare poi la versione mentre si aggiorna ed esegue il test dell'applicazione, se si vogliono usare le funzionalità più recenti offerte da ogni pacchetto dipendente.

Copiare i file nella macchina virtuale

Prima di procedere, assicurarsi di avere a portata di mano l'indirizzo IP della macchina virtuale. In caso contrario, eseguire questi comandi da Cloud Shell per recuperarlo:

ipaddress=$(az vm show \
  --name MeanStack \
  --resource-group "<rgn>[sandbox resource group name]</rgn>" \
  --show-details \
  --query [publicIps] \
  --output tsv)
echo $ipaddress
  1. La modifica dei file è stata completata. Assicurarsi di aver salvato le modifiche apportate a ogni file e chiudere l'editor.

    Per chiudere l'editor, selezionare i puntini di sospensione nell'angolo in alto a destra e quindi selezionare Chiudi editor.

  2. Eseguire il comando seguente scp per copiare il contenuto della directory ~/Books nella sessione di Cloud Shell nello stesso nome di directory nella macchina virtuale:

    scp -r ~/Books azureuser@$ipaddress:~/Books
    

Installare pacchetti Node aggiuntivi

Si supponga che durante il processo di sviluppo siano stati identificati pacchetti Node aggiuntivi che si vogliono usare. Ad esempio si tenga presente che app/model.js inizia con questa riga.

var mongoose = require('mongoose');

È importante ricordare che l'applicazione usa Mongoose per consentire il trasferimento dei dati da e verso il database MongoDB.

Per l'applicazione sono necessari anche Express.js e i pacchetti body-parser. Body-parser è un plug-in che consente a Express di usare i dati contenuti nel modulo Web inviato dal client.

Connettersi alla macchina virtuale e installare i pacchetti specificati in package.json.

  1. Prima di connettersi alla macchina virtuale, assicurarsi di avere l'indirizzo IP della macchina virtuale. Se non si ha l'indirizzo, eseguire i comandi di Cloud Shell della sezione precedente per recuperarlo.

  2. Come in precedenza, creare una connessione SSH alla macchina virtuale:

    ssh azureuser@$ipaddress
    
  3. Passare alla directory Books nella home directory:

    cd ~/Books
    
  4. Eseguire npm install per installare i pacchetti dipendenti:

    sudo apt install npm -y && npm install
    

Mantenere la connessione SSH aperta per la sezione successiva.

Testare l'applicazione

A questo punto è possibile testare l'applicazione Web Node.js.

  1. Dalla directory ~/Books eseguire questo comando per avviare l'applicazione Web:

    sudo nodejs server.js
    

    Questo comando avvia l'applicazione restando in ascolto sulla porta 80 per le richieste HTTP in ingresso.

  2. In un'altra scheda del browser, passare all'indirizzo IP pubblico della macchina virtuale.

    Verrà visualizzata la pagina di indice che include un modulo Web.

    Screenshot of the book web page with a form and submission button.

    Provare ad aggiungere alcuni libri al database. Dopo aver aggiunto un libro, la pagina aggiorna l'elenco completo dei libri.

    Screenshot of the book web page with sample data populated.

    Per eliminare un libro dal database, è anche possibile selezionare Elimina.