1. Обзор
Цели
В этом практическом занятии вы создадите веб-приложение для рекомендаций ресторанов, работающее на платформе Cloud Firestore .

Что вы узнаете
- Чтение и запись данных в Cloud Firestore из веб-приложения.
- Отслеживайте изменения в данных Cloud Firestore в режиме реального времени.
- Используйте правила аутентификации и безопасности Firebase для защиты данных Cloud Firestore.
- Пишите сложные запросы к Cloud Firestore.
Что вам понадобится
Перед началом выполнения этого практического задания убедитесь, что у вас установлено следующее:
2. Создайте и настройте проект Firebase.
Создайте проект Firebase.
- Войдите в консоль Firebase, используя свою учетную запись Google.
- Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например,
FriendlyEats). - Нажмите «Продолжить» .
- Если появится запрос, ознакомьтесь с условиями использования Firebase и примите их, после чего нажмите «Продолжить» .
- (Необязательно) Включите помощь ИИ в консоли Firebase (в Firebase она называется "Gemini").
- Для этого практического занятия вам не понадобится 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 Hosting.
- Установите CLI, выполнив следующую команду npm:
npm -g install firebase-tools
- Убедитесь в правильности установки интерфейса командной строки, выполнив следующую команду:
firebase --version
Убедитесь, что версия Firebase CLI — v7.4.0 или более поздняя.
- Для авторизации Firebase CLI выполните следующую команду:
firebase login
Мы настроили шаблон веб-приложения таким образом, чтобы он получал конфигурацию вашего приложения для Firebase Hosting из локального каталога и файлов вашего приложения. Но для этого нам необходимо связать ваше приложение с вашим проектом Firebase.
- Убедитесь, что ваша командная строка обращается к локальному каталогу вашего приложения.
- Свяжите ваше приложение с вашим проектом Firebase, выполнив следующую команду:
firebase use --add
- При появлении запроса выберите идентификатор вашего проекта , а затем присвойте вашему проекту Firebase псевдоним.
Псевдоним полезен, если у вас несколько сред (production, staging и т. д.). Однако для этого практического занятия давайте просто воспользуемся псевдонимом 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. Получить данные с помощью функции Get()
До сих пор мы показывали, как использовать 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 для инициализации проверки приложений.
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.
Щелкните меню дополнительных параметров и выберите «Управление отладочными токенами» .
Затем нажмите «Добавить отладочный токен» и вставьте отладочный токен из консоли, как будет предложено.
Поздравляем! Функция проверки приложений теперь должна работать в вашем приложении.