Pic-à-quoi : Atelier 4 : Créer une interface Web

1. Présentation

Dans cet atelier de programmation, vous allez créer une interface Web sur Google App Engine, qui permettra aux utilisateurs d'importer des images à partir de l'application Web et de parcourir les images importées et leurs vignettes.

21741cd63b425aeb.png

Cette application Web utilisera un framework CSS appelé Bulma pour disposer d'une bonne interface utilisateur, ainsi que le framework d'interface JavaScript Vue.JS pour appeler l'API de l'application que vous allez créer.

Cette application comporte trois onglets:

  • Une page d'accueil qui affiche les vignettes de toutes les images importées, ainsi que la liste des étiquettes décrivant l'image (celles détectées par l'API Cloud Vision lors d'un atelier précédent).
  • Une page de montage présentant le montage des quatre dernières photos importées.
  • Une page d'importation, sur laquelle les utilisateurs peuvent importer de nouvelles photos

L'interface obtenue se présente comme suit:

6a4d5e5603ba4b73.png

Ces trois pages sont de simples pages HTML:

  • La page home (index.html) appelle le code backend du nœud App Engine pour obtenir la liste des vignettes et de leurs étiquettes, via un appel AJAX à l'URL /api/pictures. La page d'accueil utilise Vue.js pour récupérer ces données.
  • La page de montage (collage.html) pointe vers l'image collage.png, qui assemble les quatre dernières photos.
  • La page d'importation (upload.html) propose un formulaire simple pour importer une photo via une requête POST à l'URL /api/pictures.

Points abordés

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. Préparation

Configuration de l'environnement au rythme de chacun

  1. Connectez-vous à la console Google Cloud, puis créez un projet ou réutilisez un projet existant. (Si vous ne possédez pas encore de compte Gmail ou Google Workspace, vous devez en créer un.)

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Le nom du projet est le nom à afficher pour les participants au projet. Il s'agit d'une chaîne de caractères qui n'est pas utilisée par les API Google, et que vous pouvez modifier à tout moment.
  • L'ID du projet doit être unique sur l'ensemble des projets Google Cloud et doit être immuable (vous ne pouvez pas le modifier une fois que vous l'avez défini). Cloud Console génère automatiquement une chaîne unique dont la composition importe peu, en général. Dans la plupart des ateliers de programmation, vous devrez référencer l'ID du projet (généralement identifié comme PROJECT_ID), donc s'il ne vous convient pas, générez-en un autre au hasard ou définissez le vôtre, puis vérifiez s'il est disponible. Il est ensuite "gelé" une fois le projet créé.
  • La troisième valeur est le numéro de projet, utilisé par certaines API. Pour en savoir plus sur ces trois valeurs, consultez la documentation.
  1. Vous devez ensuite activer la facturation dans Cloud Console afin d'utiliser les ressources/API Cloud. L'exécution de cet atelier de programmation est très peu coûteuse, voire sans frais. Pour arrêter les ressources afin d'éviter qu'elles ne vous soient facturées après ce tutoriel, suivez les instructions de nettoyage indiquées à la fin de l'atelier. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai gratuit pour bénéficier d'un crédit de 300 $.

Démarrer Cloud Shell

Bien que Google Cloud puisse être utilisé à distance depuis votre ordinateur portable, nous allons nous servir de Google Cloud Shell pour cet atelier de programmation, un environnement de ligne de commande exécuté dans le cloud.

Dans la console Google Cloud, cliquez sur l'icône Cloud Shell dans la barre d'outils supérieure :

55efc1aaa7a4d3ad.png

Le provisionnement et la connexion à l'environnement prennent quelques instants seulement. Une fois l'opération terminée, le résultat devrait ressembler à ceci :

7ffe5cbb04455448.png

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser toutes les activités de cet atelier dans un simple navigateur.

3. Activer les API

App Engine nécessite l'API Compute Engine. Assurez-vous qu'elle est activée:

gcloud services enable compute.googleapis.com

L'opération doit s'effectuer correctement:

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

4. Cloner le code

Consultez le code, si ce n'est pas déjà fait:

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

Vous pouvez ensuite accéder au répertoire contenant l'interface:

cd serverless-photosharing-workshop/frontend

L'interface présente la mise en page de fichiers suivante:

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

À la racine de notre projet, vous avez trois fichiers:

  • index.js contient le code Node.js
  • package.json définit les dépendances de la bibliothèque.
  • app.yaml est le fichier de configuration de Google App Engine.

Un dossier public contient les ressources statiques:

  • index.html est la page qui affiche toutes les vignettes et les libellés.
  • collage.html affiche le montage des photos récentes
  • upload.html contient un formulaire permettant d'importer de nouvelles photos
  • app.js utilise Vue.js pour insérer les données dans la page index.html
  • script.js gère le menu de navigation et son "hamburger" icône sur les petits écrans
  • style.css définit certaines directives CSS

5. Explorer le code

Dépendances

Le fichier package.json définit les dépendances de bibliothèque nécessaires:

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

Notre application dépend des éléments suivants:

  • firestore: permet d'accéder à Cloud Firestore avec nos métadonnées d'image.
  • Stockage: pour accéder à Google Cloud Storage où sont stockées les photos,
  • express: framework Web pour Node.js
  • dayjs: il s'agit d'une petite bibliothèque permettant d'afficher facilement les dates.
  • bluebird: bibliothèque de promesses JavaScript
  • express-fileupload: bibliothèque permettant de gérer facilement les importations de fichiers.

Interface Express

Au début du contrôleur index.js, vous aurez besoin de toutes les dépendances définies précédemment dans 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)

Ensuite, l'instance d'application Express est créée.

Deux middlewares Express sont utilisés:

  • L'appel express.static() indique que les ressources statiques seront disponibles dans le sous-répertoire public.
  • fileUpload() configure également l'importation de fichiers pour limiter la taille des fichiers à 10 Mo et les importer localement dans le système de fichiers en mémoire du répertoire /tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

Parmi les ressources statiques figurent les fichiers HTML de la page d'accueil, de la page de montage et de la page d'importation. Ces pages appellent le backend de l'API. Cette API aura les points de terminaison suivants:

  • POST /api/pictures À l'aide du formulaire disponible dans le fichier download.html, les images seront importées via une requête POST.
  • GET /api/pictures Ce point de terminaison renvoie un document JSON contenant la liste des images et leurs étiquettes.
  • GET /api/pictures/:name Cette URL redirige vers l'emplacement de stockage cloud de l'image en taille réelle
  • GET /api/thumbnails/:name Cette URL redirige vers l'emplacement de stockage cloud de la vignette.
  • GET /api/collage Cette dernière URL redirige vers l'emplacement de stockage cloud de l'image de montage générée.

Importation d'une image

Avant d'explorer le code Node.js d'importation d'images, examinez rapidement public/upload.html.

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

L'élément du formulaire pointe vers le point de terminaison /api/pictures, avec une méthode HTTP POST et un format en plusieurs parties. index.js doit maintenant répondre à ce point de terminaison et à cette méthode, puis extraire les fichiers:

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

Tout d'abord, vérifiez que des fichiers sont bien en cours d'importation. Ensuite, vous téléchargez les fichiers localement via la méthode mv provenant de notre module Node.js. Maintenant que les fichiers sont disponibles dans le système de fichiers local, importez les images dans le bucket Cloud Storage. Enfin, vous redirigez l'utilisateur vers l'écran principal de l'application.

Liste des images

Il est temps d'afficher vos plus belles photos !

Dans le gestionnaire /api/pictures, examinez la collection pictures de la base de données Firestore pour récupérer toutes les images (dont la vignette a été générée), classées par date de création décroissante.

Vous transférez chaque image dans un tableau JavaScript, avec son nom, les étiquettes qui la décrivent (provenant de l'API Cloud Vision), la couleur dominante et une date de création conviviale (avec dayjs, nous déterminons les décalages temporels relatifs comme "dans 3 jours").

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

Ce contrôleur renvoie des résultats qui se présentent sous la forme suivante:

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

Cette structure de données est utilisée par un petit extrait Vue.js de la page index.html. Voici une version simplifiée du balisage de cette page:

<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 du tag div indique à Vue.js que c'est la partie du balisage qui sera affichée de manière dynamique. Les itérations sont effectuées grâce aux directives v-for.

Les photos reçoivent une jolie bordure colorée correspondant à la couleur dominante de l'image, telle que trouvée par l'API Cloud Vision. Nous pointons vers les vignettes et les images en pleine largeur dans le lien et les sources des images.

Enfin, nous indiquons les étiquettes décrivant l'image.

Voici le code JavaScript pour l'extrait Vue.js (dans le fichier public/app.js importé au bas de la page index.html):

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

Le code Vue utilise la bibliothèque Axios pour effectuer un appel AJAX vers notre point de terminaison /api/pictures. Les données renvoyées sont ensuite liées au code de la vue dans le balisage que vous avez vu précédemment.

Affichage des photos

À partir du index.html, nos utilisateurs peuvent afficher les vignettes des photos, cliquer dessus pour afficher les images en taille réelle et, depuis collage.html, afficher l'image collage.png.

Dans le balisage HTML de ces pages, l'image src et le lien href pointent vers ces trois points de terminaison, qui redirigent vers les emplacements Cloud Storage des photos, des vignettes et du montage. Il n'est pas nécessaire de coder en dur le chemin dans le balisage 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`);
});

Exécuter l'application Node

Une fois tous les points de terminaison définis, votre application Node.js est prête à être lancée. Par défaut, l'application Express écoute le port 8080 et est prête à traiter les requêtes entrantes.

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. Tester en local

Testez le code localement pour vous assurer qu'il fonctionne avant de le déployer dans le cloud.

Vous devez exporter les deux variables d'environnement correspondant aux deux buckets Cloud Storage:

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

Dans le dossier frontend, installez les dépendances npm et démarrez le serveur:

npm install; npm start

Si tout s'est déroulé comme prévu, le serveur doit démarrer sur le port 8080:

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

Les noms réels de vos buckets apparaîtront dans ces journaux, ce qui est utile pour le débogage.

Dans Cloud Shell, vous pouvez utiliser la fonctionnalité d'aperçu sur le Web pour parcourir l'application qui s'exécute localement:

82fa3266d48c0d0a.png

Appuyez sur CTRL-C pour quitter.

7. Déployer l'application sur App Engine

Votre application est prête à être déployée.

Configurer App Engine

Examinez le fichier de configuration app.yaml pour App Engine:

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

La première ligne déclare que l'environnement d'exécution est basé sur Node.js 10. Deux variables d'environnement sont définies pour pointer vers les deux buckets, pour les images d'origine et pour les vignettes.

Pour remplacer GOOGLE_CLOUD_PROJECT par l'ID de votre projet, exécutez la commande suivante:

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

Déployer

Définissez la région de votre choix pour App Engine. Assurez-vous d'avoir utilisé la même région lors des précédents ateliers:

gcloud config set compute/region europe-west1

Et déployez:

gcloud app deploy

Au bout d'une ou deux minutes, un message vous indique que l'application diffuse du trafic:

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

Vous pouvez également consulter la section "App Engine" de Cloud Console pour vérifier que l'application est déployée et explorer les fonctionnalités d'App Engine, telles que la gestion des versions et la répartition du trafic:

db0e196b00fceab1.png

8. Tester l'application

Pour effectuer un test, accédez à l'URL App Engine par défaut de l'application (https://<YOUR_PROJECT_ID>.appspot.com/). L'interface utilisateur doit s'exécuter.

6a4d5e5603ba4b73.png

9. Nettoyer (facultatif)

Si vous n'avez pas l'intention de conserver l'application, vous pouvez nettoyer les ressources afin de réduire les coûts et d'utiliser le cloud de manière générale en supprimant l'intégralité du projet:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Félicitations !

Félicitations ! Cette application Web Node.js hébergée sur App Engine relie tous vos services, et permet à vos utilisateurs d'importer et de visualiser des photos.

Points abordés

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Étapes suivantes