Pic-a-daily: laboratório 4: criar um front-end da Web

1. Visão geral

Neste codelab, você criará um front-end da Web no Google App Engine para permitir que os usuários façam upload de fotos do aplicativo da Web, além de navegar pelas imagens enviadas e as miniaturas delas.

21741cd63b425aeb.png

Esse aplicativo da Web usará um framework CSS chamado Bulma, por oferecer uma boa interface do usuário, e também o framework de front-end JavaScript Vue.JS para chamar a API do aplicativo que você criará.

Esse aplicativo consistirá em três guias:

  • Uma página inicial com as miniaturas de todas as imagens enviadas com a lista de rótulos que as descrevem (aqueles detectados pela API Cloud Vision em um laboratório anterior).
  • Uma página de colagem que mostra a colagem feita das quatro fotos mais recentes enviadas.
  • Uma página de upload, em que os usuários podem fazer upload de novas fotos.

O front-end resultante será assim:

6a4d5e5603ba4b73.png

Essas três páginas são HTML simples:

  • A página home (index.html) chama o código de back-end do Node App Engine para receber a lista de imagens em miniatura e os rótulos delas por uma chamada AJAX para o URL /api/pictures. A página inicial está usando o Vue.js para buscar esses dados.
  • A página colagem (collage.html) aponta para a imagem collage.png que monta as quatro imagens mais recentes.
  • A página de upload (upload.html) oferece um formulário simples para fazer upload de uma imagem por uma solicitação POST para o URL /api/pictures.

O que você vai aprender

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. Configuração e requisitos

Configuração de ambiente autoguiada

  1. Faça login no Console do Google Cloud e crie um novo projeto ou reutilize um existente. Crie uma conta do Gmail ou do Google Workspace, se ainda não tiver uma.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • O Nome do projeto é o nome de exibição para os participantes do projeto. Ele é uma string de caracteres que não é usada pelas APIs do Google e pode ser atualizada a qualquer momento.
  • O ID do projeto precisa ser exclusivo em todos os projetos do Google Cloud e não pode ser alterado após a definição. O Console do Cloud gera automaticamente uma string única, geralmente não importa o que seja. Na maioria dos codelabs, você precisará fazer referência ao ID do projeto, que geralmente é identificado como PROJECT_ID. Então, se você não gostar dele, gere outro ID aleatório ou crie um próprio e veja se ele está disponível. Em seguida, ele fica "congelado" depois que o projeto é criado.
  • Há um terceiro valor, um Número de projeto, que algumas APIs usam. Saiba mais sobre esses três valores na documentação.
  1. Em seguida, você precisará ativar o faturamento no Console do Cloud para usar os recursos/APIs do Cloud. A execução deste codelab não será muito cara, se tiver algum custo. Para encerrar os recursos e não gerar cobranças além deste tutorial, siga as instruções de "limpeza" encontradas no final do codelab. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Inicie o Cloud Shell

Embora o Google Cloud e o Spanner possam ser operados remotamente do seu laptop, neste codelab usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Cloud.

No Console do Google Cloud, clique no ícone do Cloud Shell na barra de ferramentas superior à direita:

55efc1aaa7a4d3ad.png

O provisionamento e a conexão com o ambiente levarão apenas alguns instantes para serem concluídos: Quando o processamento for concluído, você verá algo como:

7ffe5cbb04455448.png

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Todo o trabalho neste laboratório pode ser feito apenas com um navegador.

3. Ativar APIs

O App Engine requer a API Compute Engine. Verifique se ele está ativado:

gcloud services enable compute.googleapis.com

A operação será concluída com êxito:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. Clonar o código

Confira o código, caso ainda não tenha feito isso:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

Acesse o diretório que contém o front-end:

cd serverless-photosharing-workshop/frontend

Você terá o seguinte layout de arquivo para o front-end:

frontend
 |
 ├── index.js
 ├── package.json
 ├── app.yaml
 |
 ├── public
      |
      ├── index.html
      ├── collage.html
      ├── upload.html
      |
      ├── app.js
      ├── script.js
      ├── style.css

Na raiz do nosso projeto, você tem três arquivos:

  • index.js contém o código Node.js.
  • package.json define as dependências da biblioteca.
  • app.yaml é o arquivo de configuração do Google App Engine.

Uma pasta public contém os recursos estáticos:

  • index.html é a página que mostra todas as imagens de miniaturas e rótulos
  • collage.html mostra a colagem das fotos recentes
  • upload.html contém um formulário para carregar novas fotos
  • O app.js está usando o Vue.js para preencher a página do index.html com os dados.
  • O script.js processa o menu de navegação e o "hambúrguer" dele. ícone em telas pequenas
  • style.css define algumas diretivas CSS

5. Explorar o código

Dependências

O arquivo package.json define as dependências de biblioteca necessárias:

{
  "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"
  }
}

Nosso aplicativo depende de:

  • firestore: para acessar o Cloud Firestore com nossos metadados de imagens,
  • storage: para acessar o Google Cloud Storage onde as imagens estão armazenadas,
  • express: o framework da Web para Node.js,
  • dayjs: uma pequena biblioteca para mostrar datas de uma forma legível para humanos,
  • bluebird: uma biblioteca de promessas de JavaScript,
  • express-fileupload: uma biblioteca para processar uploads de arquivos facilmente.

Front-end do Express

No início do controlador index.js, você vai precisar de todas as dependências definidas em 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 instância do aplicativo Express é criada.

São usados dois middlewares do Express:

  • A chamada express.static() indica que os recursos estáticos estarão disponíveis no subdiretório public.
  • E o fileUpload() configura o upload de arquivos para limitar o tamanho a 10 MB e fazer upload dos arquivos localmente no sistema de arquivos na memória do diretório /tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

Entre os recursos estáticos, estão os arquivos HTML da página inicial, da página de colagem e da página de upload. Essas páginas chamarão o back-end da API. Essa API terá os seguintes endpoints:

  • POST /api/pictures Através do formulário em upload.html, as imagens serão carregadas por meio de uma solicitação POST
  • GET /api/pictures Esse endpoint retorna um documento JSON contendo a lista de imagens e os rótulos delas
  • GET /api/pictures/:name Esse URL redireciona para o local de armazenamento em nuvem da imagem em tamanho original
  • GET /api/thumbnails/:name Esse URL redireciona para o local de armazenamento em nuvem da imagem em miniatura
  • GET /api/collage O último URL redireciona para o local de armazenamento em nuvem da colagem gerada

Upload de imagem

Antes de explorar o código Node.js de upload de imagens, dê uma olhada rápida em public/upload.html.

... 
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
    ... 
    <input type="file" name="pictures">
    <button>Submit</button>
    ... 
</form>
... 

O elemento de formulário aponta para o endpoint /api/pictures, com um método POST HTTP e um formato de várias partes. Agora, o index.js precisa responder a esse endpoint e método e extrair os arquivos:

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('/');
});

Primeiro, verifique se realmente há arquivos sendo enviados. Em seguida, faça o download dos arquivos localmente usando o método mv do módulo Node de upload de arquivos. Agora que os arquivos estão disponíveis no sistema de arquivos local, faça upload das imagens para o bucket do Cloud Storage. Por fim, você redireciona o usuário de volta para a tela principal do aplicativo.

Listar as imagens

É hora de mostrar suas belas fotos!

No gerenciador /api/pictures, analise a coleção pictures do banco de dados do Firestore para recuperar todas as imagens com a miniatura gerada, ordenadas por data de criação decrescente.

Você envia cada imagem em uma matriz JavaScript, com o nome dela, os rótulos que a descrevem (da API Cloud Vision), a cor dominante e uma data de criação simples (com dayjs, fazemos ajustes de horário como "3 days from now").

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);
});

Esse controlador retorna resultados no seguinte formato:

[
   {
      "name": "IMG_20180423_163745.jpg",
      "labels": [
         "Dish",
         "Food",
         "Cuisine",
         "Ingredient",
         "Orange chicken",
         "Produce",
         "Meat",
         "Staple food"
      ],
      "color": "#e78012",
      "created": "a day ago"
   },
   ...
]

Essa estrutura de dados é consumida por um pequeno snippet do Vue.js da página index.html. Veja uma versão simplificada da marcação dessa 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>

O ID do div indicará que o Vue.js é a parte da marcação que será renderizada dinamicamente. As iterações são feitas graças às diretivas v-for.

As imagens recebem uma bonita borda colorida correspondente à cor dominante na imagem, conforme encontrado pela API Cloud Vision. Nós apontamos as miniaturas e as imagens de largura total no link e nas fontes de imagem.

Por fim, listamos os rótulos que descrevem a imagem.

Este é o código JavaScript para o snippet Vue.js (no arquivo public/app.js importado na parte inferior da página index.html):

var app = new Vue({
  el: '#app',
  data() {
    return { pictures: [] }
  },
  mounted() {
    axios
      .get('/api/pictures')
      .then(response => { this.pictures = response.data })
  }
})

O código do Vue usa a biblioteca Axios para fazer uma chamada AJAX para o endpoint /api/pictures. Em seguida, os dados retornados são vinculados ao código de visualização na marcação que vimos anteriormente.

Visualização das imagens

Do index.html, nossos usuários podem ver as miniaturas das imagens, clicar nelas para ver as imagens em tamanho real e, para collage.html, os usuários visualizam a imagem collage.png.

Na marcação HTML dessas páginas, a imagem src e o link href apontam para esses três endpoints, que redirecionam para os locais do Cloud Storage das imagens, miniaturas e colagem. Não é necessário codificar o caminho na marcação 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`);
});

Como executar o aplicativo Node

Com todos os endpoints definidos, seu aplicativo Node.js está pronto para ser iniciado. Por padrão, o aplicativo Express detecta atividade na porta 8080 e está pronto para atender às solicitações recebidas.

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. Testar localmente

Teste o código localmente para garantir que ele funciona antes de implantá-lo na nuvem.

Você precisa exportar as duas variáveis de ambiente correspondentes aos dois buckets do Cloud Storage:

export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Dentro da pasta frontend, instale as dependências de npm e inicie o servidor:

npm install; npm start

Se tudo tiver dado certo, ele deverá iniciar o servidor na porta 8080:

Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}

Os nomes reais dos buckets aparecem nesses registros, o que é útil para fins de depuração.

No Cloud Shell, é possível usar o recurso de visualização da Web para navegar pelo aplicativo em execução localmente:

82fa3266d48c0d0a.png

Use CTRL-C para sair.

7. Implantar no App Engine

Seu aplicativo está pronto para ser implantado.

Configurar o App Engine

Examine o arquivo de configuração app.yaml para o App Engine:

runtime: nodejs16
env_variables:
  BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT
  BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT

A primeira linha declara que o ambiente de execução é baseado no Node.js 10. Duas variáveis de ambiente estão definidas para apontar para os dois buckets: para as imagens originais e para as miniaturas.

Para substituir GOOGLE_CLOUD_PROJECT pelo ID do projeto real, execute o seguinte comando:

sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml

Implantar

Defina a região de sua preferência para o App Engine e use a mesma região nos laboratórios anteriores:

gcloud config set compute/region europe-west1

E implante:

gcloud app deploy

Após um ou dois minutos, você verá que o aplicativo está veiculando tráfego:

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

Você também pode acessar a seção "App Engine" do console do Cloud para ver se o app foi implantado e conhecer os recursos do App Engine, como controle de versões e divisão de tráfego:

db0e196b00fceab1.png

8. Testar o app

Para testar, acesse o URL padrão do App Engine para o aplicativo (https://<YOUR_PROJECT_ID>.appspot.com/) e você verá a interface de front-end funcionando.

6a4d5e5603ba4b73.png

9. Limpeza (opcional)

Se você não pretende manter o app, limpe os recursos para economizar custos e ser um bom cidadão da nuvem excluindo todo o projeto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Parabéns!

Parabéns! Esse aplicativo da Web Node.js hospedado no App Engine une todos os seus serviços e permite que seus usuários façam upload e visualizem imagens.

O que vimos

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Próximas etapas