Pic-a-daily: Lab 4 – Web-Front-End erstellen

1. Übersicht

In diesem Codelab erstellen Sie ein Web-Frontend in Google App Engine, mit dem Nutzer Bilder aus der Webanwendung hochladen und die hochgeladenen Bilder und ihre Miniaturansichten durchsuchen können.

21741cd63b425aeb.png

Für diese Webanwendung wird ein CSS-Framework namens Bulma verwendet, um eine ansprechende Benutzeroberfläche zu erstellen. Außerdem wird das JavaScript-Frontend-Framework Vue.JS verwendet, um die API der Anwendung aufzurufen, die Sie erstellen werden.

Diese Anwendung besteht aus drei Tabs:

  • Eine Startseite, auf der die Thumbnails aller hochgeladenen Bilder zusammen mit der Liste der Labels angezeigt werden, die das Bild beschreiben (die von der Cloud Vision API in einem vorherigen Lab erkannt wurden).
  • Eine Collage-Seite mit einer Collage aus den vier zuletzt hochgeladenen Bildern.
  • Eine Uploadseite, auf der Nutzer neue Bilder hochladen können.

Das resultierende Frontend sieht so aus:

6a4d5e5603ba4b73.png

Diese drei Seiten sind einfache HTML-Seiten:

  • Auf der Startseite (index.html) wird der Node App Engine-Backend-Code aufgerufen, um über einen AJAX-Aufruf der /api/pictures-URL die Liste der Vorschaubilder und deren Labels abzurufen. Auf der Startseite wird Vue.js zum Abrufen dieser Daten verwendet.
  • Die Seite Collage (collage.html) verweist auf das Bild collage.png, das aus den vier neuesten Bildern zusammengesetzt ist.
  • Auf der Seite upload (upload.html) finden Sie ein einfaches Formular zum Hochladen eines Bildes über eine POST-Anfrage an die URL /api/pictures.

Lerninhalte

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. Einrichtung und Anforderungen

Umgebung zum selbstbestimmten Lernen einrichten

  1. Melden Sie sich in der Google Cloud Console an und erstellen Sie ein neues Projekt oder verwenden Sie ein vorhandenes Projekt. Wenn Sie noch kein Gmail- oder Google Workspace-Konto haben, müssen Sie eines erstellen.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Der Projektname ist der Anzeigename für die Teilnehmer dieses Projekts. Es handelt sich um einen String, der nicht von Google APIs verwendet wird und den Sie jederzeit aktualisieren können.
  • Die Projekt-ID muss für alle Google Cloud-Projekte eindeutig sein und ist unveränderlich (kann nach der Festlegung nicht mehr geändert werden). In der Cloud Console wird automatisch ein eindeutiger String generiert. Normalerweise ist es nicht wichtig, wie dieser aussieht. In den meisten Codelabs müssen Sie auf die Projekt-ID verweisen (die in der Regel als PROJECT_ID angegeben wird). Wenn Ihnen die ID nicht gefällt, können Sie eine andere zufällige ID generieren oder eine eigene ID ausprobieren und sehen, ob sie verfügbar ist. Nachdem das Projekt erstellt wurde, wird es „eingefroren“.
  • Es gibt einen dritten Wert, die Projektnummer, die von einigen APIs verwendet wird. Weitere Informationen zu diesen drei Werten
  1. Als Nächstes müssen Sie die Abrechnung in der Cloud Console aktivieren, um Cloud-Ressourcen/-APIs verwenden zu können. Die Durchführung dieses Codelabs sollte keine oder nur geringe Kosten verursachen. Wenn Sie Ressourcen herunterfahren möchten, damit nach Abschluss dieses Codelabs keine Gebühren anfallen, folgen Sie den Bereinigungsanweisungen am Ende des Codelabs. Neue Nutzer von Google Cloud kommen für das Programm für kostenlose Testversionen mit einem Guthaben von 300$ infrage.

Cloud Shell starten

Während Sie Google Cloud von Ihrem Laptop aus per Fernzugriff nutzen können, wird in diesem Codelab Google Cloud Shell verwendet, eine Befehlszeilenumgebung, die in der Cloud ausgeführt wird.

Klicken Sie in der Google Cloud Console rechts oben in der Symbolleiste auf das Cloud Shell-Symbol:

55efc1aaa7a4d3ad.png

Die Bereitstellung und Verbindung mit der Umgebung sollte nur wenige Augenblicke dauern. Anschließend sehen Sie in etwa Folgendes:

7ffe5cbb04455448.png

Diese virtuelle Maschine verfügt über sämtliche Entwicklertools, die Sie benötigen. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Für dieses Lab benötigen Sie lediglich einen Browser.

3. APIs aktivieren

Für App Engine ist die Compute Engine API erforderlich. Prüfen Sie, ob die Funktion aktiviert ist:

gcloud services enable compute.googleapis.com

Der Vorgang sollte erfolgreich abgeschlossen werden:

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

4. Code klonen

Checken Sie den Code aus, falls noch nicht geschehen:

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

Anschließend können Sie das Verzeichnis mit dem Frontend aufrufen:

cd serverless-photosharing-workshop/frontend

Das Frontend hat die folgende Dateistruktur:

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

Im Stammverzeichnis unseres Projekts befinden sich drei Dateien:

  • index.js enthält den Node.js-Code.
  • package.json definiert die Bibliotheksabhängigkeiten.
  • app.yaml ist die Konfigurationsdatei für Google App Engine.

Ein public-Ordner enthält die statischen Ressourcen:

  • index.html ist die Seite mit allen Thumbnails und Labels.
  • collage.html zeigt die Collage der letzten Bilder
  • upload.html enthält ein Formular zum Hochladen neuer Bilder.
  • app.js verwendet Vue.js, um die Seite index.html mit den Daten zu füllen.
  • script.js verarbeitet das Navigationsmenü und das zugehörige Dreistrich-Symbol auf kleinen Bildschirmen.
  • style.css definiert einige CSS-Anweisungen.

5. Code ansehen

Abhängigkeiten

In der Datei package.json werden die erforderlichen Bibliotheksabhängigkeiten definiert:

{
  "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"
  }
}

Unsere Bewerbung hängt von Folgendem ab:

  • firestore: für den Zugriff auf Cloud Firestore mit unseren Bildmetadaten.
  • storage: für den Zugriff auf Google Cloud Storage, wo Bilder gespeichert sind,
  • express: das Web-Framework für Node.js,
  • dayjs: Eine kleine Bibliothek, mit der sich Datumsangaben in einem für Menschen lesbaren Format anzeigen lassen.
  • bluebird: eine JavaScript-Promise-Bibliothek
  • express-fileupload: Eine Bibliothek, mit der sich Dateiuploads einfach verarbeiten lassen.

Express-Frontend

Zu Beginn des index.js-Controllers benötigen Sie alle Abhängigkeiten, die zuvor in package.json definiert wurden:

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)

Als Nächstes wird die Express-Anwendungsinstanz erstellt.

Es werden zwei Express-Middlewares verwendet:

  • Der Aufruf express.static() gibt an, dass statische Ressourcen im Unterverzeichnis public verfügbar sind.
  • Mit fileUpload() wird der Dateiupload so konfiguriert, dass die Dateigröße auf 10 MB begrenzt wird und die Dateien lokal im In-Memory-Dateisystem im Verzeichnis /tmp hochgeladen werden.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

Zu den statischen Ressourcen gehören die HTML-Dateien für die Startseite, die Collageseite und die Uploadseite. Auf diesen Seiten wird das API-Back-End aufgerufen. Diese API hat die folgenden Endpunkte:

  • POST /api/pictures Über das Formular in „upload.html“ werden Bilder über eine POST-Anfrage hochgeladen.
  • GET /api/pictures Dieser Endpunkt gibt ein JSON-Dokument mit der Liste der Bilder und ihrer Labels zurück.
  • GET /api/pictures/:name Diese URL leitet zum Cloud Storage-Speicherort des Bildes in Originalgröße weiter.
  • GET /api/thumbnails/:name Diese URL leitet zum Cloud Storage-Speicherort des Miniaturbildes weiter.
  • GET /api/collage Diese letzte URL leitet zum Cloud Storage-Speicherort des generierten Collagebilds weiter.

Bild hochladen

Bevor Sie sich den Node.js-Code zum Hochladen von Bildern ansehen, werfen Sie einen kurzen Blick auf public/upload.html.

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

Das Formularelement verweist mit einer HTTP-POST-Methode und einem Multipart-Format auf den Endpunkt /api/pictures. Der index.js muss jetzt auf diesen Endpunkt und diese Methode reagieren und die Dateien extrahieren:

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('/');
});

Prüfen Sie zuerst, ob tatsächlich Dateien hochgeladen werden. Anschließend laden Sie die Dateien lokal über die mv-Methode aus unserem Node-Modul für den Dateiupload herunter. Nachdem die Dateien im lokalen Dateisystem verfügbar sind, laden Sie die Bilder in den Cloud Storage-Bucket hoch. Schließlich leiten Sie den Nutzer zurück zum Hauptbildschirm der Anwendung.

Bilder auflisten

Zeit, deine schönen Bilder zu präsentieren!

Im /api/pictures-Handler sehen Sie sich die Sammlung pictures der Firestore-Datenbank an, um alle Bilder abzurufen, deren Miniaturansicht generiert wurde. Die Bilder sind nach absteigendem Erstellungsdatum sortiert.

Jedes Bild wird in ein JavaScript-Array eingefügt, zusammen mit seinem Namen, den Labels, die es beschreiben (aus der Cloud Vision API), der dominanten Farbe und einem benutzerfreundlichen Erstellungsdatum (mit dayjs werden relative Zeitabweichungen wie „3 Tage ab jetzt“ angegeben).

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);
});

Dieser Controller gibt Ergebnisse in folgender Form zurück:

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

Diese Datenstruktur wird von einem kleinen Vue.js-Snippet auf der Seite index.html verwendet. Hier ist eine vereinfachte Version des Markups von dieser Seite:

<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>

Die ID des Div-Elements gibt Vue.js an, dass es sich um den Teil des Markups handelt, der dynamisch gerendert wird. Die Iterationen werden durch die v-for-Anweisungen ermöglicht.

Die Bilder erhalten einen farbigen Rahmen, der der dominanten Farbe im Bild entspricht, die von der Cloud Vision API ermittelt wurde. In den Link- und Bildquellen verweisen wir auf die Thumbnails und die Bilder in voller Breite.

Zuletzt werden die Labels aufgeführt, die das Bild beschreiben.

Hier ist der JavaScript-Code für das Vue.js-Snippet (in der Datei public/app.js, die unten auf der Seite index.html importiert wird):

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

Im Vue-Code wird die Axios-Bibliothek verwendet, um einen AJAX-Aufruf an unseren /api/pictures-Endpunkt zu senden. Die zurückgegebenen Daten werden dann an den Ansichtscode im Markup gebunden, das Sie zuvor gesehen haben.

Bilder ansehen

Über index.html können unsere Nutzer die Thumbnails der Bilder aufrufen und darauf klicken, um die Bilder in voller Größe zu sehen. Über collage.html können Nutzer das Bild collage.png aufrufen.

Im HTML-Markup dieser Seiten verweisen das Bild src und der Link href auf diese drei Endpunkte, die zu den Cloud Storage-Speicherorten der Bilder, Miniaturansichten und Collagen weiterleiten. Sie müssen den Pfad nicht im HTML-Markup hartcodieren.

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`);
});

Node-Anwendung ausführen

Nachdem alle Endpunkte definiert wurden, kann Ihre Node.js-Anwendung gestartet werden. Die Express-Anwendung überwacht standardmäßig Port 8080 und ist bereit, eingehende Anfragen zu verarbeiten.

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. Lokal testen

Testen Sie den Code lokal, um sicherzustellen, dass er funktioniert, bevor Sie ihn in der Cloud bereitstellen.

Sie müssen die beiden Umgebungsvariablen exportieren, die den beiden Cloud Storage-Buckets entsprechen:

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

Installieren Sie im Ordner frontend die npm-Abhängigkeiten und starten Sie den Server:

npm install; npm start

Wenn alles geklappt hat, sollte der Server auf Port 8080 gestartet werden:

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

In diesen Logs werden die tatsächlichen Namen Ihrer Buckets angezeigt, was für das Debugging hilfreich ist.

In Cloud Shell können Sie die Webvorschau verwenden, um die lokal ausgeführte Anwendung im Browser anzusehen:

82fa3266d48c0d0a.png

Verwenden Sie zum Beenden CTRL-C.

7. Auf der App Engine bereitstellen

Ihre Anwendung kann jetzt bereitgestellt werden.

App Engine konfigurieren

Sehen Sie sich die app.yaml-Konfigurationsdatei für App Engine an:

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

In der ersten Zeile wird deklariert, dass die Laufzeit auf Node.js 10 basiert. Es sind zwei Umgebungsvariablen definiert, die auf die beiden Buckets für die Originalbilder und die Miniaturansichten verweisen.

Wenn Sie GOOGLE_CLOUD_PROJECT durch Ihre tatsächliche Projekt-ID ersetzen möchten, können Sie den folgenden Befehl ausführen:

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

Bereitstellen

Legen Sie Ihre bevorzugte Region für App Engine fest. Verwenden Sie dabei dieselbe Region wie in den vorherigen Labs:

gcloud config set compute/region europe-west1

Und so stellen Sie sie bereit:

gcloud app deploy

Nach ein oder zwei Minuten wird Ihnen mitgeteilt, dass die Anwendung Traffic verarbeitet:

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

Sie können auch den App Engine-Bereich der Cloud Console aufrufen, um zu sehen, dass die App bereitgestellt wurde, und App Engine-Funktionen wie Versionsverwaltung und Trafficaufteilung zu nutzen:

db0e196b00fceab1.png

8. Anwendung testen

Rufen Sie zum Testen die App Engine-Standard-URL für die App (https://<YOUR_PROJECT_ID>.appspot.com/) auf. Die Frontend-Benutzeroberfläche sollte nun ausgeführt werden.

6a4d5e5603ba4b73.png

9. Bereinigen (optional)

Wenn Sie die App nicht behalten möchten, können Sie Ressourcen bereinigen, um Kosten zu sparen und nicht mehr benötigte Ressourcen für andere freizugeben. Löschen Sie dazu das gesamte Projekt:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Glückwunsch!

Glückwunsch! Diese in App Engine gehostete Node.js-Webanwendung verbindet alle Ihre Dienste und ermöglicht es Ihren Nutzern, Bilder hochzuladen und zu visualisieren.

Behandelte Themen

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Nächste Schritte