Мы уже рассказывали об основах работы с async/await в Node.js, и о том, как использование этого нового механизма позволяет сделать код лучше. Сегодня поговорим о том, как создавать, используя async/await, RESTful API, взаимодействующие с базой данных Firebase. Особое внимание обратим на то, как писать красивый, удобный и понятный асинхронный код. Можете прямо сейчас попрощаться с адом коллбэков.
Для того, чтобы проработать этот материал, у вас должны быть установлены Node.js и Firebase Admin SDK. Если это не так — вот официальное руководство по настройке рабочей среды.
В примерах мы будем рассматривать фрагменты кода API, которое представляет собой серверную часть приложения, предназначенного для работы со словами. Пользователь приложения, найдя новое слово и желая сохранить его для того, чтобы позже выучить, может сохранить его в базу. Когда дело дойдёт до заучивания слова, его можно запросить из базы.
Создадим конечную точку POST, которая будет сохранять слова в базу данных:
Тут всё устроено очень просто, без излишеств. Мы принимаем идентификатор пользователя (
Однако, даже в таком вот простом примере кое-чего не хватает. Мы забыли об обработке ошибок. А именно, мы возвращаем код состояния 201 даже в том случае, если слово в базу сохранить не удалось.
Добавим в наш код обработку ошибок:
Теперь, когда конечная точка возвращает правильные коды состояний, клиент может вывести подходящее сообщение для пользователя. Например, что-то вроде: «Слово успешно сохранено», или: «Слово сохранить не удалось, попробуйте ещё раз».
Если вы неуверенно чувствуете себя, читая код, написанный с использованием возможностей ES2015+, взгляните на это руководство.
Итак, в базу данных Firebase мы уже кое-что записали. Попробуем теперь чтение. Сначала — вот как будет выглядеть конечная точка GET, созданная с использованием традиционного подхода с применением промисов:
Тут, чтобы не перегружать пример, опущена обработка ошибок.
Пока код выглядит не таким уж и сложным. Взглянем теперь на реализацию того же самого с использованием
Здесь, опять же, нет обработки ошибок. Обратите внимание на ключевое слово
Метод
Метод
Всё это хорошо, однако, по-настоящему оценить красоту решений, использующих
Скажем, надо последовательно запустить некое количество асинхронных функций:
Не очень-то хорошо получилось. Такие конструкции ещё называют «пирамидами ужаса» (pyramid of doom). А если сюда ещё добавить обработку ошибок…
Теперь перепишем этот код с использованием
Никаких ужасов тут теперь нет. Более того, все выражения с ключевым словом
Такой код выглядит вполне достойно. Теперь поговорим о параллельных запросах и
Что если нужно одновременно прочитать из базы данных множество записей? На самом деле — ничего особенно сложного тут нет. Достаточно использовать метод
Создавая конечную точку API, которая будет возвращать то, что получено из базы данных Firebase, постарайтесь не возвращать весь
Например, на стороне клиента есть такой код:
То, что будет в
Чтобы от этого защититься, можно использовать
Как видите, конструкция async/await помогает избежать ада коллбэков и прочих неприятностей, делая код понятнее и облегчая обработку ошибок. Если вы хотите взглянуть на реальный проект, построенный с применением Node.js и базы данных Firebase, вот Vocabify — приложение, которое разработал автор этого материала. Оно предназначено для запоминания новых слов.
Уважаемые читатели! Используете ли вы async/await в своих проектах на Node.js?
Для того, чтобы проработать этот материал, у вас должны быть установлены 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?
Поделиться с друзьями
Комментарии (5)
Aries_ua
10.07.2017 15:35+4Сначала скептически отсесся к async / await, когда вышла спецификация. Но теперь проект полностью был переписан с этой технологией. И да, код действительно стал проще и читабельнее. Моя рекомендация — однозначно использовать.
SuperPaintman
11.07.2017 16:53+1Зачем ради одной проверки на
null
копировать все поля? Не лучше ли было бы
сделать так:
// API router.get('/words', async (req, res) => { const {userId} = req.query; const wordsSnapshot = await db.ref(`words/${userId}`).once('value'); // Еще лучше const response = snapshot.val() || {}; res.send(response); });
Касаемо асинхронных функций в Express роутере, попробуйте сделать так (я не
думаю, что вы дождетесь ответа. Даже от обработчика ошибок):
router.get('/words', async (req, res) => { noSuchMethod(); // <= res.send({}); }); router.use((err, req, res, next) => { console.error(err); err.sendStatuc(500); });
А исправить это можно вполне легко:
function isPromise(obj) { if (!obj || !obj.constructor) { return false; } if (obj.constructor.name === 'Promise' || obj.constructor.displayName === 'Promise') { return true; } return (typeof obj.then === 'function' || typeof obj.catch === 'function'); } function wrap(fn) { const { length } = fn; if (length < 4) { return (req, res, next) => { const result = fn(req, res, next); return isPromise(result) ? result.catch(next) : result; }; } else { return (err, req, res, next) => { const result = fn(err, req, res, next); return isPromise(result) ? result.catch(next) : result; }; } }; router.get('/words', wrap(async (req, res) => { noSuchMethod(); // <= res.send({}); })); router.use((err, req, res, next) => { console.error(err); // Вот теперь сюда свалится ошибка err.sendStatuc(500); });
SuperPaintman
11.07.2017 17:53p.s. конечно же не
err.sendStatuc(500);
, аres.sendStatus(500);
. К сожалению, кончилось время редактирования, и пишу комментарием.
Reey
Не надо так демонизировать промисы, поверх которых async/await работает. Ваш пример с пирамидой ужаса можно записать так:
Пирамида ужаса это скорее про коллбеки.
Это я к тому, что раз async/await уже в браузерах, то это не повод забыть про промисы, а наоборот, повод лучше в них разобраться ибо они никуда не уйдут.