web-dev-qa-db-fra.com

Requête synchrone dans Node.js

Si je dois appeler 3 API http dans un ordre séquentiel, quelle serait la meilleure alternative au code suivant:

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) { 


          });
        });
        }
      });
    });
    }
  });
});
}
92
Howard

Utilisation de différés comme 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) {
    ...
  })

Si vous avez besoin de passer le champ d'application, alors faites quelque chose comme ça

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

J'aime bien la solution de Raynos, mais je préfère une bibliothèque de contrôle de flux différente.

https://github.com/caolan/async

Selon que vous avez besoin des résultats dans chaque fonction suivante, j'utilise soit les séries, les parallèles ou les chutes d'eau.

Series quand ils doivent être exécutés en série, mais vous n'avez pas nécessairement besoin des résultats dans chaque appel de fonction suivant.

Parallèle si elles peuvent être exécutées en parallèle, vous n'avez pas besoin des résultats de chaque fonction parallèle, et vous avez besoin d'un rappel lorsque tout est terminé.

Cascade si vous voulez transformer les résultats dans chaque fonction et passer à la suivante

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
});
52
Josh

Vous pouvez le faire en utilisant ma bibliothèque de nœuds communs :

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');
32
Oleg

demande de synchronisation

De loin le plus simple que j'ai trouvé et utilisé est sync-request et il supporte à la fois le noeud et le navigateur!

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

C'est ça, pas de configuration folle, pas d'installation complexe de lib, bien qu'il y ait un repli de lib. Fonctionne juste. J'ai essayé d'autres exemples ici et j'étais perplexe quand il y avait beaucoup de choses à faire ou que les installations ne fonctionnaient pas!

Remarques:

L'exemple que sync-request utilise ne joue pas avec Nice lorsque vous utilisez res.getBody(), tout le corps obtient est d'accepter un codage et de convertir les données de réponse. Il suffit de faire res.body.toString(encoding) à la place.

27
jemiloii

J'utiliserais une fonction récursive avec une liste d'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 );

edit: demande la version

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 );

edit: version de request/async

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
});
20
generalhenry

Une autre possibilité consiste à configurer un rappel qui suit les tâches terminées:

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();
}

Il vous suffit ensuite d’attribuer un identifiant à chacun et de définir vos besoins pour lesquels des tâches doivent être effectuées avant la fermeture de la connexion.

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

Ok, c'est pas joli. C'est juste une autre façon de faire des appels séquentiels. Il est regrettable que NodeJS ne fournisse pas les appels synchrones les plus élémentaires. Mais je comprends ce qu'est l'attrait de l'asynchronicité.

5
Nate

Il semble que les solutions à ce problème ne finissent jamais, en voici une de plus :)

// 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

5
Alexey Petrushin

utiliser sequenty.

Sudo npm install sequenty

ou 

https://github.com/AndyShin/sequenty

très simple. 

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]);

aussi vous pouvez utiliser une boucle comme ceci:

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!
4
Andy Shin

L’utilisation de la bibliothèque request peut aider à minimiser les conséquences cruelles:

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
        })
    })
})

Mais pour un maximum de confort, vous devriez essayer une bibliothèque de flux de contrôle comme Step - elle vous permettra également de paralléliser les demandes, à condition que cela soit acceptable:

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
Ricardo Tomasi

A partir de 2018 et en utilisant les modules ES6 et Promises, nous pouvons écrire une fonction comme celle-ci:

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));
});

puis dans un autre module

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

Le code doit être exécuté dans un contexte asynchrone (en utilisant le mot clé async)

2
vdegenne

Raynos a bien répondu à cette question. Pourtant, la bibliothèque de séquences a été modifiée depuis la publication de la réponse. 

Pour que la séquence fonctionne, suivez ce lien: https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c092598981717fbfb3521e .

Voici comment vous pouvez le faire fonctionner après npm install sequence:

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

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

Il y a beaucoup de bibliothèques de flux de contrôle - j'aime bien conseq (... parce que je l'ai écrit.) De plus, on('data') peut se déclencher plusieurs fois. Utilisez donc une bibliothèque d'encapsulage REST comme restler .

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
nornagon

...4 ans plus tard...

Voici une solution originale avec le framework Danf (vous n'avez besoin d'aucun code pour ce genre de choses, seulement de la configuration):

// 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'
            }
        ]
    }
};

Utilisez la même valeur order pour les opérations que vous souhaitez exécuter en parallèle.

Si vous voulez être encore plus court, vous pouvez utiliser un processus de collecte:

// 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'
            }
        ]
    }
};

Jetez un coup d'œil au aperçu du framework pour plus d'informations.

1
Gnucki

Voici ma version de @ andy-shin successivement avec des arguments dans array au lieu de index:

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

J'ai atterri ici parce que je devais limiter http.request (environ 10 000 requêtes d'agrégation à une recherche élastique pour créer un rapport analytique). Ce qui suit vient d’étouffer ma machine.

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

Mes URL sont très simples, donc cela peut ne pas s'appliquer trivialement à la question initiale, mais je pense que c'est à la fois potentiellement applicable et utile d'écrire ici pour les lecteurs qui débarquent ici avec des problèmes similaires aux miens et qui veulent une solution triviale de bibliothèque sans JavaScript.

Mon travail n'était pas dépendant de l'ordre et ma première approche pour éviter cela était de l'envelopper dans un script Shell pour le fragmenter (parce que je suis nouveau en JavaScript). C'était fonctionnel mais pas satisfaisant. Ma résolution JavaScript a finalement été la suivante:

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();

Cela ressemble à une récurrence mutuelle entre collect et get_top . Je ne suis pas sûr que ce soit le cas, car le système est asynchrone et la fonction collect se termine par un rappel mémorisé pour l'événement sur on. ('End' .

Je pense que c'est assez général pour s'appliquer à la question initiale. Si, comme dans mon scénario, la séquence/le jeu est connu, toutes les URL/clés peuvent être insérées en une seule fois dans la pile. S'ils sont calculés au fur et à mesure, la fonction on ('end' peut pousser la prochaine URL sur la pile juste avant get_top () . Le résultat est moins imbriqué et pourrait être plus facile à refactor lorsque l'API que vous appelez change.

Je réalise que cela est effectivement équivalent à la version récursive simple de @ generalhenry ci-dessus (donc j'ai voté pour ça!)

1
irwinj

Super demande

C'est un autre module synchrone basé sur des requêtes et utilisant des promesses. Super simple à utiliser, fonctionne bien avec les tests de moka.

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
jemiloii

Ce code peut être utilisé pour exécuter un tableau de promesses de manière synchrone et séquentielle, après quoi vous pouvez exécuter votre code final dans l'appel .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
galatians

En fait, j'ai exactement ce que vous (et moi) voulions, sans utiliser d'attente, de promesse ou d'inclusion dans une bibliothèque (externe) (à l'exception de la nôtre).

Voici comment le faire:

Nous allons créer un module C++ associé à node.js. Cette fonction du module C++ créera la requête HTTP et renverra les données sous forme de chaîne. Vous pouvez l'utiliser directement en effectuant:

var myData = newModule.get(url);

ÊTES-VOUS PRÊT pour commencer?

Étape 1: créez un nouveau dossier quelque part ailleurs sur votre ordinateur. Nous utilisons ce dossier uniquement pour construire le fichier module.node (compilé à partir de C++). Vous pourrez le déplacer ultérieurement.

Dans le nouveau dossier (je mets le mien dans mon nouveau dossier/src pour l’organisation):

npm init

puis

npm install node-gyp -g

maintenant créez 2 nouveaux fichiers: 1, appelé quelque chose.cpp et pour y mettre ce code (ou le modifier si vous le souhaitez):

#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);

Maintenant, créez un nouveau fichier dans le même répertoire appelé something.gyp et mettez-y (quelque chose comme) ceci:

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

Maintenant, dans le fichier package.json, ajoutez: "gypfile": true,

Maintenant: dans la console, node-gyp rebuild

S'il passe par toute la commande et dit "ok" à la fin sans erreur, vous êtes (presque) prêt à partir, sinon laissez un commentaire ..

Mais si cela fonctionne, accédez à build/Release/cobypp.node (ou à tout autre choix), copiez-le dans votre dossier node.js principal, puis dans node.js:

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

..

response.end(myData);//or whatever
0
bluejayke