Pic-a-daily: lab 4 - Creazione di un frontend web

1. Panoramica

In questo codelab, creerai un frontend web su Google App Engine che consentirà agli utenti di caricare immagini dall'applicazione web e di sfogliare le immagini caricate e le relative miniature.

21741cd63b425aeb.png

Questa applicazione web utilizzerà un framework CSS chiamato Bulma, per avere un'interfaccia utente interessante, e anche il framework frontend JavaScript Vue.JS per chiamare l'API dell'applicazione che creerai.

Questa applicazione è composta da tre schede:

  • Una home page che mostra le miniature di tutte le immagini caricate, insieme all'elenco delle etichette che descrivono l'immagine (quelle rilevate dall'API Cloud Vision in un lab precedente).
  • Una pagina collage in cui viene visualizzato il collage composto dalle quattro immagini caricate più di recente.
  • Una pagina di caricamento in cui gli utenti possono caricare nuove immagini.

Il frontend risultante ha il seguente aspetto:

6a4d5e5603ba4b73.png

Queste tre pagine sono semplici pagine HTML:

  • La home page (index.html) chiama il codice backend di Node App Engine per ottenere l'elenco di immagini in miniatura e le relative etichette, tramite una chiamata AJAX all'URL /api/pictures. La home page utilizza Vue.js per recuperare questi dati.
  • La pagina del collage (collage.html) rimanda all'immagine collage.png che riunisce le quattro immagini più recenti.
  • La pagina di caricamento (upload.html) offre un semplice modulo per caricare un'immagine tramite una richiesta POST all'URL /api/pictures.

Cosa imparerai a fare

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. Configurazione e requisiti

Configurazione dell'ambiente da seguire in modo autonomo

  1. Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai ancora un account Gmail o Google Workspace, devi crearne uno.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Il Nome progetto è il nome visualizzato dei partecipanti del progetto. Si tratta di una stringa di caratteri non utilizzata dalle API di Google e può essere aggiornata in qualsiasi momento.
  • L'ID progetto deve essere univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo essere stato impostato). La console Cloud genera automaticamente una stringa univoca. di solito non ti importa cosa sia. Nella maggior parte dei codelab, devi fare riferimento all'ID progetto (che solitamente è identificato come PROJECT_ID), quindi, se non ti piace, generane un altro a caso oppure puoi fare un tentativo personalizzato e controllare se è disponibile. Poi c'è "congelato" dopo la creazione del progetto.
  • C'è un terzo valore, il numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, dovrai abilitare la fatturazione nella console Cloud per utilizzare le risorse/le API Cloud. Eseguire questo codelab non dovrebbe costare molto. Per arrestare le risorse in modo da non incorrere in fatturazione oltre questo tutorial, segui eventuali "pulizie" istruzioni riportate alla fine del codelab. I nuovi utenti di Google Cloud sono idonei al programma prova senza costi di 300$.

Avvia Cloud Shell

Anche se Google Cloud può essere utilizzato da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

Dalla console Google Cloud, fai clic sull'icona di Cloud Shell nella barra degli strumenti in alto a destra:

55efc1aaa7a4d3ad.png

Dovrebbe richiedere solo qualche istante per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere una schermata simile al seguente:

7ffe5cbb04455448.png

Questa macchina virtuale viene caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita su Google Cloud, migliorando notevolmente le prestazioni di rete e l'autenticazione. Tutto il lavoro in questo lab può essere svolto semplicemente con un browser.

3. Abilita API

App Engine richiede l'API Compute Engine. Assicurati che sia abilitata:

gcloud services enable compute.googleapis.com

L'operazione dovrebbe essere completata correttamente:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. Clona il codice

Controlla il codice, se non lo hai già fatto:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

Quindi puoi passare alla directory contenente il frontend:

cd serverless-photosharing-workshop/frontend

Per il frontend avrai il seguente layout di file:

frontend
 |
 ├── index.js
 ├── package.json
 ├── app.yaml
 |
 ├── public
      |
      ├── index.html
      ├── collage.html
      ├── upload.html
      |
      ├── app.js
      ├── script.js
      ├── style.css

Alla base del nostro progetto sono presenti 3 file:

  • index.js contiene il codice Node.js
  • package.json definisce le dipendenze della libreria
  • app.yaml è il file di configurazione per Google App Engine

Una cartella public contiene le risorse statiche:

  • index.html è la pagina che mostra tutte le miniature e le etichette
  • collage.html mostra il collage delle immagini recenti
  • upload.html contiene un modulo per caricare nuove immagini
  • app.js utilizza Vue.js per compilare la pagina index.html con i dati
  • script.js gestisce il menu di navigazione e il relativo "hamburger" icona su schermi piccoli
  • style.css definisce alcune istruzioni CSS

5. Esplora il codice

Dipendenze

Il file package.json definisce le dipendenze di libreria necessarie:

{
  "name": "frontend",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@google-cloud/firestore": "^3.4.1",
    "@google-cloud/storage": "^4.0.0",
    "express": "^4.16.4",
    "dayjs": "^1.8.22",
    "bluebird": "^3.5.0",
    "express-fileupload": "^1.1.6"
  }
}

La nostra applicazione dipende da:

  • firestore: per accedere a Cloud Firestore con i metadati di immagine,
  • storage: per accedere a Google Cloud Storage dove sono archiviate le immagini,
  • express: il framework web per Node.js,
  • dayjs: una piccola raccolta per mostrare le date in modo semplice,
  • bluebird: una libreria di promessa JavaScript,
  • express-fileupload: una libreria per gestire facilmente i caricamenti di file.

Frontend Express

All'inizio del controller index.js avrai bisogno di tutte le dipendenze definite in precedenza in package.json:

const express = require('express');
const fileUpload = require('express-fileupload');
const Firestore = require('@google-cloud/firestore');
const Promise = require("bluebird");
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const path = require('path');
const dayjs = require('dayjs');
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)

Quindi, viene creata l'istanza dell'applicazione Express.

Vengono utilizzati due middleware Express:

  • La chiamata express.static() indica che nella sottodirectory public saranno disponibili risorse statiche.
  • Inoltre, fileUpload() configura il caricamento dei file in modo da limitare le dimensioni dei file a 10 MB, in modo da caricarli localmente nel file system in memoria della directory /tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

Tra le risorse statiche ci sono i file HTML per la home page, la pagina del collage e la pagina di caricamento. Queste pagine chiameranno il backend dell'API. Questa API avrà i seguenti endpoint:

  • POST /api/pictures Tramite il modulo in upload.html, le immagini verranno caricate tramite una richiesta POST.
  • GET /api/pictures Questo endpoint restituisce un documento JSON contenente l'elenco di immagini e le relative etichette
  • GET /api/pictures/:name Questo URL reindirizza al percorso di Cloud Storage dell'immagine a grandezza originale
  • GET /api/thumbnails/:name Questo URL reindirizza alla posizione di Cloud Storage dell'immagine in miniatura
  • GET /api/collage Questo ultimo URL reindirizza alla posizione di archiviazione sul cloud dell'immagine collage generata

Caricamento immagine

Prima di esplorare il codice Node.js per il caricamento di immagini, dai un'occhiata a public/upload.html.

... 
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
    ... 
    <input type="file" name="pictures">
    <button>Submit</button>
    ... 
</form>
... 

L'elemento del modulo punta all'endpoint /api/pictures, con un metodo POST HTTP e un formato in più parti. index.js ora deve rispondere all'endpoint e al metodo ed estrarre i file:

app.post('/api/pictures', async (req, res) => {
    if (!req.files || Object.keys(req.files).length === 0) {
        console.log("No file uploaded");
        return res.status(400).send('No file was uploaded.');
    }
    console.log(`Receiving files ${JSON.stringify(req.files.pictures)}`);

    const pics = Array.isArray(req.files.pictures) ? req.files.pictures : [req.files.pictures];

    pics.forEach(async (pic) => {
        console.log('Storing file', pic.name);
        const newPicture = path.resolve('/tmp', pic.name);
        await pic.mv(newPicture);

        const pictureBucket = storage.bucket(process.env.BUCKET_PICTURES);
        await pictureBucket.upload(newPicture, { resumable: false });
    });


    res.redirect('/');
});

Innanzitutto, controlli che siano effettivamente in corso il caricamento di file. Successivamente, potrai scaricare i file localmente con il metodo mv del modulo Nodo di caricamento file. Ora che i file sono disponibili nel file system locale, carichi le immagini nel bucket Cloud Storage. Infine, reindirizzi l'utente alla schermata principale dell'applicazione.

Elenco delle immagini

È ora di mostrare le tue splendide foto.

Nel gestore /api/pictures, esamini la raccolta pictures del database Firestore, per recuperare tutte le immagini (la cui miniatura è stata generata), in ordine decrescente per data di creazione.

Esegui il push di ogni immagine in un array JavaScript, con il nome, le etichette che la descrivono (provenienti dall'API Cloud Vision), il colore dominante e una data di creazione semplice (con dayjs, applichiamo compensazioni temporali relative come "3 giorni da adesso").

app.get('/api/pictures', async (req, res) => {
    console.log('Retrieving list of pictures');

    const thumbnails = [];
    const pictureStore = new Firestore().collection('pictures');
    const snapshot = await pictureStore
        .where('thumbnail', '==', true)
        .orderBy('created', 'desc').get();

    if (snapshot.empty) {
        console.log('No pictures found');
    } else {
        snapshot.forEach(doc => {
            const pic = doc.data();
            thumbnails.push({
                name: doc.id,
                labels: pic.labels,
                color: pic.color,
                created: dayjs(pic.created.toDate()).fromNow()
            });
        });
    }
    console.table(thumbnails);
    res.send(thumbnails);
});

Questo controller restituisce i risultati con la forma seguente:

[
   {
      "name": "IMG_20180423_163745.jpg",
      "labels": [
         "Dish",
         "Food",
         "Cuisine",
         "Ingredient",
         "Orange chicken",
         "Produce",
         "Meat",
         "Staple food"
      ],
      "color": "#e78012",
      "created": "a day ago"
   },
   ...
]

Questa struttura di dati viene utilizzata da un piccolo snippet Vue.js dalla pagina index.html. Ecco una versione semplificata del markup di quella pagina:

<div id="app">
        <div class="container" id="app">
                <div id="picture-grid">
                        <div class="card" v-for="pic in pictures">
                                <div class="card-content">
                                        <div class="content">
                                                <div class="image-border" :style="{ 'border-color': pic.color }">
                                                        <a :href="'/api/pictures/' + pic.name">
                                                                <img :src="'/api/thumbnails/' + pic.name">
                                                        </a>
                                                </div>
                                                <a class="panel-block" v-for="label in pic.labels" :href="'/?q=' + label">
                                                        <span class="panel-icon">
                                                                <i class="fas fa-bookmark"></i> &nbsp;
                                                        </span>
                                                        {{ label }}
                                                </a>
                                        </div>
                                </div>
                        </div>
            </div>
        </div>
</div>

L'ID del tag div indicherà a Vue.js che si tratta della parte del markup che verrà sottoposta a rendering dinamico. Le iterazioni vengono eseguite grazie alle istruzioni v-for.

Le immagini hanno un bordo colorato corrispondente al colore dominante nell'immagine, come trovato dall'API Cloud Vision. Indichiamo le miniature e le immagini a larghezza intera nel link e nelle fonti delle immagini.

Infine, elenchiamo le etichette che descrivono l'immagine.

Ecco il codice JavaScript per lo snippet Vue.js (nel file public/app.js importato nella parte inferiore della pagina index.html):

var app = new Vue({
  el: '#app',
  data() {
    return { pictures: [] }
  },
  mounted() {
    axios
      .get('/api/pictures')
      .then(response => { this.pictures = response.data })
  }
})

Il codice Vue utilizza la libreria Axios per effettuare una chiamata AJAX al nostro endpoint /api/pictures. I dati restituiti vengono quindi associati al codice di visualizzazione nel markup visto in precedenza.

Visualizzazione delle immagini

Da index.html i nostri utenti possono visualizzare le miniature delle immagini, farci clic sopra per visualizzare le immagini a grandezza originale, mentre per collage.html gli utenti visualizzano l'immagine collage.png.

Nel markup HTML di queste pagine, l'immagine src e il link href puntano a questi tre endpoint, che reindirizzano alle posizioni di immagini, miniature e collage in Cloud Storage. Non è necessario impostare il percorso come hardcoded nel markup HTML.

app.get('/api/pictures/:name', async (req, res) => {
    res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_PICTURES}/${req.params.name}`);
});

app.get('/api/thumbnails/:name', async (req, res) => {
    res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/${req.params.name}`);
});

app.get('/api/collage', async (req, res) => {
    res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/collage.png`);
});

Esecuzione dell'applicazione Node

Una volta definiti tutti gli endpoint, l'applicazione Node.js è pronta per essere avviata. L'applicazione Express rimane in ascolto sulla porta 8080 per impostazione predefinita ed è pronta a gestire le richieste in entrata.

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
    console.log(`Started web frontend service on port ${PORT}`);
    console.log(`- Pictures bucket = ${process.env.BUCKET_PICTURES}`);
    console.log(`- Thumbnails bucket = ${process.env.BUCKET_THUMBNAILS}`);
});

6. Esegui test in locale

Testa il codice in locale per assicurarti che funzioni prima del deployment nel cloud.

Devi esportare le due variabili di ambiente corrispondenti ai due bucket Cloud Storage:

export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

All'interno della cartella frontend, installa le dipendenze npm e avvia il server:

npm install; npm start

Se tutto è andato a buon fine, il server dovrebbe essere avviato sulla porta 8080:

Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}

In questi log verranno visualizzati i nomi reali dei bucket, il che è utile per il debug.

Da Cloud Shell puoi usare la funzionalità di anteprima web per aprire il browser dell'applicazione in esecuzione in locale:

82fa3266d48c0d0a.png

Usa CTRL-C per uscire.

7. Esegui il deployment in App Engine

L'applicazione è pronta per il deployment.

Configura App Engine

Esamina il file di configurazione app.yaml per App Engine:

runtime: nodejs16
env_variables:
  BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT
  BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT

La prima riga dichiara che il runtime si basa su Node.js 10. Vengono definite due variabili di ambiente in modo da puntare ai due bucket, per le immagini originali e per le miniature.

Per sostituire GOOGLE_CLOUD_PROJECT con l'ID progetto effettivo, puoi eseguire questo comando:

sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml

Eseguire il deployment

Imposta la tua regione preferita per App Engine, assicurati di utilizzare la stessa regione nei lab precedenti:

gcloud config set compute/region europe-west1

Ed esegui il deployment:

gcloud app deploy

Dopo un paio di minuti, ti verrà comunicato che l'applicazione gestisce il traffico:

Beginning deployment of service [default]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 8 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://GOOGLE_CLOUD_PROJECT.appspot.com]
You can stream logs from the command line by running:
  $ gcloud app logs tail -s default
To view your application in the web browser run:
  $ gcloud app browse

Puoi anche visitare la sezione App Engine di Cloud Console per vedere che è stato eseguito il deployment dell'app ed esplorare funzionalità di App Engine come il controllo delle versioni e la suddivisione del traffico:

db0e196b00fceab1.png

8. Testa l'app

Per il test, vai all'URL predefinito di App Engine per l'app dell'app (https://<YOUR_PROJECT_ID>.appspot.com/). Dovresti vedere la UI frontend attiva e in esecuzione.

6a4d5e5603ba4b73.png

9. Libera spazio (facoltativo)

Se non intendi conservare l'app, puoi eseguire la pulizia delle risorse per risparmiare sui costi ed essere nel complesso un buon cittadino del cloud eliminando l'intero progetto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Complimenti!

Complimenti! Questa applicazione web Node.js ospitata su App Engine collega tutti i tuoi servizi e consente agli utenti di caricare e visualizzare immagini.

Argomenti trattati

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Passaggi successivi