https://habrahabr.ru/post/334772/Всем ведь давно надоели колбэки в асинхронных вызовах и спагетти код? К счастью, в es6 появился новый сахар 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?