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 et de parcourir les images importées ainsi que leurs vignettes.

Cette application Web utilisera un framework CSS appelé Bulma pour une interface utilisateur esthétique, ainsi que le framework frontend 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 affichera les miniatures de toutes les images importées, ainsi que la liste des libellés décrivant l'image (ceux détectés par l'API Cloud Vision dans un atelier précédent).
- Une page montage qui affichera le montage des quatre dernières photos importées.
- Une page d'importation, où les utilisateurs peuvent importer de nouvelles photos.
L'interface utilisateur obtenue se présente comme suit :

Ces trois pages sont de simples pages HTML :
- La page accueil (
index.html) appelle le code de backend de l'application Node App Engine pour obtenir la liste des miniatures et de leurs libellés, via un appel AJAX à l'URL/api/pictures. La page d'accueil utilise Vue.js pour extraire ces données. - La page collage (
collage.html) pointe vers l'imagecollage.pngqui assemble les quatre dernières photos. - La page upload (
upload.html) propose un formulaire simple permettant d'importer une image 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
- 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.)



- 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.
- 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 sans frais 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 :

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

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'il est activé :
gcloud services enable compute.googleapis.com
L'opération devrait se terminer correctement :
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Cloner le code
Extrayez 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
La mise en page des fichiers pour le frontend sera la 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 disposez de trois fichiers :
index.jscontient le code Node.js.package.jsondéfinit les dépendances de la bibliothèque.app.yamlest le fichier de configuration pour Google App Engine.
Un dossier public contient les ressources statiques :
index.htmlest la page qui affiche toutes les miniatures et les libellés.collage.htmlaffiche le montage des photos récentes.upload.htmlcontient un formulaire permettant d'importer de nouvelles photos.app.jsutilise Vue.js pour remplir la pageindex.htmlavec les données.script.jsgère le menu de navigation et son icône "hamburger" sur les petits écrans.style.cssdé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 : pour accéder à Cloud Firestore avec nos métadonnées d'images.
- storage : pour accéder à Google Cloud Storage où les photos sont stockées.
- express : framework Web pour Node.js,
- dayjs : petite bibliothèque permettant d'afficher les dates de manière conviviale.
- 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épertoirepublic. fileUpload()configure l'importation de fichiers pour limiter leur taille à 10 Mo et les importer localement dans le système de fichiers en mémoire, dans le 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, vous trouverez les fichiers HTML pour la page d'accueil, la page de collage et la page d'importation. Ces pages appelleront le backend de l'API. Cette API aura les points de terminaison suivants :
POST /api/picturesLes photos seront importées via une requête POST à l'aide du formulaire upload.html.GET /api/pictures: ce point de terminaison renvoie un document JSON contenant la liste des images et de leurs libellés.GET /api/pictures/:name: cette URL redirige vers l'emplacement de stockage cloud de l'image en taille réelle.GET /api/thumbnails/:nameCette URL redirige vers l'emplacement Cloud Storage de l'image miniature.GET /api/collageCette dernière URL redirige vers l'emplacement Cloud Storage de l'image de montage générée.
Importation de photos
Avant d'explorer le code Node.js pour l'importation d'images, jetez un coup d'œil à 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 de formulaire pointe vers le point de terminaison /api/pictures, avec une méthode HTTP POST et un format multipartite. index.js doit maintenant répondre à ce point de terminaison et à cette méthode, et 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. Vous téléchargez ensuite les fichiers localement via la méthode mv provenant de notre module Node d'importation de fichiers. Maintenant que les fichiers sont disponibles sur le système de fichiers local, importez les photos dans le bucket Cloud Storage. Enfin, vous redirigez l'utilisateur vers l'écran principal de l'application.
Lister les photos
Il est temps d'afficher vos magnifiques photos !
Dans le gestionnaire /api/pictures, vous examinez la collection pictures de la base de données Firestore pour récupérer toutes les photos (dont la miniature a été générée), triées par ordre décroissant de date de création.
Vous insérez chaque image dans un tableau JavaScript, avec son nom, les libellés la décrivant (provenant de l'API Cloud Vision), la couleur dominante et une date de création conviviale (avec dayjs, nous obtenons des décalages temporels relatifs tels que "dans trois 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 de 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>
</span>
{{ label }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
L'ID du div indiquera à Vue.js qu'il s'agit de 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 images sont entourées d'une bordure colorée correspondant à la couleur dominante de l'image, telle qu'identifiée par l'API Cloud Vision. Nous pointons sur les miniatures et les images en pleine largeur dans les sources de liens et d'images.
Enfin, nous listons les libellés décrivant l'image.
Voici le code JavaScript pour l'extrait Vue.js (dans le fichier public/app.js importé en 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 à 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.
Afficher les photos
À partir de index.html, nos utilisateurs peuvent afficher les miniatures des images, cliquer dessus pour afficher les images en taille réelle et, à partir de collage.html, les utilisateurs peuvent 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 miniatures 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
Maintenant que tous les points de terminaison sont définis, votre application Node.js est prête à être lancée. L'application Express écoute sur le port 8080 par défaut 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 en local 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 bien passé, le serveur devrait 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 exécutée localement :

Utilisez 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 indique 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, l'un pour les images d'origine et l'autre pour les miniatures.
Pour remplacer GOOGLE_CLOUD_PROJECT par l'ID de votre projet, vous pouvez exécuter la commande suivante :
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
Déployer
Définissez votre région préférée pour App Engine. Veillez à utiliser la même région que dans les ateliers précédents :
gcloud config set compute/region europe-west1
Déployez :
gcloud app deploy
Au bout d'une ou deux minutes, vous serez informé 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 accéder à la section App Engine de la console Cloud pour vérifier que l'application est déployée et explorer les fonctionnalités d'App Engine, comme le versionnage et la répartition du trafic :

8. Tester l'application
Pour tester l'application, accédez à l'URL App Engine par défaut (https://<YOUR_PROJECT_ID>.appspot.com/). L'interface utilisateur du frontend devrait être opérationnelle.

9. Nettoyer (facultatif)
Si vous n'avez pas l'intention de conserver l'application, vous pouvez nettoyer les ressources pour réduire les coûts et utiliser le cloud de manière raisonnée 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 regroupe tous vos services et permet à vos utilisateurs d'importer et de visualiser des images.
Points abordés
- App Engine
- Cloud Storage
- Cloud Firestore