1. Descripción general
En este codelab, crearás un frontend web en Google App Engine que permitirá a los usuarios subir imágenes desde la aplicación web, así como explorar las imágenes subidas y sus miniaturas.

Esta aplicación web usará un framework de CSS llamado Bulma para tener una interfaz de usuario atractiva y el framework de frontend de JavaScript Vue.JS para llamar a la API de la aplicación que compilarás.
Esta aplicación constará de tres pestañas:
- Una página principal que mostrará las miniaturas de todas las imágenes subidas, junto con la lista de etiquetas que describen la imagen (las que detectó la API de Cloud Vision en un lab anterior).
- Una página de collage que mostrará el collage creado con las 4 fotos más recientes que se subieron.
- Una página de carga, en la que los usuarios pueden subir fotos nuevas
El frontend resultante se ve de la siguiente manera:

Esas 3 páginas son páginas HTML simples:
- La página principal (
index.html) llama al código de backend de la app de Node App Engine para obtener la lista de imágenes en miniatura y sus etiquetas a través de una llamada de AJAX a la URL/api/pictures. La página principal usa Vue.js para recuperar estos datos. - La página de collage (
collage.html) apunta a la imagencollage.pngque ensambla las 4 fotos más recientes. - La página upload (
upload.html) ofrece un formulario simple para subir una imagen a través de una solicitud POST a la URL/api/pictures.
Qué aprenderás
- App Engine
- Cloud Storage
- Cloud Firestore
2. Configuración y requisitos
Configuración del entorno de autoaprendizaje
- Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.



- El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una string de caracteres que no se utiliza en las API de Google y se puede actualizar en cualquier momento.
- El ID del proyecto debe ser único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). Cloud Console genera automáticamente una string única, que, por lo general, no importa cuál sea. En la mayoría de los codelabs, debes hacer referencia al ID del proyecto (suele ser
PROJECT_ID). Por lo tanto, si no te gusta, genera otro aleatorio o prueba con uno propio y comprueba si está disponible. Después de crear el proyecto, este ID se “congela” y no se puede cambiar. - Además, hay un tercer valor, el Número de proyecto, que usan algunas API. Obtén más información sobre estos tres valores en la documentación.
- A continuación, deberás habilitar la facturación en Cloud Console para usar las API o los recursos de Cloud. Ejecutar este codelab no debería costar mucho, tal vez nada. Si quieres cerrar los recursos para no se te facture más allá de este instructivo, sigue las instrucciones de “limpieza” que se encuentran al final del codelab. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de USD 300.
Inicia Cloud Shell
Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usarás Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.
En Google Cloud Console, haz clic en el ícono de Cloud Shell en la barra de herramientas en la parte superior derecha:

El aprovisionamiento y la conexión al entorno deberían tomar solo unos minutos. Cuando termine el proceso, debería ver algo como lo siguiente:

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Puedes realizar todo tu trabajo en este lab usando simplemente un navegador.
3. Habilita las APIs
App Engine requiere la API de Compute Engine. Asegúrate de que esté habilitada:
gcloud services enable compute.googleapis.com
Deberías ver que la operación se completa correctamente:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Clona el código
Si aún no lo hiciste, extrae el código:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
Luego, puedes ir al directorio que contiene el frontend:
cd serverless-photosharing-workshop/frontend
Tendrás el siguiente diseño de archivos para el frontend:
frontend
|
├── index.js
├── package.json
├── app.yaml
|
├── public
|
├── index.html
├── collage.html
├── upload.html
|
├── app.js
├── script.js
├── style.css
En la raíz de nuestro proyecto, tienes 3 archivos:
index.jscontiene el código de Node.jspackage.jsondefine las dependencias de la bibliotecaapp.yamles el archivo de configuración de Google App Engine
Una carpeta public contiene los recursos estáticos:
index.htmles la página que muestra todas las imágenes en miniatura y las etiquetas.collage.htmlmuestra el collage de las fotos recientesupload.htmlcontiene un formulario para subir fotos nuevasapp.jsusa Vue.js para completar la páginaindex.htmlcon los datos.script.jscontrola el menú de navegación y su ícono de "hamburguesa" en pantallas pequeñas.style.cssdefine algunas directivas de CSS
5. Cómo explorar el código
Dependencias
El archivo package.json define las dependencias de biblioteca necesarias:
{
"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"
}
}
Nuestra aplicación depende de lo siguiente:
- firestore: Para acceder a Cloud Firestore con los metadatos de nuestras imágenes
- storage: Para acceder a Google Cloud Storage, donde se almacenan las fotos
- express: Es el framework web para Node.js.
- dayjs: Una pequeña biblioteca para mostrar fechas de una manera fácil de entender.
- bluebird: Una biblioteca de promesas de JavaScript
- express-fileupload: Una biblioteca para controlar las cargas de archivos con facilidad.
Frontend de Express
Al principio del controlador index.js, deberás requerir todas las dependencias definidas en package.json anteriormente:
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)
A continuación, se crea la instancia de la aplicación Express.
Se usan dos middlewares de Express:
- La llamada a
express.static()indica que los recursos estáticos estarán disponibles en el subdirectoriopublic. - Además,
fileUpload()configura la carga de archivos para limitar el tamaño a 10 MB y subir los archivos de forma local en el sistema de archivos en memoria del directorio/tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
limits: { fileSize: 10 * 1024 * 1024 },
useTempFiles : true,
tempFileDir : '/tmp/'
}))
Entre los recursos estáticos, se encuentran los archivos HTML de la página principal, la página de collage y la página de carga. Esas páginas llamarán al backend de la API. Esta API tendrá los siguientes extremos:
POST /api/picturesA través del formulario en upload.html, las imágenes se subirán a través de una solicitud POSTGET /api/picturesEste endpoint devuelve un documento JSON que contiene la lista de imágenes y sus etiquetas.GET /api/pictures/:nameEsta URL redirecciona a la ubicación de Cloud Storage de la imagen de tamaño completo.GET /api/thumbnails/:nameEsta URL redirecciona a la ubicación de almacenamiento en la nube de la imagen en miniatura.GET /api/collageEsta última URL redirecciona a la ubicación de Cloud Storage de la imagen de collage generada.
Carga de imágenes
Antes de explorar el código de Node.js para subir imágenes, echa un vistazo rápido a public/upload.html.
...
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
...
<input type="file" name="pictures">
<button>Submit</button>
...
</form>
...
El elemento del formulario apunta al extremo /api/pictures, con un método POST HTTP y un formato de varias partes. Ahora, index.js debe responder a ese extremo y método, y extraer los archivos:
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('/');
});
Primero, verifica que se estén subiendo archivos. Luego, descargas los archivos de forma local con el método mv que proviene de nuestro módulo de Node para la carga de archivos. Ahora que los archivos están disponibles en el sistema de archivos local, subirás las imágenes al bucket de Cloud Storage. Por último, redireccionas al usuario a la pantalla principal de la aplicación.
Cómo enumerar las imágenes
Es hora de mostrar tus hermosas fotos.
En el controlador /api/pictures, buscas en la colección pictures de la base de datos de Firestore para recuperar todas las imágenes (cuya miniatura se generó), ordenadas por fecha de creación descendente.
Envías cada imagen en un array de JavaScript, con su nombre, las etiquetas que la describen (provenientes de la API de Cloud Vision), el color dominante y una fecha de creación legible (con dayjs, usamos desplazamientos de tiempo relativos, como "en 3 días").
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);
});
Este controlador devuelve resultados con la siguiente forma:
[
{
"name": "IMG_20180423_163745.jpg",
"labels": [
"Dish",
"Food",
"Cuisine",
"Ingredient",
"Orange chicken",
"Produce",
"Meat",
"Staple food"
],
"color": "#e78012",
"created": "a day ago"
},
...
]
Esta estructura de datos se consume con un pequeño fragmento de Vue.js de la página index.html. Esta es una versión simplificada del lenguaje de marcado de esa página:
<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>
El ID del elemento div indicará a Vue.js que es la parte del lenguaje de marcado que se renderizará de forma dinámica. Las iteraciones se realizan gracias a las directivas v-for.
Las imágenes obtienen un borde de color agradable que corresponde al color predominante en la imagen, según lo detecta la API de Cloud Vision, y señalamos las miniaturas y las imágenes de ancho completo en las fuentes de vínculos e imágenes.
Por último, enumeramos las etiquetas que describen la imagen.
Este es el código JavaScript para el fragmento de Vue.js (en el archivo public/app.js importado en la parte inferior de la página index.html):
var app = new Vue({
el: '#app',
data() {
return { pictures: [] }
},
mounted() {
axios
.get('/api/pictures')
.then(response => { this.pictures = response.data })
}
})
El código de Vue usa la biblioteca Axios para realizar una llamada de AJAX a nuestro extremo /api/pictures. Luego, los datos devueltos se vinculan al código de la vista en el lenguaje de marcado que viste antes.
Cómo ver las fotos
En index.html, nuestros usuarios pueden ver las miniaturas de las imágenes, hacer clic en ellas para ver las imágenes en tamaño completo y, en collage.html, los usuarios ven la imagen collage.png.
En el lenguaje de marcado HTML de esas páginas, la imagen src y el vínculo href apuntan a esos 3 extremos, que redireccionan a las ubicaciones de Cloud Storage de las imágenes, las imágenes en miniatura y el collage. No es necesario integrar la ruta como parte del código en el lenguaje de marcado 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`);
});
Ejecuta la aplicación de Node
Con todos los extremos definidos, tu aplicación de Node.js está lista para lanzarse. De forma predeterminada, la aplicación de Express escucha en el puerto 8080 y está lista para atender las solicitudes 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. Realiza pruebas locales
Prueba el código de forma local para asegurarte de que funcione antes de implementarlo en la nube.
Debes exportar las dos variables de entorno correspondientes a los dos buckets de Cloud Storage:
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
Dentro de la carpeta frontend, instala las dependencias de npm y, luego, inicia el servidor:
npm install; npm start
Si todo salió bien, debería iniciar el servidor en el puerto 8080:
Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}
Los nombres reales de tus buckets aparecerán en esos registros, lo que es útil para la depuración.
Desde Cloud Shell, puedes usar la función de vista previa en la Web para explorar la aplicación que se ejecuta de forma local:

Usa CTRL-C para salir.
7. Realiza la implementación en App Engine
Tu aplicación está lista para implementarse.
Configura App Engine
Examina el archivo de configuración app.yaml para App Engine:
runtime: nodejs16 env_variables: BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT
La primera línea declara que el entorno de ejecución se basa en Node.js 10. Se definen dos variables de entorno para apuntar a los dos buckets, uno para las imágenes originales y otro para las miniaturas.
Para reemplazar GOOGLE_CLOUD_PROJECT por el ID de tu proyecto real, puedes ejecutar el siguiente comando:
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
Implementar
Establece tu región preferida para App Engine. Asegúrate de usar la misma región en los labs anteriores:
gcloud config set compute/region europe-west1
Y para implementar, haz lo siguiente:
gcloud app deploy
Después de un minuto o dos, se te indicará que la aplicación está entregando tráfico:
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
También puedes visitar la sección de App Engine en Cloud Console para ver que la app se implementó y explorar funciones de App Engine, como el control de versiones y la división del tráfico:

8. Prueba la app
Para probarla, ve a la URL predeterminada de App Engine para la app (https://<YOUR_PROJECT_ID>.appspot.com/) y deberías ver que la IU de frontend está en funcionamiento.

9. Limpieza (opcional)
Si no tienes la intención de conservar la app, puedes borrar todo el proyecto para limpiar los recursos, ahorrar costos y ser un buen ciudadano de la nube en general:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. ¡Felicitaciones!
¡Felicitaciones! Esta aplicación web de Node.js alojada en App Engine vincula todos tus servicios y permite que los usuarios suban y visualicen imágenes.
Temas abordados
- App Engine
- Cloud Storage
- Cloud Firestore