Лаборатория веб-кода Cloud Firestore

1. Обзор

Цели

В этой лаборатории кода вы создадите веб-приложение с рекомендациями ресторанов на базе Cloud Firestore .

img5.png

Что вы узнаете

  • Чтение и запись данных в Cloud Firestore из веб-приложения.
  • Слушайте изменения в данных Cloud Firestore в режиме реального времени
  • Используйте правила аутентификации и безопасности Firebase для защиты данных Cloud Firestore.
  • Написание сложных запросов Cloud Firestore

Что вам понадобится

Прежде чем приступить к этой лабораторной работе, убедитесь, что вы установили:

  • npm , который обычно поставляется с Node.js — рекомендуется Node 16+.
  • IDE/текстовый редактор по вашему выбору, например WebStorm , VS Code или Sublime.

2. Создайте и настройте проект Firebase.

Создать проект Firebase

  1. В консоли Firebase нажмите «Добавить проект» , затем назовите проект Firebase FriendlyEats .

Запомните идентификатор вашего проекта Firebase.

  1. Нажмите Создать проект .

Приложение, которое мы собираемся создать, использует несколько сервисов Firebase, доступных в Интернете:

  • Аутентификация Firebase для легкой идентификации ваших пользователей
  • Cloud Firestore для сохранения структурированных данных в облаке и мгновенного получения уведомлений при обновлении данных.
  • Хостинг Firebase для размещения и обслуживания ваших статических ресурсов.

Для этой конкретной лаборатории мы уже настроили хостинг Firebase. Однако для Firebase Auth и Cloud Firestore мы покажем вам настройку и включение сервисов с помощью консоли Firebase.

Включить анонимную аутентификацию

Хотя аутентификация не является целью этой лаборатории, важно иметь ту или иную форму аутентификации в нашем приложении. Мы будем использовать анонимный вход – это означает, что пользователь будет входить в систему автоматически, без соответствующего запроса.

Вам необходимо включить анонимный вход.

  1. В консоли Firebase найдите раздел «Сборка» в левой навигационной панели.
  2. Нажмите «Аутентификация » , затем перейдите на вкладку «Метод входа » (или нажмите здесь , чтобы перейти непосредственно туда).
  3. Включите поставщика анонимного входа, затем нажмите «Сохранить» .

img7.png

Это позволит приложению автоматически входить в систему ваших пользователей, когда они получают доступ к веб-приложению. Не стесняйтесь читать документацию по анонимной аутентификации , чтобы узнать больше.

Включить Cloud Firestore

Приложение использует Cloud Firestore для сохранения и получения информации и рейтингов ресторанов.

Вам нужно будет включить Cloud Firestore. В разделе «Сборка» консоли Firebase нажмите «База данных Firestore» . Нажмите Создать базу данных на панели Cloud Firestore.

Доступ к данным в Cloud Firestore контролируется правилами безопасности. Мы поговорим о правилах подробнее позже в этой лаборатории кода, но сначала нам нужно установить некоторые базовые правила для наших данных, чтобы начать. На вкладке «Правила» консоли Firebase добавьте следующие правила и нажмите «Опубликовать» .

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

Приведенные выше правила ограничивают доступ к данным для пользователей, вошедших в систему, что предотвращает чтение или запись неаутентифицированными пользователями. Это лучше, чем разрешить публичный доступ, но все еще далеко от безопасности. Мы улучшим эти правила позже в лаборатории кода.

3. Получите пример кода

Клонируйте репозиторий GitHub из командной строки:

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

Пример кода должен был быть клонирован в каталог 📁 friendlyeats-web . С этого момента обязательно запускайте все свои команды из этого каталога:

cd friendlyeats-web/vanilla-js

Импортируйте начальное приложение

Используя свою IDE (WebStorm, Atom, Sublime, Visual Studio Code...), откройте или импортируйте каталог 📁 friendlyeats-web . Этот каталог содержит начальный код для лаборатории кода, которая представляет собой еще не работающее приложение для рекомендаций ресторанов. Мы сделаем его функциональным на протяжении всей этой лаборатории кода, поэтому вскоре вам нужно будет отредактировать код в этом каталоге.

4. Установите интерфейс командной строки Firebase.

Интерфейс командной строки Firebase (CLI) позволяет вам обслуживать ваше веб-приложение локально и развертывать его на хостинге Firebase.

  1. Установите CLI, выполнив следующую команду npm:
npm -g install firebase-tools
  1. Убедитесь, что CLI установлен правильно, выполнив следующую команду:
firebase --version

Убедитесь, что версия Firebase CLI — v7.4.0 или новее.

  1. Авторизуйте Firebase CLI, выполнив следующую команду:
firebase login

Мы настроили шаблон веб-приложения, чтобы получить конфигурацию вашего приложения для хостинга Firebase из локального каталога и файлов вашего приложения. Но для этого нам нужно связать ваше приложение с вашим проектом Firebase.

  1. Убедитесь, что ваша командная строка обращается к локальному каталогу вашего приложения.
  2. Свяжите свое приложение с проектом Firebase, выполнив следующую команду:
firebase use --add
  1. При появлении запроса выберите идентификатор проекта и присвойте проекту Firebase псевдоним.

Псевдоним полезен, если у вас несколько сред (производственная, промежуточная и т. д.). Однако для этой кодовой лаборатории давайте просто воспользуемся псевдонимом default .

  1. Следуйте остальным инструкциям в командной строке.

5. Запустите локальный сервер

Мы готовы начать работу над нашим приложением! Давайте запустим наше приложение локально!

  1. Выполните следующую команду Firebase CLI:
firebase emulators:start --only hosting
  1. В вашей командной строке должен появиться следующий ответ:
hosting: Local server: http://localhost:5000

Мы используем эмулятор хостинга Firebase для локального обслуживания нашего приложения. Веб-приложение теперь должно быть доступно по адресу http://localhost:5000 .

  1. Откройте свое приложение по адресу http://localhost:5000 .

Вы должны увидеть свою копию FriendlyEats, подключенную к вашему проекту Firebase.

Приложение автоматически подключилось к вашему проекту Firebase и автоматически выполнило вход как анонимный пользователь.

img2.png

6. Запишите данные в Cloud Firestore.

В этом разделе мы запишем некоторые данные в Cloud Firestore, чтобы можно было заполнить пользовательский интерфейс приложения. Это можно сделать вручную через консоль Firebase , но мы сделаем это в самом приложении, чтобы продемонстрировать базовую запись в Cloud Firestore.

Модель данных

Данные Firestore разделены на коллекции, документы, поля и подколлекции. Мы будем хранить каждый ресторан как документ в коллекции верхнего уровня под названием restaurants .

img3.png

Позже мы сохраним каждый отзыв в подколлекции под названием ratings под каждым рестораном.

img4.png

Добавьте рестораны в Firestore

Основным объектом модели в нашем приложении является ресторан. Давайте напишем код, который добавляет документ ресторана в коллекцию restaurants .

  1. Из загруженных файлов откройте scripts/FriendlyEats.Data.js .
  2. Найдите функцию FriendlyEats.prototype.addRestaurant .
  3. Замените всю функцию следующим кодом.

FriendlyEats.Data.js

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

Приведенный выше код добавляет новый документ в коллекцию restaurants . Данные документа поступают из простого объекта JavaScript. Мы делаем это, сначала получая ссылку на restaurants из коллекции Cloud Firestore, а затем add данные.

Давайте добавим рестораны!

  1. Вернитесь в приложение FriendlyEats в браузере и обновите его.
  2. Нажмите «Добавить фиктивные данные» .

Приложение автоматически сгенерирует случайный набор объектов ресторанов, а затем вызовет функцию addRestaurant . Однако вы пока не увидите данные в своем реальном веб-приложении, поскольку нам все еще нужно реализовать получение данных (следующий раздел кодовой лаборатории).

Однако если вы перейдете на вкладку Cloud Firestore в консоли Firebase, вы теперь должны увидеть новые документы в коллекции restaurants !

img6.png

Поздравляем, вы только что записали данные в Cloud Firestore из веб-приложения!

В следующем разделе вы узнаете, как получать данные из Cloud Firestore и отображать их в своем приложении.

7. Отображение данных из Cloud Firestore.

В этом разделе вы узнаете, как получать данные из Cloud Firestore и отображать их в своем приложении. Два ключевых шага — создание запроса и добавление прослушивателя снимков. Этот прослушиватель будет уведомлен обо всех существующих данных, соответствующих запросу, и будет получать обновления в режиме реального времени.

Сначала давайте создадим запрос, который будет обслуживать нефильтрованный список ресторанов по умолчанию.

  1. Вернитесь к файлу scripts/FriendlyEats.Data.js .
  2. Найдите функцию FriendlyEats.prototype.getAllRestaurants .
  3. Замените всю функцию следующим кодом.

FriendlyEats.Data.js

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

  this.getDocumentsInQuery(query, renderer);
};

В приведенном выше коде мы создаем запрос, который извлекает до 50 ресторанов из коллекции верхнего уровня с именем restaurants , упорядоченных по среднему рейтингу (в настоящее время все ноль). После объявления этого запроса мы передаем его методу getDocumentsInQuery() , который отвечает за загрузку и отображение данных.

Мы сделаем это, добавив прослушиватель снимков.

  1. Вернитесь к файлу scripts/FriendlyEats.Data.js .
  2. Найдите функцию FriendlyEats.prototype.getDocumentsInQuery .
  3. Замените всю функцию следующим кодом.

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

В приведенном выше коде query.onSnapshot запускает обратный вызов каждый раз, когда происходит изменение результата запроса.

  • В первый раз обратный вызов запускается со всем набором результатов запроса, то есть со всей коллекцией restaurants из Cloud Firestore. Затем он передает все отдельные документы функции renderer.display .
  • Когда документ удаляется, change.type равен removed . В данном случае мы вызовем функцию, которая удалит ресторан из пользовательского интерфейса.

Теперь, когда мы реализовали оба метода, обновите приложение и убедитесь, что рестораны, которые мы видели ранее в консоли Firebase, теперь видны в приложении. Если вы успешно завершили этот раздел, то ваше приложение теперь читает и записывает данные с помощью Cloud Firestore!

По мере изменения вашего списка ресторанов этот прослушиватель будет автоматически обновляться. Попробуйте зайти в консоль Firebase и вручную удалить ресторан или изменить его название — изменения сразу же отобразятся на вашем сайте!

img5.png

8. Получить() данные

До сих пор мы показали, как использовать onSnapshot для получения обновлений в реальном времени; однако это не всегда то, чего мы хотим. Иногда имеет смысл получить данные только один раз.

Мы хотим реализовать метод, который срабатывает, когда пользователь нажимает на определенный ресторан в вашем приложении.

  1. Вернитесь к своему файлу scripts/FriendlyEats.Data.js .
  2. Найдите функцию FriendlyEats.prototype.getRestaurant .
  3. Замените всю функцию следующим кодом.

FriendlyEats.Data.js

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

После реализации этого метода вы сможете просматривать страницы каждого ресторана. Просто нажмите на ресторан в списке, и вы увидите страницу с подробной информацией о ресторане:

img1.png

На данный момент вы не можете добавлять рейтинги, поскольку нам еще нужно реализовать добавление рейтингов позже в лаборатории кода.

9. Сортировка и фильтрация данных

В настоящее время наше приложение отображает список ресторанов, но у пользователя нет возможности фильтровать его по своим потребностям. В этом разделе вы будете использовать расширенные запросы Cloud Firestore для включения фильтрации.

Вот пример простого запроса для получения всех ресторанов Dim Sum :

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

Как следует из названия, методwhere where() заставит наш запрос загружать только те элементы коллекции, поля которых соответствуют установленным нами ограничениям. В этом случае будут загружены только рестораны с category Dim Sum .

В нашем приложении пользователь может объединить несколько фильтров для создания конкретных запросов, например «Пицца в Сан-Франциско» или «Морепродукты в Лос-Анджелесе в порядке популярности».

Мы создадим метод, который создаст запрос, который будет фильтровать наши рестораны на основе нескольких критериев, выбранных нашими пользователями.

  1. Вернитесь к своему файлу scripts/FriendlyEats.Data.js .
  2. Найдите функцию FriendlyEats.prototype.getFilteredRestaurants .
  3. Замените всю функцию следующим кодом.

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

В приведенном выше коде добавлено несколько where и одно предложение orderBy для построения составного запроса на основе пользовательского ввода. Наш запрос теперь будет возвращать только те рестораны, которые соответствуют требованиям пользователя.

Обновите приложение FriendlyEats в браузере, а затем убедитесь, что вы можете фильтровать по цене, городу и категории. Во время тестирования вы увидите ошибки в консоли JavaScript вашего браузера, которые выглядят следующим образом:

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

Эти ошибки связаны с тем, что Cloud Firestore требует индексов для большинства составных запросов. Требование индексов для запросов позволяет Cloud Firestore быстро масштабироваться.

Открытие ссылки из сообщения об ошибке автоматически откроет пользовательский интерфейс создания индекса в консоли Firebase с заполненными правильными параметрами. В следующем разделе мы напишем и развернем индексы, необходимые для этого приложения.

10. Развертывание индексов

Если мы не хотим исследовать каждый путь в нашем приложении и переходить по каждой ссылке для создания индекса, мы можем легко развернуть множество индексов одновременно с помощью Firebase CLI.

  1. В загруженном локальном каталоге вашего приложения вы найдете файл firestore.indexes.json .

Этот файл описывает все индексы, необходимые для всех возможных комбинаций фильтров.

firestore.indexes.json

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

   ...

 ]
}
  1. Разверните эти индексы с помощью следующей команды:
firebase deploy --only firestore:indexes

Через несколько минут ваши индексы станут активными, а сообщения об ошибках исчезнут.

11. Запись данных в транзакцию

В этом разделе мы добавим возможность пользователям оставлять отзывы о ресторанах. До сих пор все наши операции записи были атомарными и относительно простыми. Если какой-либо из них приведет к ошибке, мы, скорее всего, просто предложим пользователю повторить попытку, или наше приложение автоматически повторит попытку записи.

В нашем приложении будет много пользователей, которые захотят добавить рейтинг ресторана, поэтому нам нужно будет координировать несколько операций чтения и записи. Сначала необходимо отправить сам отзыв, затем обновить count ресторана и average rating . Если один из них дает сбой, а другой нет, мы остаемся в противоречивом состоянии, когда данные в одной части нашей базы данных не совпадают с данными в другой.

К счастью, Cloud Firestore предоставляет функциональность транзакций, которая позволяет нам выполнять несколько операций чтения и записи за одну атомарную операцию, гарантируя, что наши данные остаются согласованными.

  1. Вернитесь к своему файлу scripts/FriendlyEats.Data.js .
  2. Найдите функцию FriendlyEats.prototype.addRating .
  3. Замените всю функцию следующим кодом.

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

В приведенном выше блоке мы запускаем транзакцию для обновления числовых значений avgRating и numRatings в документе ресторана. Одновременно мы добавляем новый rating в подколлекцию ratings .

12. Защитите свои данные

В начале этой лабораторной работы мы установили правила безопасности нашего приложения, чтобы полностью открыть базу данных для любого чтения или записи. В реальном приложении нам хотелось бы установить гораздо более детальные правила, чтобы предотвратить нежелательный доступ к данным или их изменение.

  1. В разделе «Сборка» консоли Firebase нажмите «База данных Firestore» .
  2. Перейдите на вкладку «Правила» в разделе Cloud Firestore (или нажмите здесь , чтобы перейти непосредственно туда).
  3. Замените значения по умолчанию следующими правилами, затем нажмите «Опубликовать» .

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

Эти правила ограничивают доступ, чтобы клиенты могли вносить только безопасные изменения. Например:

  • Обновления документа о ресторане могут изменить только рейтинги, но не имя или какие-либо другие неизменяемые данные.
  • Рейтинги можно создавать только в том случае, если идентификатор пользователя соответствует идентификатору вошедшего в систему пользователя, что предотвращает подделку.

В качестве альтернативы использованию консоли Firebase вы можете использовать интерфейс командной строки Firebase для развертывания правил в вашем проекте Firebase. Файл firestore.rules в вашем рабочем каталоге уже содержит приведенные выше правила. Чтобы развернуть эти правила из вашей локальной файловой системы (вместо использования консоли Firebase), вы должны выполнить следующую команду:

firebase deploy --only firestore:rules

13. Заключение

В этой лабораторной работе вы узнали, как выполнять базовые и расширенные операции чтения и записи с помощью Cloud Firestore, а также как защитить доступ к данным с помощью правил безопасности. Полное решение вы можете найти в репозитории faststarts-js .

Чтобы узнать больше об Cloud Firestore, посетите следующие ресурсы:

14. [Необязательно] Принудительное применение с помощью проверки приложений

Firebase App Check обеспечивает защиту, помогая проверять и предотвращать нежелательный трафик в ваше приложение. На этом этапе вы защитите доступ к своим сервисам, добавив проверку приложений с помощью reCAPTCHA Enterprise .

Сначала вам нужно включить проверку приложений и reCaptcha.

Включение reCaptcha Enterprise

  1. В облачной консоли найдите и выберите reCaptcha Enterprise в разделе «Безопасность».
  2. Включите службу согласно запросу и нажмите «Создать ключ» .
  3. Введите отображаемое имя в соответствии с запросом и выберите Веб-сайт в качестве типа платформы.
  4. Добавьте развернутые URL-адреса в список доменов и убедитесь, что параметр «Использовать вызов флажка» не выбран .
  5. Нажмите «Создать ключ» и сохраните сгенерированный ключ где-нибудь для безопасного хранения. Он понадобится вам позже на этом этапе.

Включение проверки приложений

  1. В консоли Firebase найдите раздел «Сборка» на левой панели.
  2. Нажмите «Проверка приложений» , затем нажмите кнопку «Начать» (или перенаправьте непосредственно на консоль ).
  3. Нажмите «Зарегистрировать» и при появлении запроса введите ключ reCaptcha Enterprise, затем нажмите «Сохранить» .
  4. В представлении API выберите «Хранилище» и нажмите «Применить» . Сделайте то же самое для Cloud Firestore .

Проверка приложений теперь должна быть обязательной! Обновите приложение и попробуйте создать/просмотреть ресторан. Вы должны получить сообщение об ошибке:

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

Это означает, что проверка приложений по умолчанию блокирует непроверенные запросы. Теперь давайте добавим проверку в ваше приложение.

Перейдите к файлу FriendlyEats.View.js , обновите функцию initAppCheck и добавьте ключ reCaptcha для инициализации проверки приложений.

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

Экземпляр appCheck инициализируется с помощью ReCaptchaEnterpriseProvider с вашим ключом, а isTokenAutoRefreshEnabled позволяет токенам автоматически обновляться в вашем приложении.

Чтобы включить локальное тестирование, найдите раздел, в котором инициализируется приложение, в файле FriendlyEats.js и добавьте следующую строку в функцию FriendlyEats.prototype.initAppCheck :

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

Это зарегистрирует токен отладки в консоли вашего локального веб-приложения, аналогичный:

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.

Теперь перейдите в представление «Проверка приложений» в консоли Firebase.

Нажмите на дополнительное меню и выберите «Управление токенами отладки ».

Затем нажмите «Добавить токен отладки» и вставьте токен отладки с консоли, как будет предложено.

Поздравляем! Проверка приложений теперь должна работать в вашем приложении.