Ответ 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
})
Чтобы получить обещание для окончательных данных.