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, además del 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, en la que se mostrarán las miniaturas de todas las imágenes subidas, junto con la lista de etiquetas que describen la foto (las que detectó la API de Cloud Vision en un lab anterior).
- Una página de collage que mostrará el collage de 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 Node App Engine para obtener la lista de imágenes en miniatura y sus etiquetas a través de una llamada AJAX a la URL/api/pictures
. La página principal usa Vue.js para recuperar estos datos. - La página del collage (
collage.html
) apunta a la imagencollage.png
que reúne las 4 últimas fotos. - La página de carga (
upload.html
) ofrece un formulario simple para subir una foto 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 completó correctamente:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Clona el código
Consulta el código si aún no lo has hecho:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
Luego, puedes ir al directorio que contiene el frontend:
cd serverless-photosharing-workshop/frontend
Tendrá el siguiente diseño de archivo 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.js
contiene el código de Node.js.package.json
define las dependencias de la biblioteca.app.yaml
es el archivo de configuración de Google App Engine.
Una carpeta public
contiene los recursos estáticos:
index.html
es la página que muestra todas las imágenes en miniatura y etiquetascollage.html
muestra el collage de las fotos recientesupload.html
contiene un formulario para subir fotos nuevas.app.js
usa Vue.js para propagar los datos en la páginaindex.html
script.js
controla el menú de navegación y su "hamburguesa" ícono en pantallas pequeñasstyle.css
define 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 nuestros metadatos de fotos, sigue estos pasos:
- storage: Acceder a Google Cloud Storage, donde se almacenan las fotos,
- Express: el framework web para Node.js.
- dayjs: Es una biblioteca pequeña para mostrar fechas de manera sencilla.
- bluebird: una biblioteca de promesas de JavaScript,
- Express-fileupload: Es una biblioteca para administrar fácilmente las cargas de archivos.
Frontend de Express
Al comienzo del controlador index.js
, necesitarás todas las dependencias que se definieron 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 middleware de Express:
- La llamada
express.static()
indica que los recursos estáticos estarán disponibles en el subdirectoriopublic
. - Y
fileUpload()
configura la carga de archivos para limitar el tamaño a 10 MB para subir los archivos de forma local en el sistema de archivos en la memoria, en el 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/pictures
Mediante el formulario en upload.html, las fotos se subirán a través de una solicitud POSTGET /api/pictures
Este extremo muestra un documento JSON que contiene la lista de fotos y sus etiquetas.GET /api/pictures/:name
Esta URL redirecciona a la ubicación de almacenamiento en la nube de la imagen de tamaño completoGET /api/thumbnails/:name
Esta URL redirecciona a la ubicación de almacenamiento en la nube de la imagen en miniaturaGET /api/collage
Esta última URL redirecciona a la ubicación de almacenamiento en la nube de la imagen del collage generada
Carga de imágenes
Antes de explorar el código de Node.js para subir fotos, consulta con rapidez 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 HTTP POST 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, comprueba que se estén subiendo algunos archivos. Luego, descarga los archivos localmente a través del método mv
que proviene de nuestro módulo de nodo de carga de archivos. Ahora que los archivos están disponibles en el sistema de archivos local, sube las fotos al bucket de Cloud Storage. Por último, redireccionas al usuario de vuelta a la pantalla principal de la aplicación.
Enumera las fotos
Es hora de mostrar tus hermosas fotos.
En el controlador /api/pictures
, revisa 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 foto 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 (con dayjs
, las compensaciones horarias relativas, como “3 días a partir de ahora”).
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 muestra 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"
},
...
]
Un pequeño fragmento de Vue.js de la página index.html
consume esta estructura de datos. A continuación, se muestra 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 de 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 dominante en la imagen, tal como lo descubrió la API de Cloud Vision, y señalamos las miniaturas y las imágenes de ancho completo en el vínculo y las fuentes de imágenes.
Por último, enumeramos las etiquetas que describen la imagen.
Este es el código JavaScript del fragmento de Vue.js (en el archivo public/app.js
importado al final 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 Vue usa la biblioteca Axios para realizar una llamada AJAX a nuestro extremo /api/pictures
. Los datos mostrados se vinculan al código de vista en el lenguaje de marcado que viste antes.
Ver las fotos
Desde 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, desde 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 fotos, las miniaturas y el collage. No es necesario codificar la ruta 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 iniciarse. La aplicación Express escucha en el puerto 8080 de forma predeterminada y está lista para entregar 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 funciona antes de implementarlo en la nube.
Debes exportar las dos variables de entorno que corresponden a los dos buckets de Cloud Storage:
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT} export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
En 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 cual es útil para fines de 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, para las imágenes originales y para las miniaturas.
Para reemplazar GOOGLE_CLOUD_PROJECT
por tu ID del proyecto real, puedes ejecutar el siguiente comando:
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
Implementación
Configura tu región preferida para App Engine. Asegúrate de usar la misma región que usaste en los labs anteriores:
gcloud config set compute/region europe-west1
Luego, implementarás:
gcloud app deploy
Después de uno o dos minutos, 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 App Engine de la consola de Cloud para ver si la app está implementada y explorar las funciones de App Engine, como el control de versiones y la división del tráfico:
8. Prueba la app
Para realizar la prueba, ve a la URL predeterminada de App Engine correspondiente a la app (https://<YOUR_PROJECT_ID>.appspot.com/
). Deberías ver la IU de frontend en funcionamiento.
9. Limpieza (opcional)
Si no pretendes 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 tus usuarios suban y visualicen imágenes.
Temas abordados
- App Engine
- Cloud Storage
- Cloud Firestore