Лаборатория веб-кода 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, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например, FriendlyEats ).
  3. Нажмите «Продолжить» .
  4. Если появится запрос, ознакомьтесь с условиями использования Firebase и примите их, после чего нажмите «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (в Firebase она называется "Gemini").
  6. Для этого практического занятия вам не понадобится Google Analytics, поэтому отключите эту опцию.
  7. Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .

Настройка продуктов Firebase

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

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

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

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

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

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

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

img7.png

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

Включить Cloud Firestore

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

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

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

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

Мы обсудим эти правила и принцип их работы позже на практическом занятии.

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 Hosting.

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

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

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

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

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

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

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

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

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

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

Мы используем эмулятор Firebase Hosting для локального запуска нашего приложения. Теперь веб-приложение должно быть доступно по адресу 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. Получить данные с помощью функции Get()

До сих пор мы показывали, как использовать 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() заставит наш запрос загрузить только тех членов коллекции, поля которых соответствуют установленным нами ограничениям. В данном случае он загрузит только те рестораны, 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. Защитите свои данные.

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

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 CLI для развертывания правил в вашем проекте Firebase. Файл firestore.rules в вашей рабочей директории уже содержит правила, указанные выше. Чтобы развернуть эти правила из локальной файловой системы (а не с помощью консоли Firebase), выполните следующую команду:

firebase deploy --only firestore:rules

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

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

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

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

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

Для начала вам нужно включить проверку приложений (App Check) и reCaptcha.

Включение reCaptcha Enterprise

  1. В консоли Cloud найдите и выберите 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.

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

Перейдите в файл 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.

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

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

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