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, nonché 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 dall'aspetto gradevole, nonché il framework frontend JavaScript Vue.JS per chiamare l'API dell'applicazione che creerai.

Questa applicazione sarà composta da tre schede:

  • Una pagina Home che mostrerà 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 che mostrerà il collage composto dalle 4 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 pagina Home (index.html) chiama il codice di backend dell'app Node Engine per ottenere l'elenco delle miniature e delle relative etichette tramite una chiamata AJAX all'URL /api/pictures. La home page utilizza Vue.js per recuperare questi dati.
  • La pagina collage (collage.html) punta all'immagine collage.png che assembla le 4 foto più recenti.
  • La pagina Carica (upload.html) offre un semplice modulo per caricare un'immagine tramite una richiesta POST all'URL /api/pictures.

Obiettivi didattici

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. Configurazione e requisiti

Configurazione dell'ambiente 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 del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google e puoi aggiornarla 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). Cloud Console genera automaticamente una stringa univoca, di solito non ti interessa di cosa si tratta. Nella maggior parte dei codelab, devi fare riferimento all'ID progetto (che in genere è identificato come PROJECT_ID), quindi, se non ti piace, generane un altro casuale oppure puoi provare il tuo e vedere se è disponibile. Viene "congelato" dopo la creazione del progetto.
  • Esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
  1. Successivamente, devi abilitare la fatturazione in Cloud Console per utilizzare le risorse/API Cloud. L'esecuzione di questo codelab non dovrebbe costare molto, se non nulla. Per arrestare le risorse in modo da non incorrere in costi di fatturazione al termine di questo tutorial, segui le istruzioni di "pulizia" riportate alla fine del codelab. I nuovi utenti di Google Cloud possono beneficiare del programma prova senza costi di 300$.

Avvia Cloud Shell

Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.

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

55efc1aaa7a4d3ad.png

Bastano pochi istanti per eseguire il provisioning e connettersi all'ambiente. Al termine, dovresti vedere un risultato simile a questo:

7ffe5cbb04455448.png

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

3. Abilita API

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

gcloud services enable compute.googleapis.com

Dovresti visualizzare il completamento dell'operazione:

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

4. Clona il codice

Controlla il codice, se non l'hai già fatto:

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

Puoi quindi andare alla directory contenente il frontend:

cd serverless-photosharing-workshop/frontend

Avrai il seguente layout di file per il frontend:

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

Nella radice del progetto, hai tre 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 la relativa icona "hamburger" sugli schermi piccoli
  • style.css definisce alcune direttive CSS

5. Esplora il codice

Dipendenze

Il file package.json definisce le dipendenze della 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 nostri metadati delle immagini,
  • storage: per accedere a Google Cloud Storage, dove sono archiviate le immagini,
  • express: il framework web per Node.js,
  • dayjs: una piccola libreria per mostrare le date in modo comprensibile,
  • bluebird: una libreria di promesse 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 package.json in precedenza:

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)

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

Vengono utilizzati due middleware Express:

  • La chiamata express.static() indica che le risorse statiche saranno disponibili nella sottodirectory public.
  • fileUpload() configura il caricamento dei file in modo da limitarne le dimensioni a 10 MB, per caricarli localmente nel file system in memoria nella 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, hai 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 delle immagini e delle relative etichette
  • GET /api/pictures/:name Questo URL reindirizza alla posizione di archiviazione cloud dell'immagine a grandezza naturale
  • GET /api/thumbnails/:name Questo URL reindirizza alla posizione di archiviazione cloud dell'immagine in miniatura
  • GET /api/collage Questo ultimo URL reindirizza alla posizione di archiviazione cloud dell'immagine del collage generata

Caricamento immagine

Prima di esplorare il codice Node.js per il caricamento delle 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 multipart. index.js ora deve rispondere a questo endpoint e 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, verifica che i file vengano effettivamente caricati. Poi scarichi i file localmente tramite il metodo mv del nostro modulo Node per il caricamento dei file. Ora che i file sono disponibili nel file system locale, carica le immagini nel bucket Cloud Storage. Infine, reindirizza l'utente alla schermata principale dell'applicazione.

Elenco delle immagini

È il momento di mostrare le tue bellissime foto.

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

Inserisci ogni immagine in un array JavaScript, con il relativo nome, le etichette che la descrivono (provenienti dall'API Cloud Vision), il colore dominante e una data di creazione intuitiva (con dayjs, offset temporali relativi come "3 giorni da oggi").

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 risultati della seguente forma:

[
   {
      "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 della 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 div indicherà a Vue.js che si tratta della parte del markup che verrà visualizzata in modo dinamico. Le iterazioni vengono eseguite grazie alle direttive v-for.

Le immagini hanno un bel bordo colorato corrispondente al colore dominante nell'immagine, come rilevato dall'API Cloud Vision. Indichiamo le miniature e le immagini a tutta larghezza nelle origini dei link e 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 della visualizzazione nel markup che hai visto in precedenza.

Visualizzare le immagini

Da index.html i nostri utenti possono visualizzare le miniature delle immagini, fare clic su di esse per visualizzare le immagini a grandezza naturale e da 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 Cloud Storage delle immagini, delle miniature e del collage. Non è necessario codificare il percorso 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

Con tutti gli endpoint definiti, l'applicazione Node.js è pronta per essere avviata. L'applicazione Express è 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. Testare localmente

Testa il codice localmente 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 avviarsi sulla porta 8080:

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

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

Da Cloud Shell, puoi utilizzare la funzionalità di anteprima web per sfogliare l'applicazione in esecuzione in locale:

82fa3266d48c0d0a.png

Usa CTRL-C per uscire.

7. Esegui il deployment in App Engine

La tua applicazione è pronta per il deployment.

Configurare 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. Sono definite due variabili di ambiente che puntano 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

Esegui il deployment

Imposta la regione che preferisci per App Engine, assicurandoti di utilizzare la stessa regione dei lab precedenti:

gcloud config set compute/region europe-west1

Esegui il deployment:

gcloud app deploy

Dopo un minuto o due, ti verrà comunicato che l'applicazione sta gestendo 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 verificare che l'app sia stata implementata ed esplorare le funzionalità di App Engine come il controllo delle versioni e la suddivisione del traffico:

db0e196b00fceab1.png

8. Testare l'app

Per eseguire il test, vai all'URL App Engine predefinito per l'app (https://<YOUR_PROJECT_ID>.appspot.com/) e dovresti vedere l'interfaccia utente frontend in esecuzione.

6a4d5e5603ba4b73.png

9. Pulizia (facoltativo)

Se non intendi conservare l'app, puoi ripulire le risorse per risparmiare sui costi ed essere 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 le immagini.

Argomenti trattati

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Passaggi successivi