1. Обзор
Цели
В этой лабораторной работе вы создадите веб-приложение для рекомендации ресторанов на базе Cloud Firestore .
Чему вы научитесь
- Чтение и запись данных в Cloud Firestore из веб-приложения
- Отслеживайте изменения в данных Cloud Firestore в режиме реального времени
- Используйте аутентификацию Firebase и правила безопасности для защиты данных Cloud Firestore
- Написание сложных запросов Cloud Firestore
Что вам понадобится
Перед началом работы над этой лабораторной работой убедитесь, что у вас установлено:
2. Создайте и настройте проект Firebase.
Создать проект Firebase
- Войдите в консоль Firebase, используя свою учетную запись Google.
- Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например,
FriendlyEats
). - Нажмите «Продолжить» .
- При появлении соответствующего запроса ознакомьтесь с условиями Firebase и примите их, а затем нажмите кнопку «Продолжить» .
- (Необязательно) Включите помощь ИИ в консоли Firebase (так называемая «Gemini в Firebase»).
- Для этой лабораторной работы вам не понадобится Google Analytics, поэтому отключите опцию Google Analytics.
- Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .
Настройка продуктов Firebase
Приложение, которое мы собираемся создать, использует несколько сервисов Firebase, доступных в Интернете:
- Аутентификация Firebase для легкой идентификации ваших пользователей
- Cloud Firestore для сохранения структурированных данных в облаке и мгновенного получения уведомлений об обновлении данных.
- Firebase Hosting для размещения и обслуживания ваших статических ресурсов
Для этой конкретной лабораторной работы мы уже настроили Firebase Hosting. Однако для Firebase Auth и Cloud Firestore мы покажем вам настройку и включение сервисов с помощью консоли Firebase.
Включить анонимную аутентификацию
Хотя аутентификация не является основной темой этой лабораторной работы, важно иметь какую-либо форму аутентификации в нашем приложении. Мы будем использовать анонимный вход , то есть пользователь будет авторизован в системе без запроса подтверждения.
Вам необходимо включить анонимный вход.
- В консоли Firebase найдите раздел «Сборка» в левой навигационной панели.
- Нажмите «Аутентификация» , затем нажмите вкладку «Метод входа» (или нажмите здесь , чтобы перейти туда).
- Включите поставщика анонимного входа, затем нажмите «Сохранить» .
Это позволит приложению незаметно авторизовать ваших пользователей при доступе к веб-приложению. Подробнее см. в документации по анонимной аутентификации .
Включить 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.
- Установите CLI, выполнив следующую команду npm:
npm -g install firebase-tools
- Убедитесь, что CLI установлен правильно, выполнив следующую команду:
firebase --version
Убедитесь, что версия Firebase CLI — v7.4.0 или более поздняя.
- Авторизуйте Firebase CLI, выполнив следующую команду:
firebase login
Мы настроили шаблон веб-приложения для извлечения конфигурации вашего приложения для Firebase Hosting из локального каталога и файлов вашего приложения. Но для этого нам необходимо связать ваше приложение с вашим проектом Firebase.
- Убедитесь, что ваша командная строка обращается к локальному каталогу вашего приложения.
- Свяжите свое приложение с проектом Firebase, выполнив следующую команду:
firebase use --add
- При появлении запроса выберите идентификатор вашего проекта , а затем присвойте вашему проекту Firebase псевдоним.
Псевдоним полезен, если у вас несколько сред (производственная, промежуточная и т. д.). Однако для этой работы мы будем использовать псевдоним default
.
- Следуйте оставшимся инструкциям в командной строке.
5. Запустите локальный сервер.
Мы готовы приступить к работе над нашим приложением! Давайте запустим его локально!
- Выполните следующую команду Firebase CLI:
firebase emulators:start --only hosting
- В командной строке должен быть отображен следующий ответ:
hosting: Local server: http://localhost:5000
Мы используем эмулятор Firebase Hosting для локального обслуживания нашего приложения. Теперь веб-приложение должно быть доступно по адресу http://localhost:5000 .
- Откройте приложение по адресу http://localhost:5000 .
Вы должны увидеть копию FriendlyEats, подключенную к вашему проекту Firebase.
Приложение автоматически подключилось к вашему проекту Firebase и незаметно авторизовало вас как анонимного пользователя.
6. Запись данных в Cloud Firestore
В этом разделе мы запишем данные в Cloud Firestore, чтобы заполнить пользовательский интерфейс приложения. Это можно сделать вручную через консоль Firebase , но мы сделаем это в самом приложении, чтобы продемонстрировать базовую запись в Cloud Firestore.
Модель данных
Данные Firestore разделены на коллекции, документы, поля и подколлекции. Мы будем хранить каждый ресторан как документ в коллекции верхнего уровня с именем restaurants
.
Позже мы сохраним каждый отзыв в подколлекции под названием ratings
для каждого ресторана.
Добавить рестораны в Firestore
Основной объект модели в нашем приложении — ресторан. Давайте напишем код, который добавит документ о ресторане в коллекцию restaurants
.
- Из загруженных файлов откройте
scripts/FriendlyEats.Data.js
. - Найдите функцию
FriendlyEats.prototype.addRestaurant
. - Замените всю функцию следующим кодом.
FriendlyEats.Data.js
FriendlyEats.prototype.addRestaurant = function(data) { var collection = firebase.firestore().collection('restaurants'); return collection.add(data); };
Приведённый выше код добавляет новый документ в коллекцию restaurants
. Данные документа берутся из простого объекта JavaScript. Для этого сначала получаем ссылку на коллекцию restaurants
в Cloud Firestore, а затем add
данные.
Давайте добавим рестораны!
- Вернитесь к приложению FriendlyEats в браузере и обновите его.
- Нажмите Добавить фиктивные данные .
Приложение автоматически сгенерирует случайный набор объектов ресторанов, а затем вызовет функцию addRestaurant
. Однако вы пока не увидите данные в своём веб-приложении, поскольку нам ещё нужно реализовать извлечение данных (следующий раздел кодовой лаборатории).
Однако если вы перейдете на вкладку Cloud Firestore в консоли Firebase, вы теперь должны увидеть новые документы в коллекции restaurants
!
Поздравляем, вы только что записали данные в Cloud Firestore из веб-приложения!
В следующем разделе вы узнаете, как извлекать данные из Cloud Firestore и отображать их в своем приложении.
7. Отображение данных из Cloud Firestore
В этом разделе вы узнаете, как извлекать данные из Cloud Firestore и отображать их в приложении. Два ключевых шага — создание запроса и добавление прослушивателя снимков. Этот прослушиватель будет получать уведомления обо всех существующих данных, соответствующих запросу, и будет получать обновления в режиме реального времени.
Сначала давайте создадим запрос, который будет обслуживать нефильтрованный список ресторанов по умолчанию.
- Вернитесь к файлу
scripts/FriendlyEats.Data.js
. - Найдите функцию
FriendlyEats.prototype.getAllRestaurants
. - Замените всю функцию следующим кодом.
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()
, который отвечает за загрузку и отображение данных.
Мы сделаем это, добавив прослушиватель снимков.
- Вернитесь к файлу
scripts/FriendlyEats.Data.js
. - Найдите функцию
FriendlyEats.prototype.getDocumentsInQuery
. - Замените всю функцию следующим кодом.
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 и вручную удалить ресторан или изменить его название — вы сразу увидите изменения на вашем сайте!
8. Получить() данные
До сих пор мы показывали, как использовать onSnapshot
для получения обновлений в режиме реального времени, однако это не всегда то, что нам нужно. Иногда разумнее получить данные только один раз.
Нам нужно реализовать метод, который активируется, когда пользователь нажимает на ссылку определенного ресторана в вашем приложении.
- Вернитесь к файлу
scripts/FriendlyEats.Data.js
. - Найдите функцию
FriendlyEats.prototype.getRestaurant
. - Замените всю функцию следующим кодом.
FriendlyEats.Data.js
FriendlyEats.prototype.getRestaurant = function(id) { return firebase.firestore().collection('restaurants').doc(id).get(); };
После внедрения этого метода вы сможете просматривать страницы каждого ресторана. Просто нажмите на ресторан в списке, и вы увидите страницу с подробной информацией о нём:
На данный момент вы не можете добавлять рейтинги, так как нам еще нужно реализовать добавление рейтингов позже в кодовой лаборатории.
9. Сортировка и фильтрация данных
В настоящее время наше приложение отображает список ресторанов, но пользователь не может фильтровать их по своим потребностям. В этом разделе мы воспользуемся расширенными запросами Cloud Firestore для фильтрации.
Вот пример простого запроса для получения всех ресторанов Dim Sum
:
var filteredQuery = query.where('category', '==', 'Dim Sum')
Как следует из названия, метод where()
загрузит по нашему запросу только те элементы коллекции, поля которых соответствуют заданным нами ограничениям. В данном случае будут загружены только рестораны, category
которых — Dim Sum
.
В нашем приложении пользователь может объединить несколько фильтров для создания определенных запросов, например, «Пицца в Сан-Франциско» или «Морепродукты в Лос-Анджелесе, упорядоченные по популярности».
Мы создадим метод, который будет формировать запрос, фильтрующий наши рестораны на основе нескольких критериев, выбранных нашими пользователями.
- Вернитесь к файлу
scripts/FriendlyEats.Data.js
. - Найдите функцию
FriendlyEats.prototype.getFilteredRestaurants
. - Замените всю функцию следующим кодом.
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.
- В загруженном локальном каталоге вашего приложения вы найдете файл
firestore.indexes.json
.
В этом файле описываются все индексы, необходимые для всех возможных комбинаций фильтров.
firestore.indexes.json
{ "indexes": [ { "collectionGroup": "restaurants", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "city", "order": "ASCENDING" }, { "fieldPath": "avgRating", "order": "DESCENDING" } ] }, ... ] }
- Разверните эти индексы с помощью следующей команды:
firebase deploy --only firestore:indexes
Через несколько минут ваши индексы будут готовы, а сообщения об ошибках исчезнут.
11. Запись данных в транзакцию
В этом разделе мы добавим возможность пользователям оставлять отзывы о ресторанах. До сих пор все наши операции записи были атомарными и относительно простыми. Если бы какая-либо из них приводила к ошибке, мы, скорее всего, просто предложили бы пользователю повторить попытку, или наше приложение автоматически повторило бы попытку.
В нашем приложении будет много пользователей, желающих оценить ресторан, поэтому нам потребуется координировать несколько операций чтения и записи. Сначала необходимо отправить сам отзыв, затем обновить count
оценок ресторана и average rating
. Если один из этих этапов не пройдёт проверку, а другой — нет, мы окажемся в несогласованном состоянии, когда данные в одной части нашей базы данных не будут соответствовать данным в другой.
К счастью, Cloud Firestore предоставляет функционал транзакций, который позволяет нам выполнять несколько операций чтения и записи в рамках одной атомарной операции, гарантируя, что наши данные остаются непротиворечивыми.
- Вернитесь к файлу
scripts/FriendlyEats.Data.js
. - Найдите функцию
FriendlyEats.prototype.addRating
. - Замените всю функцию следующим кодом.
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
- В консоли Cloud найдите и выберите reCaptcha Enterprise в разделе «Безопасность».
- Включите службу при появлении запроса и нажмите «Создать ключ» .
- При появлении запроса введите отображаемое имя и выберите «Веб-сайт» в качестве типа платформы.
- Добавьте развернутые URL-адреса в список доменов и убедитесь, что опция «Использовать проверку флажка» не выбрана .
- Нажмите «Создать ключ» и сохраните сгенерированный ключ в безопасном месте. Он понадобится вам позже на этом этапе.
Включение проверки приложений
- В консоли Firebase найдите раздел «Сборка» на левой панели.
- Нажмите «Проверка приложений» , затем нажмите кнопку «Начать» (или перенаправьте непосредственно в консоль ).
- Нажмите «Зарегистрироваться» и введите ключ reCaptcha Enterprise при появлении запроса, затем нажмите «Сохранить» .
- В представлении API выберите «Хранилище» и нажмите «Применить» . Сделайте то же самое для Cloud Firestore .
Проверка приложений теперь должна быть активирована! Обновите приложение и попробуйте создать/просмотреть ресторан. Должно появиться сообщение об ошибке:
Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
Это означает, что App Check по умолчанию блокирует непроверенные запросы. Теперь давайте добавим проверку в ваше приложение.
Перейдите к файлу FriendlyEats.View.js , обновите функцию initAppCheck
и добавьте ключ reCaptcha для инициализации App Check.
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.
Нажмите на дополнительное меню и выберите Управление токенами отладки .
Затем нажмите Добавить токен отладки и вставьте токен отладки из консоли, как будет предложено.
Поздравляем! Теперь App Check должен работать в вашем приложении.