在异步Node JS API调用中苦苦挣扎

时间:2013-08-13 19:53:57

标签: javascript node.js google-maps-api-3 asynchronous express

我刚刚开始学习Node.js,我正在尝试一个查询Factual API和Google maps API的项目。我一直在从几个不同的来源把代码放在一起,它变得一堆乱七八糟。此外,它目前使用for循环,这意味着循环中的异步API调用永远不会运行。如果有人能帮助我重构这段代码并且还告诉我如何让HTTP请求在循环中运行,我将非常感激。

这是我当前的代码,我收到错误“TypeError:无法读取未定义的属性'leg'”:

var auth = require('./auth');
var Factual = require('./node_modules/factual-api');
var factual = new Factual(auth.key, auth.secret);
var path = require('path');
var express = require('express');
app = express();

app.configure(function() {
    app.use(express.static(path.join(__dirname, 'public')));
});

var http = require("http");

app.get('/', function(req, res) {
    var placedata = "<link rel='stylesheet' type='text/css' href='default.css' /> <table>";
    factual.get('/t/restaurants-gb', {
        limit: 50,
        sort: "$distance:asc",
        geo: {
            "$circle": {
                "$center": [req.query.lat, req.query.lng],
                "$meters": 15000
            }
        }
    }, function(error, result) {


        for (var i = 0; i < result.data.length; i++) {
            var d = new Date();
            var seconds = (d.getTime() / 1000).toFixed(0);
            url = "http://maps.googleapis.com/maps/api/directions/json?mode=transit&origin=" + req.query.lat + "," + req.query.lng + "&destination=" + result.data[i].latitude + "," + result.data[i].longitude + "&sensor=false&departure_time=" + seconds;
            var request = http.get(url, function(response) { 
                var buffer = "",
                    data, route;

                response.on("data", function(chunk) {
                    console.log(buffer);
                    buffer += chunk;
                });

                response.on("end", function(err) {
                    data = JSON.parse(buffer);
                    route = data.routes[0];

                    console.log("Time: " + route.legs[0].duration.text);
                    placedata += "<tr><th align='left'>" + result.data[i].name + "</th><td>" + result.data[i].address + "</td><td>" + result.data[i].tel + "</td><td>" + result.data[i].$distance + " Metres" + "</td></tr>";
                });
            });
        }
        placedata += "</table>";
        res.send(placedata);
        console.log(">> Home");
    });

});

app.listen(process.env.PORT, process.env.IP, function() {
    console.log('Server is running');
});

2 个答案:

答案 0 :(得分:4)

您收到的错误来自Google地图API rate limiter。如果你看看你回来的结果,你可能会得到这样的东西。

{ routes: [], status: 'OVER_QUERY_LIMIT' }

在访问阵列中的元素之前,应检查以确保其中存在元素以防止此崩溃。

首次重构通行证

var auth = require('./auth');
var Factual = require('factual-api');
var factual = new Factual(auth.key, auth.secret);
var path = require('path');
var express = require('express');
var concat = require('concat-stream');
var http = require("http");
var async = require("async");

app = express();

app.configure(function() {
    app.use(express.static(path.join(__dirname, 'public')));
});


function getDirections(data, fn) {
    var url = "http://maps.googleapis.com/maps/api/directions/json?mode=transit&origin=" + data.originLat + "," + data.originLong + "&destination=" + data.destLat + "," + data.destLong + "&sensor=false&departure_time=" + data.time;
    http.get(url, function(response) { 
        response.pipe(concat(function(results) {
            fn(JSON.parse(results));
        })
    )});
}

app.get('/', function(req, res) {
    res.write("<table>");
    factual.get('/t/restaurants-gb', {
        limit: 10,
        sort: "$distance:asc",
        geo: {
            "$circle": {
                "$center": [req.query.lat, req.query.lng],
                "$meters": 15000
            }
        }
    }, function(err, result) {
        async.eachSeries(result.data, function(data, fn) {
            var d = new Date();
            var seconds = (d.getTime() / 1000).toFixed(0);
            var directionData = { originLat: req.query.lat, originLong: req.query.lng, destLat: data.latitude, destLong: data.longitude, time: seconds };

            function writeEntry(directions) {
                console.log(directions);
                if(directions.routes.length == 0) {
                    setTimeout(function() {
                        getDirections(directionData, writeEntry);
                    }, 500);
                    return;
                }
                route = directions.routes[0];
                res.write("<tr><th align='left'>" + data.name + "</th><td>" + data.address + "</td><td>" + data.tel + "</td><td>" + data.$distance + " Metres" + "</td><td>" + route.legs[0].duration.text + "</td></tr>");
                fn();            
            }

            getDirections(directionData, writeEntry);
        }, function(err) {
            res.end("</table>");    
        });
    });
});

app.listen(process.env.PORT, process.env.IP, function() {
    console.log('Server is running');
});

我在这里做了一些重大改变。

  1. 首先,我使用concat-stream摆脱了谷歌api响应的手动缓冲。您将流指向它,一旦流通过写入,它将通过您完整的响应。

  2. 我将输出从缓冲(使用placementata)更改为只要有数据就直接写出。这意味着页面将更快地显示,而不是等待所有结果返回并立即发送。

  3. 我用async.eachSeries替换了for循环。这删除了许多错误(关闭错误,在行之前写入表的末尾等)并简化了数据访问。

  4. 检查Google地图调用是否失败。如果确实如此,请在500毫秒内再试一次。

  5. 我会认为这是最终版本。这段代码仍有很多问题,但它至少让你更接近你想要做的事情。您应该获得与Google地图一起使用的开发人员密钥。这样可以让您更快地拨打电话而不会出错。

答案 1 :(得分:1)

这是一个(未经测试的)重构器。我使用的是async库,特别是async.map

async docs

现在发送Google地图请求的循环有一个回调。使用传统的for循环或Array.forEach,没有异步回调,因此您必须以自己的方式在异步任务完成后执行某些操作。

除了用async.map替换for循环之外,我做的最重要的事情是将一些匿名函数拆分为命名函数,以帮助减少回调地狱。所有重要位都隐藏在getRestsPlusRoutes内。

比较app.get()的长度。更容易理解发生了什么,因为在发送响应之前,您只关注单级回调。

我还从数据逻辑(Factual和Google Maps API调用)中分离了表示逻辑(创建HTML标记)。现在,获取餐馆和路线的代码回调数据,因此您可以重新调整功能它在你项目的其他地方。

我重新缩进到两个空格,个人偏好看到更多的嵌套。

我将事实results.data重命名为rests,因此更具描述性的变量名称有助于记录代码。

var async = require('async');
var auth = require('./auth');
var Factual = require('./node_modules/factual-api');
var factual = new Factual(auth.key, auth.secret);
var path = require('path');
var express = require('express');
app = express();

app.configure(function() {
  app.use(express.static(path.join(__dirname, 'public')));
});

var http = require("http");

app.get('/', function(req, res) {
  getRestsPlusRoutes(req, function(err, restsPlusRoutes){
    if(err){ 
      console.log(err);
      return res.send(500);
    };
    var pd = buildPlaceData(restsPlusRoutes);
    res.send(pd);
    console.log(">> Home");
  });
});


// You can mitigate callback hell by declaring your asynchronous functions
// outside the code which calls them
function getRestsPlusRoutes(req, callback){
  // we have to pass in req so we have access to req.query.lat etc
  factual.get(
  '/t/restaurants-gb', 
  {
    limit: 50,
    sort: "$distance:asc",
    geo: {
      "$circle": {
        "$center": [req.query.lat, req.query.lng],
        "$meters": 15000,
      },
    },
  }, 
  function(error, result) {
    var rests = result.data;
    async.mapLimit(
    rests,               // For each item in rests,
    1,                   // with max 1 concurrency,
    getDirections,       // call getDirections(rest, done)
    function(err, mappedAll){
      // this callback executed upon all getDirections done(null, mappedOne)
      // or on any getDirections done(err)
      // mappedAll is an array of JSON.parsed bodys from Google Maps API call
      if(err){
        return callback(err);
      };
      for(var i=0; i<rests.length; i++){
        // Attach the `.routes` property of the google map result 
        // to the factual restaurant object
        rests[i].routes = mappedAll[i].routes;
      };
      return callback(null, rests);
    }); // end of async.mapLimit function call, including inline callback declaration
  });

  // declare the iterator function within getRestsPlusRoutes 
  // to ensure `var url = ...` can access `req`
  function getDirections(rest, done){
    var d = new Date();
    var seconds = (d.getTime() / 1000).toFixed(0);
    var url = "http://maps.googleapis.com/maps/api/directions/json?mode=transit&origin=" + req.query.lat + "," + req.query.lng + "&destination=" + rest.latitude + "," + rest.longitude + "&sensor=false&departure_time=" + seconds;

    http.get(url, function(response) { 
      var buffer = "",
          data, route;

      response.on("data", function(chunk) {
        console.log(buffer);
        buffer += chunk;
      });

      response.on("end", function(err) {
        if(err){ return done(err) };
        data = JSON.parse(buffer);
        done(null, data);
        // for simplicity, the entire parsed response is passed to the iterator done()
      });
    });
  }; // end of getDirections iterator function

}; // end of getRestsPlusRoutes


function buildPlaceData(restsPlusRoutes){
  var placedata = "<link rel='stylesheet' type='text/css' href='default.css' /> <table>";

  for(var i=0; i < restsPlusRoutes.length; i++){
    var rest = restsPlusRoutes[i];
    var route = rest.routes[0];
    console.log("Time: " + route.legs[0].duration.text);
    placedata += "<tr><th align='left'>" + rest.name + "</th><td>" + rest.address + "</td><td>" + rest.tel + "</td><td>" + rest.$distance + " Metres" + "</td></tr>";
  };

  placedata += "</table>";
  return placedata;
};


app.listen(process.env.PORT, process.env.IP, function() {
  console.log('Server is running');
});