Мы уже рассказывали об основах работы с async/await в Node.js, и о том, как использование этого нового механизма позволяет сделать код лучше. Сегодня поговорим о том, как создавать, используя async/await, RESTful API, взаимодействующие с базой данных Firebase. Особое внимание обратим на то, как писать красивый, удобный и понятный асинхронный код. Можете прямо сейчас попрощаться с адом коллбэков.
Для того, чтобы проработать этот материал, у вас должны быть установлены Node.js и Firebase Admin SDK. Если это не так — вот официальное руководство по настройке рабочей среды.
Запись данных
В примерах мы будем рассматривать фрагменты кода API, которое представляет собой серверную часть приложения, предназначенного для работы со словами. Пользователь приложения, найдя новое слово и желая сохранить его для того, чтобы позже выучить, может сохранить его в базу. Когда дело дойдёт до заучивания слова, его можно запросить из базы.
Создадим конечную точку POST, которая будет сохранять слова в базу данных:
// Зависимости
const admin = require('firebase-admin');
const express = require('express');
// Настройка
const db = admin.database();
const router = express.Router();
// Вспомогательные средства
router.use(bodyParser.json());
// API
router.post('/words', (req, res) => {
const {userId, word} = req.body;
db.ref(`words/${userId}`).push({word});
res.sendStatus(201);
});
Тут всё устроено очень просто, без излишеств. Мы принимаем идентификатор пользователя (userId
) и слово (word
), затем сохраняем слово в коллекции words
.
Однако, даже в таком вот простом примере кое-чего не хватает. Мы забыли об обработке ошибок. А именно, мы возвращаем код состояния 201 даже в том случае, если слово в базу сохранить не удалось.
Добавим в наш код обработку ошибок:
// API
router.post('/words', (req, res) => {
const {userId, word} = req.body;
db.ref(`words/${userId}`).push({word}, error => {
if (error) {
res.sendStatus(500);
// Логируем сообщение об ошибке во внешний сервис, например, в Sentry
} else {
res.sendStatus(201);
}
};
});
Теперь, когда конечная точка возвращает правильные коды состояний, клиент может вывести подходящее сообщение для пользователя. Например, что-то вроде: «Слово успешно сохранено», или: «Слово сохранить не удалось, попробуйте ещё раз».
Если вы неуверенно чувствуете себя, читая код, написанный с использованием возможностей ES2015+, взгляните на это руководство.
Чтение данных
Итак, в базу данных Firebase мы уже кое-что записали. Попробуем теперь чтение. Сначала — вот как будет выглядеть конечная точка GET, созданная с использованием традиционного подхода с применением промисов:
// API
router.get('/words', (req, res) => {
const {userId} = req.query;
db.ref(`words/${userId}`).once('value')
.then( snapshot => {
res.send(snapshot.val());
});
});
Тут, чтобы не перегружать пример, опущена обработка ошибок.
Пока код выглядит не таким уж и сложным. Взглянем теперь на реализацию того же самого с использованием async/await
:
// API
router.get('/words', async (req, res) => {
const {userId} = req.query;
const wordsSnapshot = await db.ref(`words/${userId}`).once('value');
res.send(wordsSnapshot.val())
});
Здесь, опять же, нет обработки ошибок. Обратите внимание на ключевое слово async
, добавленное перед параметрами (res, req)
стрелочной функции, и на ключевое слово await
, которое предшествует выражению db.ref()
.
Метод db.ref()
возвращает промис. Это означает, что тут мы можем задействовать await
для того, чтобы «приостановить» выполнение скрипта. Ключевое слово await
можно использовать с любыми промисами.
Метод res.send()
, расположенный в конце функции, будет вызван только после того, как разрешится промис db.ref()
.
Всё это хорошо, однако, по-настоящему оценить красоту решений, использующих async/await
, можно в случаях, когда нужно объединить в цепочку несколько асинхронных запросов.
Скажем, надо последовательно запустить некое количество асинхронных функций:
const example = require('example-library');
example.firstAsyncRequest()
.then( fistResponse => {
example.secondAsyncRequest(fistResponse)
.then( secondResponse => {
example.thirdAsyncRequest(secondResponse)
.then( thirdAsyncResponse => {
// Безумие продолжается
});
});
});
Не очень-то хорошо получилось. Такие конструкции ещё называют «пирамидами ужаса» (pyramid of doom). А если сюда ещё добавить обработку ошибок…
Теперь перепишем этот код с использованием async/await
:
const example = require('example-library');
const runDemo = async () => {
const fistResponse = await example.firstAsyncRequest();
const secondResponse = await example.secondAsyncRequest(fistResponse);
const thirdAsyncRequest = await example.thirdAsyncRequest(secondResponse);
};
runDemo();
Никаких ужасов тут теперь нет. Более того, все выражения с ключевым словом await
можно обернуть в один блок try/catch
для обработки любых ошибок:
const example = require('example-library');
const runDemo = async () => {
try {
const fistResponse = await example.firstAsyncRequest();
const secondResponse = await example.secondAsyncRequest(fistResponse);
const thirdAsyncRequest = await example.thirdAsyncRequest(secondResponse);
}
catch (error) {
// Обработка ошибок
}
};
runDemo();
Такой код выглядит вполне достойно. Теперь поговорим о параллельных запросах и async/await
.
Параллельные запросы и async/await
Что если нужно одновременно прочитать из базы данных множество записей? На самом деле — ничего особенно сложного тут нет. Достаточно использовать метод Promise.all()
для параллельного выполнения запросов:
// API
router.get('/words', async (req, res) => {
const wordsRef = db.ref(`words`).once('value');
const usersRef = db.ref(`users`).once('value');
const values = await Promise.all([wordsRef, usersRef]);
const wordsVal = values[0].val();
const userVal = values[1].val();
res.sendStatus(200);
});
Примечания о работе с Firebase
Создавая конечную точку API, которая будет возвращать то, что получено из базы данных Firebase, постарайтесь не возвращать весь snapshot.val()
. Это может вызвать проблемы с разбором JSON на клиенте.
Например, на стороне клиента есть такой код:
fetch('https://your-domain.com/api/words')
.then( response => response.json())
.then( json => {
// Обработка данных
})
.catch( error => {
// Обработка ошибок
});
То, что будет в snapshot.val()
, возвращённом Firebase, может оказаться либо JSON-объектом, либо значением null
, если ни одной записи найти не удалось. Если возвратить null
, тогда json.response()
в вышеприведённом коде выдаст ошибку, так как он попытается этот null
, не являющийся объектом, разобрать.
Чтобы от этого защититься, можно использовать Object.assign()
для того, чтобы всегда возвращать клиенту объект:
// API
router.get('/words', async (req, res) => {
const {userId} = req.query;
const wordsSnapshot = await db.ref(`words/${userId}`).once('value');
// Плохо
res.send(wordsSnapshot.val())
// Хорошо
const response = Object.assign({}, snapshot.val());
res.send(response);
});
Итоги
Как видите, конструкция async/await помогает избежать ада коллбэков и прочих неприятностей, делая код понятнее и облегчая обработку ошибок. Если вы хотите взглянуть на реальный проект, построенный с применением Node.js и базы данных Firebase, вот Vocabify — приложение, которое разработал автор этого материала. Оно предназначено для запоминания новых слов.
Уважаемые читатели! Используете ли вы async/await в своих проектах на Node.js?
Автор: ru_vds