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

1. Visão geral

Neste codelab, você vai criar um front-end da Web no Google App Engine que permite aos usuários fazer upload de imagens do aplicativo da Web e navegar pelas imagens e miniaturas enviadas por upload.

21741cd63b425aeb.png

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

O aplicativo vai consistir em três guias:

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

O front-end resultante vai ficar assim:

6a4d5e5603ba4b73.png

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

  • A página inicial (index.html) chama o código do back-end do Node App Engine para receber a lista de miniaturas e seus rótulos, usando uma chamada AJAX para o URL /api/pictures. A página inicial usa Vue.js para buscar esses dados.
  • A página colagem (collage.html) aponta para a imagem collage.png que reúne as quatro fotos mais recentes.
  • A página upload (upload.html) oferece um formulário simples para fazer upload de uma imagem via 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 sucesso:

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

4. Clonar o código

Confira o código, se ainda não tiver feito isso:

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

Em seguida, 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 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 em miniatura e os rótulos.
  • collage.html mostra a colagem das fotos recentes
  • upload.html contém um formulário para fazer upload de novas fotos
  • app.js está usando o Vue.js para preencher a página index.html com os dados.
  • script.js processa o menu de navegação e o ícone de "hambúrguer" em telas pequenas.
  • style.css define algumas diretivas de 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 imagem.
  • storage: para acessar o Google Cloud Storage, onde as fotos são armazenadas.
  • express: o framework da Web para Node.js;
  • dayjs: uma pequena biblioteca para mostrar datas de maneira fácil de entender.
  • bluebird: uma biblioteca de promessas JavaScript.
  • express-fileupload: uma biblioteca para processar uploads de arquivos com facilidade.

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)

Em seguida, a instância do aplicativo Express é criada.

Dois middlewares do Express são usados:

  • A chamada express.static() indica que os recursos estáticos estarão disponíveis no subdiretório public.
  • O fileUpload() configura o upload de arquivos para limitar o tamanho a 10 MB e fazer o upload dos arquivos localmente no sistema de arquivos na memória no 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, você tem os arquivos HTML da página inicial, da página de colagem e da página de upload. Essas páginas vão chamar o back-end da API. Essa API terá os seguintes endpoints:

  • POST /api/pictures Pelo formulário em upload.html, as imagens serão enviadas por uma solicitação POST.
  • GET /api/pictures: esse endpoint retorna um documento JSON com 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 Esse último URL redireciona para o local do Cloud Storage da imagem de colagem gerada.

Upload de imagem

Antes de analisar o código Node.js de upload de imagens, confira rapidamente 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 HTTP POST e um formato de várias partes. O index.js agora 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 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 fotos para o bucket do Cloud Storage. Por fim, redirecione o usuário de volta à tela principal do aplicativo.

Listar as imagens

É hora de mostrar suas lindas fotos!

No manipulador /api/pictures, você consulta a coleção pictures do banco de dados do Firestore para recuperar todas as imagens (cuja miniatura foi gerada), ordenadas por data de criação em ordem 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 amigável (com dayjs, usamos compensações de tempo relativas, como "3 dias a partir de agora").

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 na página index.html. Confira 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 da div indica ao Vue.js que ela é 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 borda colorida correspondente à cor dominante na imagem, conforme encontrado pela API Cloud Vision. Além disso, apontamos para as miniaturas e as imagens de largura total nas fontes de link e imagem.

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

Confira o código JavaScript do snippet do Vue.js (no arquivo public/app.js importado na parte de baixo 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 Vue usa a biblioteca Axios para fazer uma chamada AJAX ao nosso endpoint /api/pictures. Os dados retornados são vinculados ao código de visualização na marcação que você viu antes.

Como ver as fotos

Em index.html, os usuários podem ver as miniaturas das fotos e clicar nelas para conferir as imagens em tamanho real. Em collage.html, os usuários veem 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 fotos, miniaturas e colagem. Não é preciso 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 lançado. 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 funcione antes de implantar 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}

Na pasta frontend, instale as dependências do npm e inicie o servidor:

npm install; npm start

Se tudo der certo, o servidor será iniciado 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 seus buckets vão aparecer nesses registros, o que é útil para fins de depuração.

No Cloud Shell, use 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 do 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 são definidas para apontar os dois buckets, um para as imagens originais e outro 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 sua região preferida para o App Engine e use a mesma região dos laboratórios anteriores:

gcloud config set compute/region europe-west1

e implante:

gcloud app deploy

Depois de um ou dois minutos, você vai receber uma mensagem informando 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 do App Engine no console do Cloud para ver se o app foi implantado e conhecer 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 app https://<YOUR_PROJECT_ID>.appspot.com/ e confira se a interface do usuário de front-end está 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 do Node.js hospedado no App Engine vincula todos os seus serviços e permite que os usuários façam upload e visualizem imagens.

O que vimos

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Próximas etapas