Синхронний запит у Node.js


99

Якщо мені потрібно викликати 3 http API в послідовному порядку, що було б кращою альтернативою наступному коду:

http.get({ host: 'www.example.com', path: '/api_1.php' }, function(res) { 
  res.on('data', function(d) { 

    http.get({ host: 'www.example.com', path: '/api_2.php' }, function(res) { 
      res.on('data', function(d) { 

        http.get({ host: 'www.example.com', path: '/api_3.php' }, function(res) { 
          res.on('data', function(d) { 


          });
        });
        }
      });
    });
    }
  });
});
}

крім очищення цього, я не думаю, що ти можеш зробити краще, ніж це.
hvgotcodes

2
Чому вони повинні бути в порядку?
Райнос

11
@Raynos Можливо, вам знадобляться деякі дані з api_1, перш ніж ви дізнаєтесь, що надіслати api_2
andyortlieb

9
Варто згадати, що Futures є досить застарілим, подумайте про використання новішої бібліотеки, наприклад Bluebird або Q.
Benjamin Gruenbaum

1
Заголовок і питання суперечать один одному. Ви не описуєте у своєму запитанні синхронний запит, а послідовність запитів, які, як правило, виникають асинхронно. Велика різниця - синхронні блоки викликів, і послідовність асинхронних дій не блокує (блокуйте інтерфейс користувача, блокуйте сервер від обробки інших запитів). Нижче є відповідь, де згадується sync-requestбібліотека, що є хорошою відповіддю до назви цього питання, але не є відповіддю на те, що означає код питання. Відповідь нижче про Обіцянки - краща відповідь на це. Що ви мали на увазі?
Джейк

Відповіді:


69

Використання відкладених на кшталт Futures .

var sequence = Futures.sequence();

sequence
  .then(function(next) {
     http.get({}, next);
  })
  .then(function(next, res) {
     res.on("data", next);
  })
  .then(function(next, d) {
     http.get({}, next);
  })
  .then(function(next, res) {
    ...
  })

Якщо вам потрібно пройти рамки, просто зробіть щось подібне

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })

Спробуйте спробувати IcedCoffeScript, який забезпечує очікування та відстрочку для nodejs.
Thanigainathan

Це не блокує? Я маю на увазі, що він блокує наступну функцію в черзі, але це не блокує виконання інших функцій асинхронізації, чи не так?
Октав

1
Так, відкладені методи не блокують / асинхронізують.
dvlsg

4
API Promise ES6 повинен ефективно замінити це, навіть за словами автора "Futures"
Олександра Міллса

Ф'ючерси дуже старі і застарілі. Дивіться замість q.
Джим Ахо

53

Мені також подобається рішення Raynos, але я віддаю перевагу іншій бібліотеці управління потоком.

https://github.com/caolan/async

Залежно від того, чи потрібні вам результати в кожній наступній функції, я б використовував серію, паралель або водоспад.

Серії, коли їх потрібно виконувати серійно, але вам не обов'язково потрібні результати при кожному наступному виклику функції.

Паралельно, якщо вони можуть виконуватися паралельно, вам не потрібні результати кожного під час кожної паралельної функції, і вам потрібен зворотний виклик, коли всі завершені.

Водоспад, якщо ви хочете перетворити результати в кожній функції і перейти до наступної

endpoints = 
 [{ host: 'www.example.com', path: '/api_1.php' },
  { host: 'www.example.com', path: '/api_2.php' },
  { host: 'www.example.com', path: '/api_3.php' }];

async.mapSeries(endpoints, http.get, function(results){
    // Array of results
});

9
var http = need ('http');
Elle Mundy

7
Ха-ха. example.com - це власне домен, призначений для подібних речей. Ого.
meawoppl

Код async.series не працює, принаймні станом на async v0.2.10. series () бере лише два аргументи і виконує елементи першого аргументу як функції, тому async видає помилку, намагаючись виконати об'єкти як функції.
кришка

1
Ви можете зробити щось подібне до того, що призначено з цим кодом, використовуючи forEachAsync ( github.com/FuturesJS/forEachAsync ).
кришка

Це робить саме те, що я хотів. Дякую!
aProperFox

33

Ви можете зробити це за допомогою моєї бібліотеки загальних вузлів :

function get(url) {
  return new (require('httpclient').HttpClient)({
    method: 'GET',
      url: url
    }).finish().body.read().decodeToString();
}

var a = get('www.example.com/api_1.php'), 
    b = get('www.example.com/api_2.php'),
    c = get('www.example.com/api_3.php');

3
лайно, я підтримував думку, що це спрацює, і це не так :(require(...).HttpClient is not a constructor
moeiscool

30

sync-запит

На сьогодні найпростішим із тих, що я знайшов і використовував, є синхронізований запит, який підтримує і вузол, і браузер!

var request = require('sync-request');
var res = request('GET', 'http://google.com');
console.log(res.body.toString('utf-8'));

Це все, ніяка божевільна конфігурація, не встановлена ​​складна lib, хоча у неї є резервна версія ліб. Просто працює. Я спробував інші приклади тут, і я наткнувся, коли було багато додаткових налаштувань зробити або встановлення не спрацювало!

Примітки:

Приклад, який використовує синхронізований запит , не грає приємно, коли ви використовуєте res.getBody(), все тіло get get - це прийняти кодування та перетворити дані відповіді. Просто зробіть res.body.toString(encoding)замість цього.


Я виявив, що запит на синхронізацію відбувається дуже повільно. Я в кінцевому підсумку використав ще один github.com/dhruvbird/http-sync, який у моєму випадку в 10 разів швидший.
Філіп Спірідонов

у мене не було жодних повільних пробіжок для цього. Це породить дочірній процес. Скільки cpus використовує ваша система та яку версію вузла ви використовуєте? Я хотів би знати, щоб визначити, чи потрібно мені перемикатися чи ні.
jemiloii

Я згоден з Філіпом, це повільно.
Rambo7

Те саме, що я попросив flip, але я не отримав відповіді: скільки процесорів використовує ваша система та яку версію вузла ви використовуєте?
jemiloii

для цього використовується серйозна кількість процесора, не рекомендується для виробничого використання.
moeiscool

20

Я б використовував рекурсивну функцію зі списком apis

var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  http.get({ host: host, path: API }, function(res) { 
    var body = '';
    res.on('data', function (d) {
      body += d; 
    });
    res.on('end', function () {
      if( APIs.length ) {
        callAPIs ( host, APIs );
      }
    });
  });
}

callAPIs( host, APIs );

редагувати: версія запиту

var request = require('request');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  request(API, function(err, res, body) { 
    if( APIs.length ) {
      callAPIs ( host, APIs );
    }
  });
}

callAPIs( host, APIs );

редагувати: запит / асинхронна версія

var request = require('request');
var async = require('async');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

async.eachSeries(function (API, cb) {
  request(API, function (err, res, body) {
    cb(err);
  });
}, function (err) {
  //called when all done, or error occurs
});

Це метод, який я застосував, оскільки у мене є змінний перелік запитів на запити (600 позицій і зростаючих). При цьому, з вашим кодом існує проблема: подія "дані" випромінюватиметься кілька разів на запит, якщо вихід API перевищує розмір фрагмента. Ви хочете "буферувати" такі дані, як: var body = ''; res.on ('data', function (data) {body + = data;}). on ('end', function () {callback (body); if (APIs.length) callAPI (хост, API);} );
Анкіт Аггарвал

Оновлено. Я просто хотів показати, як проблема може бути спрощеною / гнучкішою через рекурсію. Особисто я завжди використовую модуль запиту для подібних речей, оскільки він дозволяє легко пропускати кілька зворотних викликів.
генералхенрі

@generalhenry, як би це зробити, якби я хотів використовувати модуль запиту? Чи можете ви запропонувати фрагмент коду, який досягає вищезазначеного, використовуючи запит?
Скотті

Я додав версію запиту та версію запиту / асинхронізації.
generalhenry

5

Здається, рішення цієї проблеми не має кінця, ось ще один :)

// do it once.
sync(fs, 'readFile')

// now use it anywhere in both sync or async ways.
var data = fs.readFile(__filename, 'utf8')

http://alexeypetrushin.github.com/synchronize


Хоча бібліотека, яку ви пов’язали, пропонує рішення проблеми ОП, у вашому прикладі fs.readFile завжди синхронізується.
Ерік

1
Ні, ви можете надавати зворотній виклик явно і використовувати його як асинхронну версію, якщо хочете.
Alex Craft

1
наприклад, для запитів http, але не для зв'язку з файловою системою.
Сет

5

Іншою можливістю є встановлення зворотного дзвінка, який відстежує виконані завдання:

function onApiResults(requestId, response, results) {
    requestsCompleted |= requestId;

    switch(requestId) {
        case REQUEST_API1:
            ...
            [Call API2]
            break;
        case REQUEST_API2:
            ...
            [Call API3]
            break;
        case REQUEST_API3:
            ...
            break;
    }

    if(requestId == requestsNeeded)
        response.end();
}

Потім просто призначте ідентифікатор кожному, і ви можете встановити свої вимоги, завдання яких необхідно виконати перед тим, як закрити з'єднання.

const var REQUEST_API1 = 0x01;
const var REQUEST_API2 = 0x02;
const var REQUEST_API3 = 0x03;
const var requestsNeeded = REQUEST_API1 | REQUEST_API2 | REQUEST_API3;

Гаразд, це не дуже. Це просто ще один спосіб здійснення послідовних дзвінків. Прикро, що NodeJS не забезпечує найпростіші синхронні дзвінки. Але я розумію, що таке приманка до асинхронності.


4

використовувати секвенті.

sudo npm встановити секвент

або

https://github.com/AndyShin/sequenty

дуже просто.

var sequenty = require('sequenty'); 

function f1(cb) // cb: callback by sequenty
{
  console.log("I'm f1");
  cb(); // please call this after finshed
}

function f2(cb)
{
  console.log("I'm f2");
  cb();
}

sequenty.run([f1, f2]);

також ви можете використовувати цикл, як це:

var f = [];
var queries = [ "select .. blah blah", "update blah blah", ...];

for (var i = 0; i < queries.length; i++)
{
  f[i] = function(cb, funcIndex) // sequenty gives you cb and funcIndex
  {
    db.query(queries[funcIndex], function(err, info)
    {
       cb(); // must be called
    });
  }
}

sequenty.run(f); // fire!

3

Використання бібліотеки запитів може допомогти мінімізувати шару:

var request = require('request')

request({ uri: 'http://api.com/1' }, function(err, response, body){
    // use body
    request({ uri: 'http://api.com/2' }, function(err, response, body){
        // use body
        request({ uri: 'http://api.com/3' }, function(err, response, body){
            // use body
        })
    })
})

Але для максимальної дивовижності вам слід спробувати деяку бібліотеку потоків управління, як Step - це також дозволить вам паралелізувати запити, вважаючи, що це прийнятно:

var request = require('request')
var Step    = require('step')

// request returns body as 3rd argument
// we have to move it so it works with Step :(
request.getBody = function(o, cb){
    request(o, function(err, resp, body){
        cb(err, body)
    })
}

Step(
    function getData(){
        request.getBody({ uri: 'http://api.com/?method=1' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=2' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=3' }, this.parallel())
    },
    function doStuff(err, r1, r2, r3){
        console.log(r1,r2,r3)
    }
)

3

Станом на 2018 рік та за допомогою модулів ES6 та Обіцянь ми можемо написати таку функцію:

import { get } from 'http';

export const fetch = (url) => new Promise((resolve, reject) => {
  get(url, (res) => {
    let data = '';
    res.on('end', () => resolve(data));
    res.on('data', (buf) => data += buf.toString());
  })
    .on('error', e => reject(e));
});

а потім в інший модуль

let data;
data = await fetch('http://www.example.com/api_1.php');
// do something with data...
data = await fetch('http://www.example.com/api_2.php');
// do something with data
data = await fetch('http://www.example.com/api_3.php');
// do something with data

Код потрібно виконати в асинхронному контексті (використовуючи asyncключове слово)


2

Існує багато бібліотек контрольних потоків - мені подобається conseq (... тому що я це написав). Також on('data')можна запускати кілька разів, тому використовуйте бібліотеку обгортки REST, як рестлер .

Seq()
  .seq(function () {
    rest.get('http://www.example.com/api_1.php').on('complete', this.next);
  })
  .seq(function (d1) {
    this.d1 = d1;
    rest.get('http://www.example.com/api_2.php').on('complete', this.next);
  })
  .seq(function (d2) {
    this.d2 = d2;
    rest.get('http://www.example.com/api_3.php').on('complete', this.next);
  })
  .seq(function (d3) {
    // use this.d1, this.d2, d3
  })

2

На це добре відповів Райнос. Але в бібліотеці послідовностей змінилися з моменту опублікування відповіді.

Щоб отримати послідовність роботи, перейдіть за цим посиланням: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e .

Ось як це можна зробити після роботи npm install sequence:

var seq = require('sequence').Sequence;
var sequence = seq.create();

seq.then(function call 1).then(function call 2);

1

Ось моя версія @ andy-shin послідовно з аргументами в масиві замість індексу:

function run(funcs, args) {
    var i = 0;
    var recursive = function() {
        funcs[i](function() {
            i++;
            if (i < funcs.length)
                recursive();
        }, args[i]);
    };
    recursive();
}

1

... через 4 роки ...

Ось оригінальне рішення з рамкою Danf (вам не потрібен код для подібних речей, лише деякі конфігурації):

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                order: 0,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_1.php',
                    'GET'
                ],
                scope: 'response1'
            },
            {
                order: 1,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_2.php',
                    'GET'
                ],
                scope: 'response2'
            },
            {
                order: 2,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_3.php',
                    'GET'
                ],
                scope: 'response3'
            }
        ]
    }
};

Використовуйте те саме orderзначення для операцій, які ви хочете виконувати паралельно.

Якщо ви хочете бути ще коротшим, можете скористатися процедурою збору:

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                service: 'danf:http.router',
                method: 'follow',
                // Process the operation on each item
                // of the following collection.
                collection: {
                    // Define the input collection.
                    input: [
                        'www.example.com/api_1.php',
                        'www.example.com/api_2.php',
                        'www.example.com/api_3.php'
                    ],
                    // Define the async method used.
                    // You can specify any collection method
                    // of the async lib.
                    // '--' is a shorcut for 'forEachOfSeries'
                    // which is an execution in series.
                    method: '--'
                },
                arguments: [
                    // Resolve reference '@@.@@' in the context
                    // of the input item.
                    '@@.@@',
                    'GET'
                ],
                // Set the responses in the property 'responses'
                // of the stream.
                scope: 'responses'
            }
        ]
    }
};

Подивіться огляд рамки для отримання додаткової інформації.


1

Я приземлився сюди, тому що мені потрібно було обмежити швидкість http.request (~ 10 к. Запитів агрегації для еластичного пошуку для складання аналітичного звіту). Наступний щойно задушив мою машину.

for (item in set) {
    http.request(... + item + ...);
}

Мої URL-адреси дуже прості, тому це може не тривіально стосуватися оригінального запитання, але я думаю, що це є і потенційно застосовним, і варто написати тут для читачів, які приїжджають сюди з проблемами, схожими на мої, і які хочуть тривіального рішення, що не має бібліотеки JavaScript.

Моя робота не залежала від порядку, і моїм першим підходом до вирішення цього питання було загортання її в сценарій оболонки, щоб скинути її (бо я новачок у JavaScript). Це було функціонально, але не задовільно. Зрештою, моя роздільна здатність JavaScript полягала в наступному:

var stack=[];
stack.push('BOTTOM');

function get_top() {
  var top = stack.pop();
  if (top != 'BOTTOM')
    collect(top);
}

function collect(item) {
    http.request( ... + item + ...
    result.on('end', function() {
      ...
      get_top();
    });
    );
}

for (item in set) {
   stack.push(item);
}

get_top();

Це схоже на взаємну рекурсію між збиранням та get_top . Я не впевнений, що це насправді, оскільки система асинхронна, а збір функцій завершується зворотним дзвоном, захованим для події в . ('End') .

Я думаю, що достатньо загального для звернення до оригінального питання. Якщо, як і мій сценарій, відома послідовність / набір, всі URL-адреси / клавіші можна натиснути на стек за один крок. Якщо вони обчислюються під час переходу, функція on ('end' може натиснути наступний URL на стеку безпосередньо перед get_top () . Якщо що-небудь, результат має менше вкладення і може бути простішим для рефакторації, коли API, який ви викликаєте зміни.

Я усвідомлюю, що це ефективно еквівалентно простої рекурсивної версії @ Generalhenry (тому я підтримав це!)


0

Супер Запит

Це ще один синхронний модуль, який базується на запиті та використовує обіцянки. Супер простий у використанні, добре працює з тестами на мокко.

npm install super-request

request("http://domain.com")
    .post("/login")
    .form({username: "username", password: "password"})
    .expect(200)
    .expect({loggedIn: true})
    .end() //this request is done 
    //now start a new one in the same session 
    .get("/some/protected/route")
    .expect(200, {hello: "world"})
    .end(function(err){
        if(err){
            throw err;
        }
    });

0

Цей код можна використовувати для синхронного та послідовного виконання масиву обіцянок, після чого ви можете виконати свій остаточний код під час .then()виклику.

const allTasks = [() => promise1, () => promise2, () => promise3];

function executePromisesSync(tasks) {
  return tasks.reduce((task, nextTask) => task.then(nextTask), Promise.resolve());
}

executePromisesSync(allTasks).then(
  result => console.log(result),
  error => console.error(error)
);

0

Я фактично отримав саме те, що ви (і я) хотіли, без використання очікуючих, Обіцянь або включень будь-якої (зовнішньої) бібліотеки (крім нашої).

Ось як це зробити:

Ми зробимо модуль C ++ для роботи з node.js, і ця функція модуля C ++ зробить HTTP-запит і поверне дані у вигляді рядка, і ви можете використовувати це безпосередньо, виконавши:

var myData = newModule.get(url);

Чи готові ви розпочати роботу?

Крок 1. Створіть нову папку десь на вашому комп’ютері, ми використовуємо цю папку лише для створення файлу module.node (зібраного з C ++), ви можете перемістити її пізніше.

У новій папці (я помістив mine в mynewFolder / src для організації-ness):

npm init

тоді

npm install node-gyp -g

тепер зробіть 2 нові файли: 1, званий something.cpp і для цього введіть цей код (або змініть його, якщо хочете):

#pragma comment(lib, "urlmon.lib")
#include <sstream>
#include <WTypes.h>  
#include <node.h>
#include <urlmon.h> 
#include <iostream>
using namespace std;
using namespace v8;

Local<Value> S(const char* inp, Isolate* is) {
    return String::NewFromUtf8(
        is,
        inp,
        NewStringType::kNormal
    ).ToLocalChecked();
}

Local<Value> N(double inp, Isolate* is) {
    return Number::New(
        is,
        inp
    );
}

const char* stdStr(Local<Value> str, Isolate* is) {
    String::Utf8Value val(is, str);
    return *val;
}

double num(Local<Value> inp) {
    return inp.As<Number>()->Value();
}

Local<Value> str(Local<Value> inp) {
    return inp.As<String>();
}

Local<Value> get(const char* url, Isolate* is) {
    IStream* stream;
    HRESULT res = URLOpenBlockingStream(0, url, &stream, 0, 0);

    char buffer[100];
    unsigned long bytesReadSoFar;
    stringstream ss;
    stream->Read(buffer, 100, &bytesReadSoFar);
    while(bytesReadSoFar > 0U) {
        ss.write(buffer, (long long) bytesReadSoFar);
        stream->Read(buffer, 100, &bytesReadSoFar);
    }
    stream->Release();
    const string tmp = ss.str();
    const char* cstr = tmp.c_str();
    return S(cstr, is);
}

void Hello(const FunctionCallbackInfo<Value>& arguments) {
    cout << "Yo there!!" << endl;

    Isolate* is = arguments.GetIsolate();
    Local<Context> ctx = is->GetCurrentContext();

    const char* url = stdStr(arguments[0], is);
    Local<Value> pg = get(url,is);

    Local<Object> obj = Object::New(is);
    obj->Set(ctx,
        S("result",is),
        pg
    );
    arguments.GetReturnValue().Set(
       obj
    );

}

void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "get", Hello);
}

NODE_MODULE(cobypp, Init);

Тепер зробіть новий файл у тому ж каталозі, який називається, something.gypі покладіть (щось на зразок) цього в нього:

{
   "targets": [
       {
           "target_name": "cobypp",
           "sources": [ "src/cobypp.cpp" ]
       }
   ]
}

Тепер у файл package.json додайте: "gypfile": true,

Тепер: у консолі, node-gyp rebuild

Якщо він пройде через всю команду і каже "добре" наприкінці без помилок, ви (майже) добре піти, якщо ні, тоді залиште коментар.

Але якщо він працює, тоді перейдіть до створення / випуску / cobypp.node (або як би він не викликав вас), скопіюйте його у вашу основну папку node.js, а потім у node.js:

var myCPP = require("./cobypp")
var myData = myCPP.get("http://google.com").result;
console.log(myData);

..

response.end(myData);//or whatever
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.