Pic-a-daily: Lab 4: Crea un frontend web

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.

21741cd63b425aeb.png

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:

6a4d5e5603ba4b73.png

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 imagen collage.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

  1. 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.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 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.
  1. 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:

55efc1aaa7a4d3ad.png

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:

7ffe5cbb04455448.png

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 etiquetas
  • collage.html muestra el collage de las fotos recientes
  • upload.html contiene un formulario para subir fotos nuevas.
  • app.js usa Vue.js para propagar los datos en la página index.html
  • script.js controla el menú de navegación y su "hamburguesa" ícono en pantallas pequeñas
  • style.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 subdirectorio public.
  • 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 POST
  • GET /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 completo
  • GET /api/thumbnails/:name Esta URL redirecciona a la ubicación de almacenamiento en la nube de la imagen en miniatura
  • GET /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> &nbsp;
                                                        </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:

82fa3266d48c0d0a.png

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:

db0e196b00fceab1.png

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.

6a4d5e5603ba4b73.png

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

Próximos pasos