Codelab da Web do Cloud Firestore

1. Visão geral

Metas

Neste codelab, você vai criar um app da Web voltado a recomendações de restaurantes com tecnologia do Cloud Firestore.

img5.png

O que você aprenderá

  • Ler e gravar dados no Cloud Firestore usando um app da Web
  • Detectar mudanças nos dados do Cloud Firestore em tempo real
  • Usar o Firebase Authentication e as regras de segurança para proteger os dados do Cloud Firestore
  • Gravar consultas complexas do Cloud Firestore

O que é necessário

Antes de iniciar este codelab, verifique se você instalou:

  • npm, que geralmente vem com o Node.js. Recomendamos o Node 16 ou mais recente.
  • O ambiente de desenvolvimento integrado/editor de texto de sua escolha, como o WebStorm, o VS Code ou o Sublime

2. Criar e configurar um projeto do Firebase

Criar um projeto do Firebase

  1. No Console do Firebase, clique em Adicionar projeto e nomeie o projeto como FriendlyEats.

Lembre-se do ID do projeto do Firebase.

  1. Clique em Criar projeto.

O aplicativo que vamos criar usa alguns serviços do Firebase disponíveis na Web:

  • Firebase Authentication: usado para identificar seus usuários com facilidade.
  • Cloud Firestore: usado para salvar dados estruturados na nuvem e receber notificações instantâneas quando os dados são atualizados.
  • Firebase Hosting: usado para hospedar e exibir seus recursos estáticos.

Para este codelab específico, já configuramos o Firebase Hosting. No entanto, para o Firebase Auth e o Cloud Firestore, vamos mostrar como configurar e ativar os serviços usando o Console do Firebase.

Ativar a autenticação anônima

Embora a autenticação não seja o foco deste codelab, é importante ter uma forma desse processo no seu app. Vamos usar o Login anônimo, o que significa que o usuário será conectado silenciosamente sem nenhuma solicitação.

É necessário ativar o Login anônimo.

  1. No Console do Firebase, localize a seção Build no painel de navegação à esquerda.
  2. Clique em Autenticação e na guia Método de login (ou clique aqui para acessar diretamente).
  3. Ative o provedor de login Anônimo e clique em Salvar.

img7.png

Isso permitirá que o aplicativo faça login dos usuários silenciosamente quando eles acessarem o app da Web. Leia a documentação de autenticação anônima para saber mais.

Ativar o Cloud Firestore

O app usa o Cloud Firestore para salvar e receber informações e classificações de restaurantes.

Você precisa ativar o Cloud Firestore. Na seção Build do Console do Firebase, clique em Firestore Database. Clique em Criar banco de dados no painel do Cloud Firestore.

O acesso aos dados no Cloud Firestore é controlado pelas Regras de segurança. Vamos falar mais sobre as regras mais adiante neste codelab, mas primeiro precisamos definir algumas regras básicas nos dados para começar. Na guia "Regras" do console do Firebase, adicione as regras a seguir e clique em Publicar.

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in resource.data)
      && (key in request.resource.data)
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && unchanged("name");

      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}

Vamos discutir essas regras e como elas funcionam mais tarde no codelab.

3. Acessar o exemplo de código

Clone o repositório do GitHub (link em inglês) na linha de comando:

git clone https://github.com/firebase/friendlyeats-web

O exemplo de código precisa ter sido clonado para o diretório 📁friendlyeats-web. De agora em diante, execute todos os comandos neste diretório:

cd friendlyeats-web/vanilla-js

Importar o app inicial

Usando seu ambiente de desenvolvimento integrado (WebStorm, Atom, Sublime, Visual Studio Code...), abra ou importe o diretório 📁friendlyeats-web. O diretório contém o código inicial do codelab, que consiste em um app de recomendação de restaurantes ainda não funcional. Vamos torná-lo funcional ao longo deste codelab, então você vai precisar editar o código nesse diretório em breve.

4. Instalar a interface de linha de comando do Firebase

A interface de linha de comando (CLI) do Firebase permite veicular seu app da Web localmente e implantá-lo no Firebase Hosting.

  1. Instale a CLI executando o seguinte comando do npm:
npm -g install firebase-tools
  1. Verifique se a CLI foi instalada corretamente executando o seguinte comando:
firebase --version

A CLI do Firebase precisa estar na versão v7.4.0 ou mais recente.

  1. Autorize a CLI do Firebase executando o seguinte comando:
firebase login

Configuramos o modelo de app da Web para extrair a configuração do Firebase Hosting do diretório e dos arquivos locais do app. Para isso, precisamos associar seu app ao projeto do Firebase.

  1. Confira se a linha de comando está acessando o diretório local do seu app.
  2. Para associar o app ao projeto do Firebase, execute o seguinte comando:
firebase use --add
  1. Quando solicitado, selecione o ID do projeto e atribua um alias ao projeto do Firebase.

O alias é útil se você tiver vários ambientes (produção, preparo etc.). No entanto, neste codelab, basta usar o alias default.

  1. Siga as instruções restantes na linha de comando.

5. Executar o servidor local

Já podemos começar a trabalhar no app. Vamos executar o app localmente.

  1. Execute o seguinte comando da CLI do Firebase:
firebase emulators:start --only hosting
  1. A linha de comando vai mostrar a seguinte resposta:
hosting: Local server: http://localhost:5000

Estamos usando o emulador do Firebase Hosting para disponibilizar nosso app localmente. Agora o app da Web está disponível em http://localhost:5000.

  1. Abra o app em http://localhost:5000.

Você vai encontrar a cópia do FriendlyEats, que foi conectada ao seu projeto do Firebase.

O app se conectou automaticamente ao seu projeto do Firebase e fez um login silencioso como um usuário anônimo.

img2.png

6. Gravar dados no Cloud Firestore

Nesta seção, vamos gravar alguns dados no Cloud Firestore para preencher a interface do app. Isso pode ser feito manualmente no Console do Firebase, mas vamos fazer isso no próprio app para demonstrar uma gravação básica do Cloud Firestore.

Modelo de dados

Os dados do Firestore são divididos em coleções, documentos, campos e subcoleções. Vamos armazenar cada restaurante como um documento em uma coleção de nível superior chamada restaurants.

img3.png

Mais tarde, vamos armazenar cada avaliação em uma subcoleção chamada ratings em cada restaurante.

img4.png

Adicionar restaurantes ao Firestore

O principal objeto de modelo no app é um restaurante. Vamos escrever um código que adicione um documento de restaurante à coleção restaurants.

  1. Abra scripts/FriendlyEats.Data.js nos arquivos transferidos por download.
  2. Encontre a função FriendlyEats.prototype.addRestaurant.
  3. Substitua a função inteira por este código:

FriendlyEats.Data.js

FriendlyEats.prototype.addRestaurant = function(data) {
  var collection = firebase.firestore().collection('restaurants');
  return collection.add(data);
};

O código acima adiciona um novo documento à coleção restaurants. Os dados do documento são provenientes de um objeto JavaScript simples. Para isso, primeiro você precisa de uma referência a uma coleção restaurants do Cloud Firestore e depois add (adicionar) os dados.

Vamos adicionar restaurantes.

  1. Volte para o app FriendlyEats no navegador e atualize.
  2. Clique em Adicionar dados simulados.

O app vai gerar automaticamente um conjunto aleatório de objetos de restaurantes e chamar a função addRestaurant. No entanto, você ainda não vai encontrar os dados no seu app da Web real, porque ainda precisamos implementar a recuperação dos dados (próxima seção do codelab).

No entanto, se você navegar até a guia do Cloud Firestore no Console do Firebase, vai encontrar novos documentos na coleção restaurants.

img6.png

Parabéns! Você acabou de gravar dados no Cloud Firestore com um app da Web.

Na próxima seção, você vai aprender a recuperar dados do Cloud Firestore e exibi-los no seu app.

7. Mostrar dados do Cloud Firestore

Nesta seção, você vai aprender a recuperar dados do Cloud Firestore e exibi-los no seu app. As duas etapas principais são criar uma consulta e adicionar um listener de snapshot. Esse listener vai ser notificado de todos os dados existentes que correspondem à consulta e vai receber atualizações em tempo real.

Primeiro, vamos criar a consulta que vai exibir a lista de restaurantes não filtrada padrão.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getAllRestaurants.
  3. Substitua a função inteira por este código:

FriendlyEats.Data.js

FriendlyEats.prototype.getAllRestaurants = function(renderer) {
  var query = firebase.firestore()
      .collection('restaurants')
      .orderBy('avgRating', 'desc')
      .limit(50);

  this.getDocumentsInQuery(query, renderer);
};

No código acima, criamos uma consulta que recupera até 50 restaurantes da coleção de nível superior chamada restaurants, que são ordenados pela nota média (atualmente, zero para todos). Depois de declarar essa consulta, transmitimos para o método getDocumentsInQuery(), que é responsável por carregar e renderizar os dados.

Para isso, vamos adicionar um listener de snapshot.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getDocumentsInQuery.
  3. Substitua a função inteira por este código:

FriendlyEats.Data.js

FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) {
  query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants".

    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        renderer.remove(change.doc);
      } else {
        renderer.display(change.doc);
      }
    });
  });
};

No código acima, query.onSnapshot vai acionar o callback sempre que houver uma mudança no resultado da consulta.

  • Na primeira vez, o callback é acionado com o conjunto de resultados inteiro da consulta, ou seja, toda a coleção restaurants do Cloud Firestore. Em seguida, ele transmite todos os documentos individuais para a função renderer.display.
  • Quando um documento é excluído, change.type é igual a removed. Nesse caso, vamos chamar uma função que remove o restaurante da interface.

Agora que implementamos os dois métodos, atualize o app e verifique se os restaurantes que você viu anteriormente no console do Firebase estão visíveis no app. Se você concluiu esta seção, isso significa que seu app está lendo e gravando dados com o Cloud Firestore.

Conforme a lista de restaurantes muda, esse listener continua sendo atualizado automaticamente. Acesse o Console do Firebase e exclua manualmente um restaurante ou mude o nome dele. As mudanças vão aparecer no seu site imediatamente.

img5.png

8. Dados de Get()

Até agora, mostramos como usar onSnapshot para recuperar atualizações em tempo real. No entanto, nem sempre é isso que queremos. Às vezes, faz mais sentido buscar os dados apenas uma vez.

Vamos implementar um método que é acionado quando um usuário clica em um restaurante específico no app.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getRestaurant.
  3. Substitua a função inteira por este código:

FriendlyEats.Data.js

FriendlyEats.prototype.getRestaurant = function(id) {
  return firebase.firestore().collection('restaurants').doc(id).get();
};

Depois de implementar esse método, você poderá acessar as páginas de cada restaurante. Basta clicar em um restaurante na lista para acessar a página de detalhes dele:

img1.png

Por enquanto, não é possível adicionar classificações, porque ainda precisamos implementar esse recurso mais adiante no codelab.

9. Classificar e filtrar dados

Atualmente, o app exibe uma lista de restaurantes, mas não há como o usuário filtrar de acordo com as necessidades dele. Nesta seção, você vai usar a consulta avançada do Cloud Firestore para ativar o uso de filtros.

Aqui está um exemplo de consulta simples para buscar todos os restaurantes de Dim Sum:

var filteredQuery = query.where('category', '==', 'Dim Sum')

Como o nome indica, o método where() faz com que a consulta faça o download apenas dos membros da coleção cujos campos atendam às restrições que definimos. Nesse caso, ele só faz o download de restaurantes em que a category é Dim Sum.

No nosso app, o usuário pode encadear vários filtros para criar consultas específicas, como "Pizza em São Paulo" ou "Frutos do mar em São Paulo, ordenados por popularidade".

Vamos criar um método que gera uma consulta que vai filtrar nossos restaurantes com base em vários critérios selecionados pelos usuários.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.getFilteredRestaurants.
  3. Substitua a função inteira por este código:

FriendlyEats.Data.js

FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) {
  var query = firebase.firestore().collection('restaurants');

  if (filters.category !== 'Any') {
    query = query.where('category', '==', filters.category);
  }

  if (filters.city !== 'Any') {
    query = query.where('city', '==', filters.city);
  }

  if (filters.price !== 'Any') {
    query = query.where('price', '==', filters.price.length);
  }

  if (filters.sort === 'Rating') {
    query = query.orderBy('avgRating', 'desc');
  } else if (filters.sort === 'Reviews') {
    query = query.orderBy('numRatings', 'desc');
  }

  this.getDocumentsInQuery(query, renderer);
};

O código acima adiciona vários filtros where e uma única cláusula orderBy para criar uma consulta composta com base na entrada do usuário. Agora, nossa consulta vai retornar apenas restaurantes que atendem aos requisitos do usuário.

Atualize o app FriendlyEats no navegador e verifique se é possível filtrar por preço, cidade e categoria. Durante os testes, você vai encontrar erros no console JavaScript do navegador semelhantes a este:

The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...

Esses erros ocorrem porque o Cloud Firestore exige índices para a maioria das consultas compostas. A exigência de índices em consultas mantém o Cloud Firestore rápido em escala.

Ao abrir o link pela mensagem de erro, a IU de criação do índice será aberta automaticamente no Console do Firebase com os parâmetros corretos preenchidos. Na próxima seção, vamos gravar e implantar os índices necessários para este aplicativo.

10. Implantar índices

Se não quisermos analisar cada caminho do app e acessar cada um dos links de criação de índice, é possível implantar vários índices de uma só vez usando a CLI do Firebase.

  1. No diretório local de download do app, você vai encontrar um arquivo firestore.indexes.json.

Esse arquivo descreve todos os índices necessários para todas as combinações possíveis de filtros.

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. Implante essas índices com o seguinte comando:
firebase deploy --only firestore:indexes

Após alguns minutos, os índices serão ativados e as mensagens de erro vão desaparecer.

11. Gravar dados em uma transação

Nesta seção, vamos adicionar um recurso para que os usuários avaliem os restaurantes. Até agora, todas as suas gravações foram atômicas e relativamente simples. Se alguma delas apresentar erro, provavelmente solicitaremos que o usuário tente novamente ou o app repetirá a tentativa de forma automática.

O app terá muitos usuários querendo adicionar uma nota para um restaurante, então precisamos coordenar várias leituras e gravações. Primeiro, a avaliação precisa ser enviada e, depois, a count da nota e a average rating do restaurante precisam ser atualizadas. Se uma delas falhar, mas a outra não, você ficará em um estado inconsistente em que os dados de uma parte do banco de dados não correspondem aos da outra.

Felizmente, o Cloud Firestore oferece uma funcionalidade de transação que permite realizar várias leituras e gravações em uma única operação atômica, garantindo que os dados permaneçam consistentes.

  1. Volte para o arquivo scripts/FriendlyEats.Data.js.
  2. Encontre a função FriendlyEats.prototype.addRating.
  3. Substitua a função inteira por este código:

FriendlyEats.Data.js

FriendlyEats.prototype.addRating = function(restaurantID, rating) {
  var collection = firebase.firestore().collection('restaurants');
  var document = collection.doc(restaurantID);
  var newRatingDocument = document.collection('ratings').doc();

  return firebase.firestore().runTransaction(function(transaction) {
    return transaction.get(document).then(function(doc) {
      var data = doc.data();

      var newAverage =
          (data.numRatings * data.avgRating + rating.rating) /
          (data.numRatings + 1);

      transaction.update(document, {
        numRatings: data.numRatings + 1,
        avgRating: newAverage
      });
      return transaction.set(newRatingDocument, rating);
    });
  });
};

No bloco acima, acionamos uma transação para atualizar os valores numéricos de avgRating e numRatings no documento do restaurante. Ao mesmo tempo, adicionamos a nova rating à subcoleção ratings.

12. Proteger seus dados

No início deste codelab, definimos as regras de segurança do app para restringir o acesso a ele.

firestore.rules

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in resource.data)
      && (key in request.resource.data)
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && unchanged("name");

      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}

Essas regras restringem o acesso para garantir que os clientes façam apenas mudanças seguras. Exemplo:

  • Atualizações no documento de um restaurante podem mudar apenas as notas, não o nome ou qualquer outro dado imutável.
  • As notas só poderão ser criadas se o ID do usuário for o do usuário conectado, o que evita o spoofing.

Em vez de usar o Console do Firebase, você pode usar a CLI do Firebase para implantar regras no seu projeto. O arquivo firestore.rules no seu diretório de trabalho já contém as regras acima. Para implantar essas regras no seu sistema de arquivos local, em vez de usar o Console do Firebase, execute o seguinte comando:

firebase deploy --only firestore:rules

13. Conclusão

Neste codelab, você aprendeu a realizar leituras e gravações básicas e avançadas com o Cloud Firestore, além de proteger o acesso aos dados com regras de segurança. Você pode encontrar a solução completa no repositório quickstarts-js.

Para saber mais sobre o Cloud Firestore, acesse estes recursos:

14. [Opcional] Aplicar com o App Check

O App Check do Firebase ajuda a proteger seu app, validando e impedindo tráfego indesejado. Nesta etapa, você vai proteger o acesso aos seus serviços adicionando o App Check com o reCAPTCHA Enterprise.

Primeiro, você precisa ativar o App Check e o reCaptcha.

Ativar o reCAPTCHA Enterprise

  1. No console do Cloud, encontre e selecione reCAPTCHA Enterprise em "Segurança".
  2. Ative o serviço conforme solicitado e clique em Criar chave.
  3. Insira um nome de exibição conforme solicitado e selecione Site como o tipo de plataforma.
  4. Adicione os URLs implantados à Lista de domínios e verifique se a opção "Usar desafio da caixa de seleção" está desmarcada.
  5. Clique em Criar chave e armazene a chave gerada em um local seguro. Você vai precisar dele mais tarde nesta etapa.

Como ativar o App Check

  1. No Console do Firebase, localize a seção Build no painel à esquerda.
  2. Clique em Verificação de app e depois no botão Começar (ou redirecione diretamente para o console).
  3. Clique em Registrar e insira sua chave do reCaptcha Enterprise quando solicitado. Em seguida, clique em Salvar.
  4. Na visualização "APIs", selecione Armazenamento e clique em Aplicar. Faça o mesmo para o Cloud Firestore.

O App Check agora é obrigatório. Atualize o app e tente criar ou conferir um restaurante. Você vai receber a seguinte mensagem de erro:

Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

Isso significa que o App Check está bloqueando solicitações não validadas por padrão. Agora vamos adicionar a validação ao app.

Acesse o arquivo FriendlyEats.View.js, atualize a função initAppCheck e adicione sua chave reCaptcha para inicializar a verificação de app.

FriendlyEats.prototype.initAppCheck = function() {
    var appCheck = firebase.appCheck();
    appCheck.activate(
    new firebase.appCheck.ReCaptchaEnterpriseProvider(
      /* reCAPTCHA Enterprise site key */
    ),
    true // Set to true to allow auto-refresh.
  );
};

A instância appCheck é inicializada com um ReCaptchaEnterpriseProvider com sua chave, e o isTokenAutoRefreshEnabled permite que os tokens sejam atualizados automaticamente no app.

Para ativar os testes locais, encontre a seção em que o app é inicializado no arquivo FriendlyEats.js e adicione a seguinte linha à função FriendlyEats.prototype.initAppCheck:

if(isLocalhost) {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

Isso vai registrar um token de depuração no console do seu app da Web local, semelhante a:

App Check debug token: 8DBDF614-649D-4D22-B0A3-6D489412838B. You will need to add it to your app's App Check settings in the Firebase console for it to work.

Agora, acesse a Visualização de apps do App Check no console do Firebase.

Clique no menu de contexto e selecione Gerenciar tokens de depuração.

Em seguida, clique em Adicionar token de depuração e cole o token de depuração do console conforme solicitado.

Parabéns! A verificação do app deve estar funcionando no seu app.