Javascript Promise node.js?

Я новичок node.js, и я пытаюсь понять, как я могу организовать некоторую логику неблокируемым способом. node нравится.

У меня есть набор сред ['stage', 'prod'] и другой набор параметров, называемых брендами ['A', 'B', 'C'] и набором устройств ['phone', ' таблетки '].

В node мире с обратным вызовом у меня есть это:

brands.forEach( function(brand) {
    devices.forEach( function(device) {
        var tapeS = getTape('stage',brand,device); // bad example...tapeS never set
        var tapeP = getTape('prod' ,brand,device);
    })
} )
// more stuff here
function getTape(env,brand,device) {
   var req = http.request(someOptions,function(resp) {
       // ok, so we handle the response here, but how do I sequence this with all the other
       // responses, also happening asynchronously?
   });
}

Я пытаюсь создать отчет с блоками для каждой среды:

A:
    Stage -- report
    Prod  -- report 
B:    ...

Моя проблема в том, что, поскольку все здесь так асинхронно, особенно внутри getTape, которое вызывает node http.request. Как я могу сериализовать все в конце всего этого асинхронного удивления, чтобы я мог создать отчет в том порядке, в котором я хочу?

Я слышал что-то о javascript Promises. Будет ли это помогать, т.е. Каким-то образом собрать все эти Promises, а затем дождаться их завершения, затем получить собранные данные?

Ответ 1

Q является доминирующей реализацией обещания в node.js. У меня также есть своя суперлегкая promises библиотека Promise. В моей библиотеке не реализованы все функции, которые я использовал в этих примерах, но это может быть сделано для работы с незначительной адаптацией. Основополагающая спецификация для promises работает и ineroperate Promises/A +. Он определяет поведение для метода .then и является достаточно читаемым, поэтому определенно дайте ему взглянуть на какой-то момент (не обязательно сразу).

Идея promises заключается в том, что они инкапсулируют асинхронное значение. Это упрощает рассуждение о том, как конвертировать синхронный код в асинхронный код, потому что обычно есть хорошие параллели. В качестве введения к этим концепциям я бы порекомендовал свой разговор на Promises и генераторах или один из переговоров Доменика Дениколы (например, Promises, Promises или Обратные вызовы, Promises и Coroutines (oh my!)).

Первое, что нужно решить, - это то, хотите ли вы делать свои запросы параллельно или по одному последовательно. Из вопроса я собираюсь предположить, что вы хотите сделать их параллельно. Я также предполагаю, что вы используете Q, что означает, что вам нужно будет установить его с помощью:

npm install q

и требуйте его в верхней части каждого файла, в котором вы его используете:

var Q = require('q');

Размышляя о идеальной структуре данных, которая будет использоваться для печати этого отчета, я думаю, у вас будет массив брендов с массивом устройств, которые будут объектами со свойствами stage и prod, что-то вроде

[
  {
      brand: 'A',
      devices: [
        {
          device: 'phone',
          stage: TAPE,
          prod: TAPE
        },
        {
          device: 'tablet',
          stage: TAPE,
          prod: TAPE
        }
        ...
      ]
  },
  {
      brand: 'B',
      devices: [
        {
          device: 'phone',
          stage: TAPE,
          prod: TAPE
        },
        {
          device: 'tablet',
          stage: TAPE,
          prod: TAPE
        }
        ...
      ]
  }
  ...
]

Я собираюсь предположить, что если бы у вас было это, вам не составит труда распечатать требуемый отчет.

Обещанный HTTP-запрос

Давайте начнем с просмотра функции getTape. Ожидаете ли вы вернуть поток node.js или буфер/строку, содержащую весь загруженный файл? В любом случае, с помощью библиотеки вам будет намного проще найти ее. Если вы новичок в node.js, я бы рекомендовал request как библиотеку, которая просто делает то, что вы ожидаете. Если вы чувствуете себя более уверенно, substack hyperquest - это гораздо меньшая библиотека и, возможно, более аккуратная, но она требует, чтобы вы обрабатывали такие вещи, как перенаправления вручную, которые вы, вероятно, надеваете Не хочу заходить.

Потоковая передача (трудная)

Подход потоковой передачи является сложным. Это может быть сделано и будет необходимо, если ваши ленты имеют длину 100 с МБ, но promises - это, вероятно, не правильный путь. Я с удовольствием рассмотрю это более подробно, если это проблема, которую вы действительно имеете.

Буферизация с запросом (простой)

Чтобы создать функцию, которая выполняет буферизацию HTTP-запроса с помощью request и возвращает обещание, это довольно просто.

var Q = require('q')
var request = Q.denodeify(require('request'))

Q.denodeify является просто ярлыком для высказывания: "Возьмите эту функцию, которая обычно ожидает обратного вызова и дает мне функцию, которая обещает".

Чтобы написать getTape на основе этого, мы делаем что-то вроде:

function getTape(env, brand, device) {
  var response = request({
    uri: 'http://example.com/' + env + '/' + brand + '/' + device,
    method: 'GET'
  })
  return response.then(function (res) {
    if (res.statusCode >= 300) {
      throw new Error('Server responded with status code ' + res.statusCode)
    } else {
      return res.body.toString() //assuming tapes are strings and not binary data
    }
  })
}

Что происходит, что request (через Q.denodeify) возвращает обещание. Мы называем .then(onFulfilled, onRejected) этим обещанием. Это возвращает новое преобразованное обещание. Если обещание ответа было отклонено (эквивалентно throw в синхронном коде), то это преобразованное обещание (потому что мы не приложили обработчик onRejected).

Если вы выбрасываете одного из обработчиков, преобразованное обещание отклоняется. Если вы вернете значение из одного из обработчиков, преобразованное обещание "выполнено" (также иногда называемое "разрешено" ) с этим значением. Затем мы можем связать больше вызовов .then в конце нашего преобразованного обещания.

Мы возвращаем преобразованное обещание как результат нашей функции.

Выполнение запросов

JavaScript имеет действительно полезную функцию под названием .map. Это похоже на .forEach, но возвращает преобразованный массив. Я собираюсь использовать это, чтобы как можно ближе подойти к исходному синхронному коду.

var data = brands.map(function (brand) {
  var b = {brand: brand}
  b.devices = devices.map(function (device) {
    var d = {device: device}
    d.tapeS = getTape('stage',brand,device); // bad example...tapeS never set
    d.tapeP = getTape('prod' ,brand,device);
    return d
  })
})

Теперь у нас есть код, который дает нам структуру данных, которую я предложил в начале, кроме Promise<TAPE> вместо TAPE.

Ожидание запросов

Q имеет действительно полезный метод Q.all. Он принимает массив promises и ждет их завершения, поэтому мы перейдем к структуре данных в массив promises, чтобы перейти к Q.all.

Один из способов сделать это в конце, мы можем пройти через каждый элемент и дождаться разрешения promises.

var updated = Q.all(data.map(function (brand) {
  return Q.all(brand.devices.map(function (device) {
    return Q.all([device.tapeS, device.tapeP])
      .spread(function (tapeS, tapeP) {
        //update the values with the returned promises
        device.tapeS = tapeS
        device.tapeP = tapeP
      })
  })
}))

//if you add a line that reads `updated = updated.thenResolve(data)`,
//updated would become a promise for the data structure (after being resolved)

updated.then(function () {
  // `data` structure now has no promises in it and is ready to be printed
})

Еще один подход - это сделать это, когда мы идем, чтобы код "сделать запросы" заменен на:

var data = Q.all(brands.map(function (brand) {
  var b = {brand: brand}
  Q.all(devices.map(function (device) {
    var d = {device: device}
    var tapeSPromise = getTape('stage',brand,device);
    var tapePPromise = getTape('prod' ,brand,device);
    return Q.all([tapeSPromise, tapePPromise])
      .spread(function (tapeS, tapeP) { //now these are the actual tapes
        d.tapeS = tapeS
        d.tapeP = tapeP
        return d
      })
  }))
  .then(function (devices) {
    b.devices = devices
    return b
  })
}))

data.then(function (data) {
  // `data` structure now has no promises in it and is ready to be printed
})

Еще один подход - использовать небольшую библиотеку-утилиту, которая выполняет рекурсивное глубокое разрешение объекта. Мне не удалось опубликовать его, но эта функция полезности (заимствованная из работы Kriskowal) делает глубокое решение, которое позволит вам использовать:

var data = deep(brands.map(function (brand) {
  var b = {brand: brand}
  b.devices = devices.map(function (device) {
    var d = {device: device}
    d.tapeS = getTape('stage',brand,device); // bad example...tapeS never set
    d.tapeP = getTape('prod' ,brand,device);
    return d
  })
}))

data.then(function (data) {
  // `data` structure now has no promises in it and is ready to be printed
})

Чтобы получить обещание для окончательных данных.

Ответ 2

Я также довольно новичок в node.js, и недавно я обнаружил несколько библиотек, которые особенно эффективны при организации асинхронных обратных вызовов различными способами. Однако, безусловно, мой любимый async by caolan. Он имеет несколько полезных шаблонов, но те, которые я нашел наиболее полезными, - async.series, async.parallel, async.waterfall. Первый, async.series, просто выполняет асинхронные функции в линейном порядке:

async.series([
function(callback){
    // do some stuff ...
    callback(null, 'one');
},
function(callback){
    // do some more stuff ...
    callback(null, 'two');
}
],
// optional callback
function(err, results){
    // results is now equal to ['one', 'two']
});

Второй, async.parallel, просто выполняет функции одновременно:

async.parallel([
function(callback){
    setTimeout(function(){
        callback(null, 'one');
    }, 200);
},
function(callback){
    setTimeout(function(){
        callback(null, 'two');
    }, 100);
}
],
// optional callback
function(err, results){
    // the results array will equal ['one','two'] even though
    // the second function had a shorter timeout.
});

Последняя, ​​которая также является моей любимой, похожа на предыдущие async.series, но также передает результаты предыдущей функции следующей:

async.waterfall([
function(callback){
    callback(null, 'one', 'two');
},
function(arg1, arg2, callback){
    callback(null, 'three');
},
function(arg1, callback){
    // arg1 now equals 'three'
    callback(null, 'done');
}
], function (err, result) {
    // result now equals 'done'    
});

Хорошо, это моя штука. Это самый простой способ отформатировать node сумасшедшую неблокирующую архитектуру, на мой взгляд. Если вам нужна дополнительная помощь, пришлите мне премьер-министра. Я знаю, насколько сложным node.js может стать с более сложными кодовыми базами.

Приветствия.

Ответ 3

Альтернативным вариантом для promises будет использование модуля async:

async.map(brands, function(brand, brand_cb) {
    async.map(brand.devices, function(device, device_cb) {
        async.parallel({
            stage: function(cb) {
                // ...
                cb(null, stage_data)
            },
            prod: function(cb) {
                // ...
                cb(null, prod_data)
            }
        }, function(err, data) {
            device_cb(null, {name: device, data: data});
        });
    }, function(err, data) {
        brand_cb(null, {name: brand, devices: data});
    });
}, function(err, all_the_results) {
    console.log(all_the_results[0].devices[0].data.prod;
});

Ответ 4

Если вы заинтересованы в использовании promises, вы можете взглянуть на мою библиотеку Faithful. Он имитирует API Async для множества функций, а также имеет функцию "собрать", о которой вы кратко говорили.

Обратите внимание, что на данный момент верным.параллельно принимают только массив, а не хэш. Это еще предстоит реализовать.

Ответ 5

Как новичок, вы можете остаться с обратными вызовами и простыми библиотеками управления потоками. Просмотрите promises после того, как вы хорошо понимаете обратные вызовы и стиль продолжения.

Вот простой подход с использованием библиотеки queue, например:

var queue = require('queue-async')

var q = queue()

brands.forEach(function(brand){
    brand.devices.forEach(function(device){
        q.defer(getTape.bind(null, 'stage', brand, device))
        q.defer(getTape.bind(null, 'prod', brand, device))
    })
})

q.awaitAll(function(error, results){
    // use result pairs here
    console.log(results)
})