Лаборатория веб-кода 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.

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

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

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