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.
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:
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'imagecollage.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
- 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 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 :
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'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.jspackage.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écentesupload.html
contient un formulaire permettant d'importer de nouvelles photosapp.js
utilise Vue.js pour insérer les données dans la pageindex.html
script.js
gère le menu de navigation et son "hamburger" icône sur les petits écransstyle.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épertoirepublic
. 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éelleGET /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>
</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:
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:
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.
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