javascript

Используем async/await в nodejs уже сегодня

  • понедельник, 20 ноября 2017 г. в 03:11:55
https://habrahabr.ru/post/334772/
  • Node.JS
  • JavaScript


Всем ведь давно надоели колбэки в асинхронных вызовах и спагетти код? К счастью, в es6 появился новый сахар async/await для использования любых асинхронных функций без головной боли.

Представим себе типовую задачу. Клиент положил товары в корзину, нажал «Оплатить». Корзина должна улететь в ноду, создается новый заказ, клиенту возвращается ссылка на оплату в платежную систему.

Алгоритм примерно такой:

  1. получаем от клиента id товара(ов) методом POST
  2. смотрим цену этих товаров в БД
  3. кладем заказ к себе в базу
  4. отправляем POST запрос платежной системе, чтобы она вернула нам уникальную ссылку для оплаты
  5. если всё ок, отдаем эту ссылку клиенту
  6. Перед тем, как писать злой коммент, надо представить что мы живем в идеальном мире и у нас не бывает ошибок от клиентов, в базе, в запросах..., так что во всём коде ниже их обработку я оставлю на волю судьбы, потому что это не интересно

Обычный код будет выглядеть как-то так:

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?