Como criar aplicativos front-end sem servidor usando o Google Cloud Platform
Publicados: 2022-03-10Recentemente, o paradigma de desenvolvimento de aplicativos começou a mudar de ter que implantar, dimensionar e atualizar manualmente os recursos usados em um aplicativo para depender de provedores de serviços de nuvem terceirizados para fazer a maior parte do gerenciamento desses recursos.
Como um desenvolvedor ou uma organização que deseja criar um aplicativo adequado ao mercado no menor tempo possível, seu foco principal pode ser fornecer o serviço de aplicativo principal aos usuários enquanto você gasta menos tempo na configuração, implantação e testes de estresse seu aplicativo. Se este for o seu caso de uso, manipular a lógica de negócios do seu aplicativo sem servidor pode ser sua melhor opção. Mas como?
Este artigo é útil para engenheiros de front-end que desejam criar determinadas funcionalidades em seus aplicativos ou engenheiros de back-end que desejam extrair e manipular uma determinada funcionalidade de um serviço de back-end existente usando um aplicativo sem servidor implantado no Google Cloud Platform.
Nota : Para se beneficiar do que será abordado aqui, você precisa ter experiência em trabalhar com React. Nenhuma experiência anterior em aplicativos sem servidor é necessária.
Antes de começarmos, vamos entender o que realmente são aplicativos sem servidor e como a arquitetura sem servidor pode ser usada ao construir um aplicativo no contexto de um engenheiro de front-end.
Aplicativos sem servidor
Aplicativos sem servidor são aplicativos divididos em pequenas funções reutilizáveis orientadas a eventos, hospedadas e gerenciadas por provedores de serviços de nuvem de terceiros dentro da nuvem pública em nome do autor do aplicativo. Estes são acionados por determinados eventos e são executados sob demanda. Embora o sufixo “ less ” anexado à palavra serverless indique a ausência de um servidor, isso não é 100% o caso. Esses aplicativos ainda são executados em servidores e outros recursos de hardware, mas, nesse caso, esses recursos não são provisionados pelo desenvolvedor, mas sim por um provedor de serviços de nuvem terceirizado. Portanto, eles não têm servidor para o autor do aplicativo, mas ainda são executados em servidores e podem ser acessados pela Internet pública.
Um exemplo de caso de uso de um aplicativo sem servidor seria enviar e-mails para usuários em potencial que visitam sua página de destino e se inscrevem para receber e-mails de lançamento de produtos. Nesse estágio, você provavelmente não tem um serviço de back-end em execução e não gostaria de sacrificar o tempo e os recursos necessários para criar, implantar e gerenciar um, tudo porque precisa enviar e-mails. Aqui, você pode escrever um único arquivo que usa um cliente de e-mail e implantar em qualquer provedor de nuvem que ofereça suporte a aplicativos sem servidor e permitir que eles gerenciem esse aplicativo em seu nome enquanto você conecta esse aplicativo sem servidor à sua página de destino.
Embora existam vários motivos pelos quais você pode considerar aproveitar os aplicativos sem servidor ou o Functions As A Service (FAAS), como são chamados, para o seu aplicativo de front-end, aqui estão alguns motivos muito notáveis que você deve considerar:
- Escalonamento automático do aplicativo
Aplicativos sem servidor são dimensionados horizontalmente e esse “ escalonamento ” é feito automaticamente pelo provedor de nuvem com base na quantidade de invocações, para que o desenvolvedor não precise adicionar ou remover recursos manualmente quando o aplicativo estiver sob carga pesada. - Custo-benefício
Sendo orientados a eventos, os aplicativos sem servidor são executados apenas quando necessário e isso reflete nas cobranças, pois são cobrados com base no número de tempo invocado. - Flexibilidade
Os aplicativos sem servidor são criados para serem altamente reutilizáveis e isso significa que eles não estão vinculados a um único projeto ou aplicativo. Uma funcionalidade específica pode ser extraída em um aplicativo sem servidor, implantada e usada em vários projetos ou aplicativos. Os aplicativos sem servidor também podem ser escritos no idioma preferido do autor do aplicativo, embora alguns provedores de nuvem ofereçam suporte apenas a uma quantidade menor de idiomas.
Ao fazer uso de aplicativos sem servidor, todo desenvolvedor tem uma vasta gama de provedores de nuvem dentro da nuvem pública para usar. No contexto deste artigo, focaremos em aplicativos sem servidor no Google Cloud Platform — como eles são criados, gerenciados, implantados e como também se integram a outros produtos no Google Cloud. Para fazer isso, adicionaremos novas funcionalidades a este aplicativo React existente enquanto trabalhamos no processo de:
- Armazenar e recuperar dados do usuário na nuvem;
- Criação e gerenciamento de cron jobs no Google Cloud;
- Como implantar o Cloud Functions no Google Cloud.
Nota : Os aplicativos sem servidor não estão vinculados apenas ao React, desde que sua biblioteca ou estrutura de front-end preferida possa fazer uma solicitação HTTP
, ela pode usar um aplicativo sem servidor.
Funções do Google Cloud
O Google Cloud permite que os desenvolvedores criem aplicativos sem servidor usando o Cloud Functions e os executem usando o Functions Framework. Como são chamadas, as funções do Cloud são funções orientadas a eventos reutilizáveis implantadas no Google Cloud para detectar acionadores específicos dos seis acionadores de eventos disponíveis e, em seguida, executar a operação para a qual foi escrita.
As funções de nuvem de curta duração ( com um tempo limite de execução padrão de 60 segundos e no máximo 9 minutos ) podem ser escritas usando JavaScript, Python, Golang e Java e executadas usando seu tempo de execução. Em JavaScript, eles podem ser executados usando apenas algumas versões disponíveis do tempo de execução do Node e são escritos na forma de módulos CommonJS usando JavaScript simples, pois são exportados como a função principal a ser executada no Google Cloud.
Um exemplo de uma função de nuvem é o que está abaixo, que é um clichê vazio para a função manipular os dados de um usuário.
// index.js exports.firestoreFunction = function (req, res) { return res.status(200).send({ data: `Hello ${req.query.name}` }); }
Acima temos um módulo que exporta uma função. Quando executado, ele recebe os argumentos de solicitação e resposta semelhantes a uma rota HTTP
.
Nota : Uma função de nuvem corresponde a cada protocolo HTTP
quando uma solicitação é feita. Isso vale a pena notar ao esperar dados no argumento de solicitação, pois os dados anexados ao fazer uma solicitação para executar uma função de nuvem estariam presentes no corpo da solicitação para solicitações POST
enquanto no corpo da consulta para solicitações GET
.
As funções de nuvem podem ser executadas localmente durante o desenvolvimento instalando o pacote @google-cloud/functions-framework
dentro da mesma pasta onde a função escrita é colocada ou fazendo uma instalação global para usá-la para várias funções executando npm i -g @google-cloud/functions-framework
da sua linha de comando. Uma vez instalado, ele deve ser adicionado ao script package.json
com o nome do módulo exportado semelhante ao abaixo:
"scripts": { "start": "functions-framework --target=firestoreFunction --port=8000", }
Acima temos um único comando dentro de nossos scripts no arquivo package.json
que executa o functions-framework e também especifica o firestoreFunction
como a função de destino a ser executada localmente na porta 8000
.
Podemos testar o endpoint dessa função fazendo uma solicitação GET
para a porta 8000
no localhost usando curl. Colar o comando abaixo em um terminal fará isso e retornará uma resposta.
curl https://localhost:8000?name="Smashing Magazine Author"
O comando acima faz uma solicitação com um método GET HTTP
e responde com um código de status 200
e um objeto data contendo o nome adicionado na consulta.
Como implantar uma função do Cloud
Dentre os métodos de implantação disponíveis, uma maneira rápida de implantar uma função de nuvem de uma máquina local é usar o SDK de nuvem após instalá-lo. Executar o comando abaixo no terminal depois de autenticar o gcloud sdk com seu projeto no Google Cloud implantaria uma função criada localmente no serviço Cloud Function.
gcloud functions deploy "demo-function" --runtime nodejs10 --trigger-http --entry-point=demo --timeout=60 --set-env-vars=[name="Developer"] --allow-unauthenticated
Usando os sinalizadores explicados abaixo, o comando acima implanta uma função acionada por HTTP na nuvem do Google com o nome “ demo-function ”.
- NOME
Este é o nome dado a uma função de nuvem ao implantá-la e é obrigatório. -
region
Esta é a região onde a função de nuvem deve ser implantada. Por padrão, ele é implantado emus-central1
. -
trigger-http
Isso seleciona HTTP como o tipo de gatilho da função. -
allow-unauthenticated
Isso permite que a função seja invocada fora do Google Cloud pela Internet usando o endpoint gerado sem verificar se o chamador está autenticado. -
source
Caminho local do terminal para o arquivo que contém a função a ser implantada. -
entry-point
Este é o módulo exportado específico a ser implantado a partir do arquivo onde as funções foram escritas. -
runtime
Este é o tempo de execução da linguagem a ser usado para a função entre esta lista de tempo de execução aceito. -
timeout
Este é o tempo máximo que uma função pode ser executada antes do tempo limite. É 60 segundos por padrão e pode ser definido para um máximo de 9 minutos.
Nota : Fazer uma função permitir solicitações não autenticadas significa que qualquer pessoa com o endpoint da sua função também pode fazer solicitações sem que você a conceda. Para mitigar isso, podemos garantir que o endpoint permaneça privado usando-o por meio de variáveis de ambiente ou solicitando cabeçalhos de autorização em cada solicitação.
Agora que nossa função de demonstração foi implantada e temos o endpoint, podemos testar essa função como se estivesse sendo usada em um aplicativo do mundo real usando uma instalação global do autocannon. A execução autocannon -d=5 -c=300 CLOUD_FUNCTION_URL
a partir do terminal aberto geraria 300 solicitações simultâneas para a função de nuvem em um período de 5 segundos. Isso mais do que suficiente para iniciar a função de nuvem e também gerar algumas métricas que podemos explorar no painel da função.
Nota : O endpoint de uma função será impresso no terminal após a implantação. Se não for o caso, execute gcloud function describe FUNCTION_NAME
no terminal para obter os detalhes sobre a função implantada, incluindo o endpoint.
Usando a guia de métricas no painel, podemos ver uma representação visual da última solicitação que consiste em quantas invocações foram feitas, quanto tempo duraram, o consumo de memória da função e quantas instâncias foram giradas para lidar com as solicitações feitas.
Uma análise mais detalhada do gráfico de instâncias ativas na imagem acima mostra a capacidade de escalonamento horizontal do Cloud Functions, pois podemos ver que 209 instâncias foram geradas em alguns segundos para lidar com as solicitações feitas usando o autocannon.
Registros de funções do Cloud
Cada função implantada na nuvem do Google possui um log e cada vez que essa função é executada, uma nova entrada nesse log é feita. Na guia Log no painel da função, podemos ver uma lista de todas as entradas de logs de uma função de nuvem.
Abaixo estão as entradas de log de nossa demo-function
implantada criada como resultado das solicitações que fizemos usando autocannon
.
Cada uma das entradas de log acima mostra exatamente quando uma função foi executada, quanto tempo a execução levou e com qual código de status ela terminou. Se houver algum erro resultante de uma função, os detalhes do erro, incluindo a linha em que ocorreu, serão mostrados nos logs aqui.
O Explorador de registros no Google Cloud pode ser usado para ver detalhes mais abrangentes sobre os registros de uma função de nuvem.
Funções de nuvem com aplicativos front-end
As funções de nuvem são muito úteis e poderosas para engenheiros de front-end. Um engenheiro de front-end sem o conhecimento de gerenciamento de aplicativos de back-end pode extrair uma funcionalidade em uma função de nuvem, implantar no Google Cloud e usar em um aplicativo de front-end fazendo solicitações HTTP
para a função de nuvem por meio de seu endpoint.
Para mostrar como as funções de nuvem podem ser usadas em um aplicativo front-end, adicionaremos mais recursos a este aplicativo React. A aplicação já possui um roteamento básico entre a autenticação e a configuração das home pages. Vamos expandi-lo para usar a API do React Context para gerenciar o estado do nosso aplicativo, pois o uso das funções de nuvem criadas seria feito dentro dos redutores do aplicativo.
Para começar, criamos o contexto do nosso aplicativo usando a API createContext
e também criamos um redutor para lidar com as ações dentro do nosso aplicativo.
// state/index.js import { createContext } from “react”;
export const UserReducer = (ação, estado) => { switch (action.type) { case “CREATE-USER”: break; case “UPLOAD-USER-IMAGE”: break; case “FETCH-DATA” : break case “LOGOUT” : break; default: console.log(
${action.type} is not recognized
) } };export const userState = { user: null, isLoggedIn : false };
export const UserContext = createContext(userState);
Acima, começamos com a criação de uma função UserReducer
que contém uma instrução switch, permitindo que ela execute uma operação baseada no tipo de ação despachada para ela. A instrução switch tem quatro casos e estas são as ações que iremos manipular. Por enquanto eles ainda não fazem nada, mas quando começarmos a integração com nossas funções de nuvem, implementaremos incrementalmente as ações a serem executadas neles.
Também criamos e exportamos o contexto do nosso aplicativo usando a API React createContext e demos a ele um valor padrão do objeto userState
que contém um valor de usuário atualmente que seria atualizado de null para os dados do usuário após a autenticação e também um valor booleano isLoggedIn
para saber se o usuário está logado ou não.
Agora podemos continuar consumindo nosso contexto, mas antes disso, precisamos envolver toda a nossa árvore de aplicativos com o Provider anexado ao UserContext
para que os componentes filhos possam assinar a alteração de valor do nosso contexto.
// index.js import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./app"; import { UserContext, userState } from "./state/"; ReactDOM.render( <React.StrictMode> <UserContext.Provider value={userState}> <App /> </UserContext.Provider> </React.StrictMode>, document.getElementById("root") ); serviceWorker.unregister();
Envolvemos nosso aplicativo enter com o provedor UserContext
no componente raiz e passamos nosso valor padrão userState
criado anteriormente no value prop.
Agora que temos o estado do aplicativo totalmente configurado, podemos passar para a criação do modelo de dados do usuário usando o Google Cloud Firestore por meio de uma função de nuvem.
Manipulação de dados do aplicativo
Os dados de um usuário dentro deste aplicativo consistem em um ID exclusivo, um e-mail, uma senha e o URL de uma imagem. Usando uma função de nuvem, esses dados serão armazenados na nuvem usando o serviço Cloud Firestore que é oferecido no Google Cloud Platform.
O Google Cloud Firestore , um banco de dados NoSQL flexível, foi criado a partir do Firebase Realtime Database com novos recursos aprimorados que permitem consultas mais ricas e rápidas juntamente com suporte a dados offline. Os dados no serviço Firestore são organizados em coleções e documentos semelhantes a outros bancos de dados NoSQL, como o MongoDB.
O Firestore pode ser acessado visualmente por meio do Console do Google Cloud. Para iniciá-lo, abra o painel de navegação esquerdo e role para baixo até a seção Banco de dados e clique em Firestore. Isso mostraria a lista de coleções para usuários com dados existentes ou solicitaria que o usuário criasse uma nova coleção quando não houver uma coleção existente. Criaríamos uma coleção de usuários para ser usada pelo nosso aplicativo.
Semelhante a outros serviços no Google Cloud Platform, o Cloud Firestore também possui uma biblioteca cliente JavaScript criada para ser usada em um ambiente de nó ( será gerado um erro se usado no navegador ). Para improvisar, usamos o Cloud Firestore em uma função de nuvem usando o pacote @google-cloud/firestore
.
Como usar o Cloud Firestore com uma função de nuvem
Para começar, renomearemos a primeira função que criamos de demo-function
para firestoreFunction
e a expandiremos para conectar-se ao Firestore e salvar dados na coleção de nossos usuários.
require("dotenv").config(); const { Firestore } = require("@google-cloud/firestore"); const { SecretManagerServiceClient } = require("@google-cloud/secret-manager"); const client = new SecretManagerServiceClient(); exports.firestoreFunction = function (req, res) { return { const { email, password, type } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); console.log(document) // prints details of the collection to the function logs if (!type) { res.status(422).send("An action type was not specified"); } switch (type) { case "CREATE-USER": break case "LOGIN-USER": break; default: res.status(422).send(`${type} is not a valid function action`) } };
Para lidar com mais operações envolvendo o armazenamento de fogo, adicionamos uma instrução switch com dois casos para lidar com as necessidades de autenticação de nosso aplicativo. Nossa instrução switch avalia uma expressão de type
que adicionamos ao corpo da solicitação ao fazer uma solicitação para esta função de nosso aplicativo e sempre que esse type
dado não estiver presente em nosso corpo da solicitação, a solicitação é identificada como Bad Request e um código de status 400
ao lado de uma mensagem para indicar que o type
ausente é enviado como resposta.
Estabelecemos uma conexão com o Firestore usando a biblioteca Application Default Credentials (ADC) na biblioteca de cliente do Cloud Firestore. Na próxima linha, chamamos o método de coleção em outra variável e passamos o nome de nossa coleção. Estaremos usando isso para realizar outras operações na coleta dos documentos contidos.
Observação : as bibliotecas de cliente para serviços no Google Cloud se conectam ao respectivo serviço usando uma chave de conta de serviço criada transmitida ao inicializar o construtor. Quando a chave da conta de serviço não está presente, o padrão é usar as credenciais padrão do aplicativo que, por sua vez, se conectam usando as funções do IAM
atribuídas à função de nuvem.
Depois de editar o código-fonte de uma função que foi implantada localmente usando o Gcloud SDK, podemos executar novamente o comando anterior de um terminal para atualizar e reimplantar a função de nuvem.
Agora que uma conexão foi estabelecida, podemos implementar o caso CREATE-USER
para criar um novo usuário usando dados do corpo da solicitação.
require("dotenv").config(); const { Firestore } = require("@google-cloud/firestore"); const path = require("path"); const { v4 : uuid } = require("uuid") const cors = require("cors")({ origin: true }); const client = new SecretManagerServiceClient(); exports.firestoreFunction = function (req, res) { return cors(req, res, () => { const { email, password, type } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); if (!type) { res.status(422).send("An action type was not specified"); } switch (type) { case "CREATE-USER": if (!email || !password) { res.status(422).send("email and password fields missing"); } const id = uuid() return bcrypt.genSalt(10, (err, salt) => { bcrypt.hash(password, salt, (err, hash) => { document.doc(id) .set({ id : id email: email, password: hash, img_uri : null }) .then((response) => res.status(200).send(response)) .catch((e) => res.status(501).send({ error : e }) ); }); }); case "LOGIN": break; default: res.status(400).send(`${type} is not a valid function action`) } }); };
Geramos um UUID usando o pacote uuid para ser usado como o ID do documento prestes a ser salvo, passando-o para o método set
no documento e também o id do usuário. Por padrão, um ID aleatório é gerado em cada documento inserido, mas neste caso, atualizaremos o documento ao lidar com o upload da imagem e o UUID é o que será usado para que um determinado documento seja atualizado. Em vez de armazenar a senha do usuário em texto simples, primeiro usamos bcryptjs e depois armazenamos o hash resultante como a senha do usuário.
Integrando a função de nuvem firestoreFunction
no aplicativo, nós a usamos a partir do caso CREATE_USER
dentro do redutor de usuário.
Após clicar no botão Create Account , uma ação é despachada para os redutores com um tipo CREATE_USER
para fazer uma solicitação POST
contendo o email e a senha digitados para o endpoint da função firestoreFunction
.
import { createContext } from "react"; import { navigate } from "@reach/router"; import Axios from "axios"; export const userState = { user : null, isLoggedIn: false, }; export const UserReducer = (state, action) => { switch (action.type) { case "CREATE_USER": const FIRESTORE_FUNCTION = process.env.REACT_APP_FIRESTORE_FUNCTION; const { userEmail, userPassword } = action; const data = { type: "CREATE-USER", email: userEmail, password: userPassword, }; Axios.post(`${FIRESTORE_FUNCTION}`, data) .then((res) => { navigate("/home"); return { ...state, isLoggedIn: true }; }) .catch((e) => console.log(`couldnt create user. error : ${e}`)); break; case "LOGIN-USER": break; case "UPLOAD-USER-IMAGE": break; case "FETCH-DATA" : break case "LOGOUT": navigate("/login"); return { ...state, isLoggedIn: false }; default: break; } }; export const UserContext = createContext(userState);
Acima, utilizamos o Axios para fazer a requisição para o firestoreFunction
e após esta requisição ser resolvida definimos o estado inicial do usuário de null
para os dados retornados da requisição e por último roteamos o usuário para a página inicial como usuário autenticado .
Neste ponto, um novo usuário pode criar uma conta com sucesso e ser roteado para a página inicial. Esse processo demonstra como usamos o Firestore para realizar uma criação básica de dados de uma função de nuvem.
Manipulando o armazenamento de arquivos
O armazenamento e a recuperação dos arquivos de um usuário em um aplicativo é, na maioria das vezes, um recurso muito necessário em um aplicativo. Em um aplicativo conectado a um back-end node.js, Multer é frequentemente usado como um middleware para lidar com os dados multipart/form-data nos quais um arquivo carregado vem. Mas na ausência do backend node.js, poderíamos usar um arquivo online serviço de armazenamento, como o Google Cloud Storage, para armazenar ativos de aplicativos estáticos.
O Google Cloud Storage é um serviço de armazenamento de arquivos disponível globalmente usado para armazenar qualquer quantidade de dados como objetos para aplicativos em buckets. É flexível o suficiente para lidar com o armazenamento de ativos estáticos para aplicativos de pequeno e grande porte.
Para usar o serviço Cloud Storage em um aplicativo, podemos usar os endpoints da API Storage disponíveis ou usar a biblioteca de cliente Storage do node oficial. No entanto, a biblioteca de cliente do Node Storage não funciona em uma janela do navegador, portanto, podemos usar uma função do Cloud onde usaremos a biblioteca.
Um exemplo disso é a Função do Cloud abaixo, que conecta e faz upload de um arquivo para um Cloud Bucket criado.
const cors = require("cors")({ origin: true }); const { Storage } = require("@google-cloud/storage"); const StorageClient = new Storage(); exports.Uploader = (req, res) => { const { file } = req.body; StorageClient.bucket("TEST_BUCKET") .file(file.name) .then((response) => { console.log(response); res.status(200).send(response) }) .catch((e) => res.status(422).send({error : e})); }); };
A partir da função de nuvem acima, estamos realizando as duas operações principais a seguir:
Primeiro, criamos uma conexão com o Cloud Storage no
Storage constructor
de armazenamento e ele usa o recurso Application Default Credentials (ADC) no Google Cloud para autenticar com o Cloud Storage.Segundo, carregamos o arquivo incluído no corpo da solicitação para nosso
TEST_BUCKET
chamando o método.file
e passando o nome do arquivo. Como esta é uma operação assíncrona, usamos uma promessa para saber quando essa ação foi resolvida e enviamos uma resposta200
de volta, encerrando assim o ciclo de vida da invocação.
Agora, podemos expandir o Uploader
Cloud Function acima para lidar com o upload da imagem do perfil de um usuário. A função de nuvem receberá a imagem de perfil de um usuário, a armazenará no bucket de nuvem do nosso aplicativo e, em seguida, atualizará os dados img_uri
do usuário na coleção de nossos usuários no serviço Firestore.
require("dotenv").config(); const { Firestore } = require("@google-cloud/firestore"); const cors = require("cors")({ origin: true }); const { Storage } = require("@google-cloud/storage"); const StorageClient = new Storage(); const BucketName = process.env.STORAGE_BUCKET exports.Uploader = (req, res) => { return Cors(req, res, () => { const { file , userId } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); StorageClient.bucket(BucketName) .file(file.name) .on("finish", () => { StorageClient.bucket(BucketName) .file(file.name) .makePublic() .then(() => { const img_uri = `https://storage.googleapis.com/${Bucket}/${file.path}`; document .doc(userId) .update({ img_uri, }) .then((updateResult) => res.status(200).send(updateResult)) .catch((e) => res.status(500).send(e)); }) .catch((e) => console.log(e)); }); }); };
Agora expandimos a função Upload acima para realizar as seguintes operações extras:
- Primeiro, ele faz uma nova conexão com o serviço Firestore para obter nossa coleção de
users
inicializando o construtor Firestore e usa o Application Default Credentials (ADC) para autenticar com o Cloud Storage. - Depois de carregar o arquivo adicionado no corpo da solicitação, tornamos público para poder ser acessado por meio de uma URL pública, chamando o método
makePublic
no arquivo carregado. De acordo com o Controle de Acesso padrão do Cloud Storage, sem tornar um arquivo público, um arquivo não pode ser acessado pela internet e poder fazer isso quando o aplicativo for carregado.
Nota : Tornar um arquivo público significa que qualquer pessoa usando seu aplicativo pode copiar o link do arquivo e ter acesso irrestrito ao arquivo. Uma maneira de evitar isso é usar um URL assinado para conceder acesso temporário a um arquivo em seu bucket em vez de torná-lo totalmente público.
- Em seguida, atualizamos os dados existentes do usuário para incluir o URL do arquivo carregado. Encontramos os dados do usuário específico usando a consulta
WHERE
do Firestore e usamos ouserId
incluído no corpo da solicitação, depois definimos o campoimg_uri
para conter o URL da imagem recém-atualizada.
A função Upload
nuvem acima pode ser usada em qualquer aplicativo que tenha usuários registrados no serviço Firestore. Tudo o que é necessário para fazer uma requisição POST
ao endpoint, colocando o IS do usuário e uma imagem no corpo da requisição.
Um exemplo disso dentro da aplicação é o caso UPLOAD-FILE
que faz uma requisição POST
para a função e coloca o link da imagem retornado da requisição no estado da aplicação.
# index.js import Axios from 'axios' const UPLOAD_FUNCTION = process.env.REACT_APP_UPLOAD_FUNCTION export const UserReducer = (state, action) => { switch (action.type) { case "CREATE-USER" : # .....CREATE-USER-LOGIC .... case "UPLOAD-FILE": const { file, id } = action return Axios.post(UPLOAD_FUNCTION, { file, id }, { headers: { "Content-Type": "image/png", }, }) .then((response) => {}) .catch((e) => console.log(e)); default : return console.log(`${action.type} case not recognized`) } }
A partir do switch case acima, fazemos uma solicitação POST
usando Axios para o UPLOAD_FUNCTION
passando o arquivo adicionado para ser incluído no corpo da solicitação e também adicionamos uma imagem Content-Type
no cabeçalho da solicitação.
Após um upload bem-sucedido, a resposta retornada da função de nuvem contém o documento de dados do usuário que foi atualizado para conter um URL válido da imagem carregada no armazenamento em nuvem do Google. Podemos então atualizar o estado do usuário para conter os novos dados e isso também atualizará o elemento src
da imagem de perfil do usuário no componente do perfil.
Lidando com trabalhos Cron
Tarefas automatizadas repetitivas, como enviar e-mails para usuários ou realizar uma ação interna em um horário específico, são na maioria das vezes um recurso incluído nos aplicativos. Em um aplicativo node.js normal, essas tarefas podem ser tratadas como cron jobs usando node-cron ou node-schedule. Ao criar aplicativos sem servidor usando o Google Cloud Platform, o Cloud Scheduler também foi projetado para executar uma operação cron.
Observação : embora o Cloud Scheduler funcione de maneira semelhante ao utilitário cron do Unix na criação de tarefas que serão executadas no futuro, é importante observar que o Cloud Scheduler não executa um comando como o utilitário cron. Em vez disso, ele executa uma operação usando um destino especificado.
Como o nome indica, o Cloud Scheduler permite que os usuários programem uma operação para ser realizada em um momento futuro. Cada operação é chamada de trabalho e os trabalhos podem ser criados, atualizados e até mesmo destruídos visualmente na seção Agendador do Console do Cloud. Além de um campo de nome e descrição, os jobs no Cloud Scheduler consistem no seguinte:
- Frequência
Isso é usado para agendar a execução do trabalho Cron. Os agendamentos são especificados usando o formato unix-cron que é originalmente usado ao criar trabalhos em segundo plano na tabela cron em um ambiente Linux. O formato unix-cron consiste em uma string com cinco valores, cada um representando um ponto no tempo. Abaixo podemos ver cada uma das cinco strings e os valores que elas representam.
- - - - - - - - - - - - - - - - minute ( - 59 ) | - - - - - - - - - - - - - hour ( 0 - 23 ) | | - - - - - - - - - - - - day of month ( 1 - 31 ) | | | - - - - - - - - - month ( 1 - 12 ) | | | | - - - - - -- day of week ( 0 - 6 ) | | | | | | | | | | | | | | | | | | | | | | | | | * * * * *
A ferramenta geradora Crontab é útil ao tentar gerar um valor de frequência-tempo para um trabalho. Caso esteja encontrando dificuldade em juntar os valores de tempo, o gerador do Crontab possui um drop-down visual onde você pode selecionar os valores que compõem um agendamento e copiar o valor gerado e utilizar como frequência.
- Fuso horário
O fuso horário de onde o cron job é executado. Devido à diferença de horário entre os fusos horários, os cron jobs executados com diferentes fusos horários especificados terão tempos de execução diferentes. - Alvo
Isso é o que é usado na execução do Job especificado. Um destino pode ser um tipoHTTP
em que o job faz uma solicitação no horário especificado para um URL ou um tópico Pub/Sub no qual o job pode publicar mensagens ou extrair mensagens e, por último, um aplicativo do App Engine.
O Cloud Scheduler combina perfeitamente com as Cloud Functions acionadas por HTTP. Quando um job no Cloud Scheduler é criado com seu destino definido como HTTP, esse job pode ser usado para executar uma função de nuvem. Tudo o que precisa ser feito é especificar o endpoint da função de nuvem, especificar o verbo HTTP da solicitação e adicionar quaisquer dados que precisem ser passados para a função no campo de corpo exibido. Conforme mostra o exemplo abaixo:
O cron job na imagem acima será executado às 9h todos os dias, fazendo uma solicitação POST
para o endpoint de amostra de uma função de nuvem.
Um caso de uso mais realista de um cron job é enviar emails programados para usuários em um determinado intervalo usando um serviço de email externo, como o Mailgun. Para ver isso em ação, criaremos uma nova função de nuvem que envia um email HTML para um endereço de email especificado usando o pacote JavaScript nodemailer para se conectar ao Mailgun:
# index.js require("dotenv").config(); const nodemailer = require("nodemailer"); exports.Emailer = (req, res) => { let sender = process.env.SENDER; const { reciever, type } = req.body var transport = nodemailer.createTransport({ host: process.env.HOST, port: process.env.PORT, secure: false, auth: { user: process.env.SMTP_USERNAME, pass: process.env.SMTP_PASSWORD, }, }); if (!reciever) { res.status(400).send({ error: `Empty email address` }); } transport.verify(function (error, success) { if (error) { res .status(401) .send({ error: `failed to connect with stmp. check credentials` }); } }); switch (type) { case "statistics": return transport.sendMail( { from: sender, to: reciever, subject: "Your usage satistics of demo app", html: { path: "./welcome.html" }, }, (error, info) => { if (error) { res.status(401).send({ error : error }); } transport.close(); res.status(200).send({data : info}); } ); default: res.status(500).send({ error: "An available email template type has not been matched.", }); } };
Using the cloud function above we can send an email to any user's email address specified as the receiver value in the request body. It performs the sending of emails through the following steps:
- It creates an SMTP transport for sending messages by passing the
host
,user
andpass
which stands for password, all displayed on the user's Mailgun dashboard when a new account is created. - Next, it verifies if the SMTP transport has the credentials needed in order to establish a connection. If there's an error in establishing the connection, it ends the function's invocation and sends back a
401 unauthenticated
status code. - Next, it calls the
sendMail
method to send the email containing the HTML file as the email's body to the receiver's email address specified in theto
field.
Note : We use a switch statement in the cloud function above to make it more reusable for sending several emails for different recipients. This way we can send different emails based on the type
field included in the request body when calling this cloud function.
Now that there is a function that can send an email to a user; we are left with creating the cron job to invoke this cloud function. This time, the cron jobs are created dynamically each time a new user is created using the official Google cloud client library for the Cloud Scheduler from the initial firestoreFunction
.
We expand the CREATE-USER
case to create the job which sends the email to the created user at a one-day interval.
require("dotenv").config();cloc const { Firestore } = require("@google-cloud/firestore"); const scheduler = require("@google-cloud/scheduler") const cors = require("cors")({ origin: true }); const EMAILER = proccess.env.EMAILER_ENDPOINT const parent = ScheduleClient.locationPath( process.env.PROJECT_ID, process.env.LOCATION_ID ); exports.firestoreFunction = function (req, res) { return cors(req, res, () => { const { email, password, type } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); const client = new Scheduler.CloudSchedulerClient() if (!type) { res.status(422).send({ error : "An action type was not specified"}); } switch (type) { case "CREATE-USER":
const job = { httpTarget: { uri: process.env.EMAIL_FUNCTION_ENDPOINT, httpMethod: "POST", body: { email: email, }, }, schedule: "*/30 */6 */5 10 4", timezone: "Africa/Lagos", }
if (!email || !password) { res.status(422).send("email and password fields missing"); } return bcrypt.genSalt(10, (err, salt) => { bcrypt.hash(password, salt, (err, hash) => { document .add({ email: email, password: hash, }) .then((response) => {
client.createJob({ parent : parent, job : job }).then(() => res.status(200).send(response)) .catch(e => console.log(`unable to create job : ${e}`) )
}) .catch((e) => res.status(501).send(`error inserting data : ${e}`) ); }); }); default: res.status(422).send(`${type} is not a valid function action`) } }); };
From the snippet above, we can see the following:
- A connection to the Cloud Scheduler from the Scheduler constructor using the Application Default Credentials (ADC) is made.
- We create an object consisting of the following details which make up the cron job to be created:
-
uri
The endpoint of our email cloud function in which a request would be made to. -
body
This is the data containing the email address of the user to be included when the request is made. -
schedule
The unix cron format representing the time when this cron job is to be performed.
-
- After the promise from inserting the user's data document is resolved, we create the cron job by calling the
createJob
method and passing in the job object and the parent. - The function's execution is ended with a
200
status code after the promise from thecreateJob
operation has been resolved.
After the job is created, we'll see it listed on the scheduler page.
From the image above we can see the time scheduled for this job to be executed. We can decide to manually run this job or wait for it to be executed at the scheduled time.
Conclusão
Within this article, we have had a good look into serverless applications and the benefits of using them. We also had an extensive look at how developers can manage their serverless applications on the Google Cloud using Cloud Functions so you now know how the Google Cloud is supporting the use of serverless applications.
Within the next years to come, we will certainly see a large number of developers adapt to the use of serverless applications when building applications. If you are using cloud functions in a production environment, it is recommended that you read this article from a Google Cloud advocate on “6 Strategies For Scaling Your Serverless Applications”.
The source code of the created cloud functions are available within this Github repository and also the used front-end application within this Github repository. The front-end application has been deployed using Netlify and can be tested live here.
Referências
- Google Cloud
- Cloud Functions
- Cloud Source Repositories
- Cloud Scheduler overview
- Cloud Firestore
- “6 Strategies For Scaling Your Serverless Applications,” Preston Holmes