Всем ведь давно надоели колбэки в асинхронных вызовах и спагетти код? К счастью, в es6 появился новый сахар async/await для использования любых асинхронных функций без головной боли.
Представим себе типовую задачу. Клиент положил товары в корзину, нажал «Оплатить». Корзина должна улететь в ноду, создается новый заказ, клиенту возвращается ссылка на оплату в платежную систему.
Алгоритм примерно такой:
Обычный код будет выглядеть как-то так:
Сколько колбэков насчитали?
На текущий день, многие библиотеки под ноду не умеют отдавать промис, и приходится использовать различные промисификаторы. Но использование промисов тоже выглядит не так круто, как async/await
А как вам такой код?
Ни единого колбека, Карл! Выглядит прямо как в php. Что же происходит под капотом в функции promise? Магия достаточно простая. В основном все функции передают в колбэк 2 объекта: error и result. В этом и будет заключаться универсальность использования функции:
То есть, по факту, оборачивая какой то метод в эту функцию, на выходе мы будем получать
Такие дела. Засовываем последний кусок кода в файл с названием promise, потом в том месте, где нужно избавиться от колбэка пишем const promise = require('./promise');
Есть и более изящные методы использования. Можно прокидывать туда названия параметров, которые хотим получить.
Например, при проверке файла на существование (fx.exist), метод возвращает только одно значение, true или false. И при текущем коде пришлось бы использовать if ( fileExist.err ) console.log('файл найден'), что не есть хорошо. Ну и неплохо было бы повесить ошибку на reject.
Так же можно выполнять и параллельные запросы в цикле, типа
Код используется в продакшне с максимальной зафиксированной нагрузкой ~300 запросов в секунду к нашему API (нет, это не интернет магазин из примера) около полугода (как раз тогда вышла нода 8 версии с поддержкой async/await). Небыло замечено багов, утечек или потери производительности, в сравнении с теми же колбэками или промисами в чистом виде. Как часы. Причем каждый метод генерит от 0 до 10 таких await-ов.
Статья не претендует на Нобелевсую премию, и такой код даже страшно выложить в github. Голова готова для камней «костыли/говнокод/велосипед», но тем не менее, надеюсь, кому то такая идея использования может оказаться интересной.
Пишите в комменты, как вы используете async/await?
Представим себе типовую задачу. Клиент положил товары в корзину, нажал «Оплатить». Корзина должна улететь в ноду, создается новый заказ, клиенту возвращается ссылка на оплату в платежную систему.
Алгоритм примерно такой:
- получаем от клиента id товара(ов) методом POST
- смотрим цену этих товаров в БД
- кладем заказ к себе в базу
- отправляем POST запрос платежной системе, чтобы она вернула нам уникальную ссылку для оплаты
- если всё ок, отдаем эту ссылку клиенту
- Перед тем, как писать злой коммент, надо представить что мы живем в идеальном мире и у нас не бывает ошибок от клиентов, в базе, в запросах..., так что во всём коде ниже их обработку я оставлю на волю судьбы, потому что это не интересно
Обычный код будет выглядеть как-то так:
const http = require('http'); const request = require('request'); const mysql = require('mysql') .createConnection() .connect() http.createServer( function(req, res){ // сохраним тут ссылку на объект res, чтобы потом через него можно было ответить клиенту let clientRes = res; // пришел запрос // ... получаем и парсим POST данные // предположим что пришедшие id распарсились так: let order = { ids: [10,15,17], phone: '79631234567' } // лезем в БД mysql.query('select cost from myshop where id in', order.ids, function(err, res){ // в res у нас лежат все строки из базы. Посчитаем сумму заказа (я знаю, что можно сумму посчитать в запросе. Но у нас может быть более сложная логика обработки заказа) let totalPrice = 0; for(let i = 0; i < prices.result.length; i++){ totalPrice += prices.result[i].price; } // сохраняем заказ у себя в базе (сохраняем как быдло, все товары в одну строку) mysq.query('insert into orders set client=?, ids=?, status=1', [order.phone, order.ids.join(',')], function(err, res){ // mysql возвратит insertId, его мы укажем как номер заказа в платежной системе let insertId = res.insertId; request.post('http://api.payment.example.com', {form: { name: `оплата заказа ${insertId}`, amount: totalPrice }}, function(err, res, body){ // парсим JSON ответ от платежки let link = JSON.parse(body).link; // отвечаем клиенту ссылкой clientRes.end(link); // обновим в базе статус заказа клиента, типа он получил ссылку на оплату mysql.query('update orders set status=2 where id=?', insertId, function(err, res){ console.log(`Статус заказа ${insertId} обновлен`); }); }); }); }); }).listen(8080);
Сколько колбэков насчитали?
На текущий день, многие библиотеки под ноду не умеют отдавать промис, и приходится использовать различные промисификаторы. Но использование промисов тоже выглядит не так круто, как async/await
А как вам такой код?
const http = require('http'); const request = require('request'); const promise = require('promise'); const mysql = require('mysql') .createConnection() .connect() http.createServer( async function(req, res){ // сохраним тут ссылку на объект res, чтобы потом через него можно было ответить клиенту let clientRes = res; // пришел запрос // ... получаем и парсим POST данные // предположим что пришедшие id распарсились так: let order = { ids: [10,15,17], phone: '79631234567' } // первый запрос let selectCost = await promise(mysql, mysql.query, 'select cost from myshop where id in', order.ids); // оп, и у нас есть selectCost{ err: ..., res: ... } // считает сумму let totalPrice = 0; for(let i = 0; i < prices.result.length; i++){ totalPrice += prices.result[i].price; } // сохраняем заказ у себя в базе let newOrder = await promise(mysql, mysql.query, 'insert into orders set client=?, ids=?, status=1', [order.phone, order.ids.join(',')]); let insertId = newOrder.res.insertId; // и опять без колбэка // кидаем запрос в платежку let payment = await promise(request, request.post, {form: { name: `оплата заказа ${insertId}`, amount: totalPrice }}); // парсим ответ от платежки и возвращаем ссылку клиенту let link = JSON.parse(payment.res.body).link; // отвечаем клиенту ссылкой clientRes.end(link); // обновляем статус заказа let updateOrder = await promise(mysql, mysql.query, 'update orders set status=2 where id=?', insertId); console.log(`Статус заказа ${insertId} обновлен`); }).listen(8080);
Ни единого колбека, Карл! Выглядит прямо как в php. Что же происходит под капотом в функции promise? Магия достаточно простая. В основном все функции передают в колбэк 2 объекта: error и result. В этом и будет заключаться универсальность использования функции:
"use strict" function promise(context, func, ...params){ // тут мы принимаем контекст, саму функцию (метод, если хотите) и всё что нужно передать в этот метод // оборачиваем вызов в промис return new Promise( resolve => { // собственно, вызов из нужного контекста func.call(context, ...params, (...callbackParams) => { // а это наш колбэк, который мы отдаем в resolve, предварительно отпарсив результат в удобный вывод (см. ниже ф-ию promiseToAssoc); let returnObject = promiseToAssoc([...callbackParams]); resolve( returnedObject ); }) }) } /* вспомогательная функция для разбора ответа от промисифицированной функции */ function promiseToAssoc(results){ let res = {}; // первые 3 объекта, которые приходят в колбэк мы по дефолту назовем err, res и body let assoc = ['err', 'res', 'body']; for(let i = 0; i < results.length; i++){ // остальные объекты (если они есть) будем называть field_3, field_4 и тд. let field = assoc[i] || `field_${i}`; res[ field ] = results[i]; } return res; } module.exports = promise;
То есть, по факту, оборачивая какой то метод в эту функцию, на выходе мы будем получать
let result = await promise(fs, fs.readFile, 'index.html'); // result = {err: null, res: 'содержимое файла'}
Такие дела. Засовываем последний кусок кода в файл с названием promise, потом в том месте, где нужно избавиться от колбэка пишем const promise = require('./promise');
Есть и более изящные методы использования. Можно прокидывать туда названия параметров, которые хотим получить.
Например, при проверке файла на существование (fx.exist), метод возвращает только одно значение, true или false. И при текущем коде пришлось бы использовать if ( fileExist.err ) console.log('файл найден'), что не есть хорошо. Ну и неплохо было бы повесить ошибку на reject.
Так же можно выполнять и параллельные запросы в цикле, типа
var files = ['1.csv', '2.csv', '3.csv']; var results = []; // всех вызываем for(let i = 0;i<files.length;i++){ results.push( promise(fs, fs.readFile, files[i]) ); } // а потом дожидаемся всех (при это все 3 могут придти одновременно, тоесть не нужно ждать каждый отдельно. Если третий прочитается быстрее чем первый, он уже будет готов ) for(let i = 0;i<files.length;i++){ results[i] = await results[i]; }
Код используется в продакшне с максимальной зафиксированной нагрузкой ~300 запросов в секунду к нашему API (нет, это не интернет магазин из примера) около полугода (как раз тогда вышла нода 8 версии с поддержкой async/await). Небыло замечено багов, утечек или потери производительности, в сравнении с теми же колбэками или промисами в чистом виде. Как часы. Причем каждый метод генерит от 0 до 10 таких await-ов.
Статья не претендует на Нобелевсую премию, и такой код даже страшно выложить в github. Голова готова для камней «костыли/говнокод/велосипед», но тем не менее, надеюсь, кому то такая идея использования может оказаться интересной.
Пишите в комменты, как вы используете async/await?
