【问题标题】:Struggling with asynchronous Node JS API calls与异步 Node JS API 调用作斗争
【发布时间】:2013-08-15 14:18:35
【问题描述】:

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

这是我当前的代码,其中出现错误“TypeError: Cannot read property 'legs' of undefined”:

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

【问题讨论】:

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


    【解决方案1】:

    您收到的错误来自 google maps 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 摆脱了对google api 响应的手动缓冲。您将流指向它,一旦流完成写入,它就会将完整的响应传递给您。

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

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

    4. 检查谷歌地图调用是否失败。如果是,请在 500 毫秒内重试。

    认为这是最终版本。这段代码仍然有很多错误,但它至少让你更接近你想要做的事情。您应该获得用于 Google 地图的开发人员密钥。这应该使您能够更快地拨打电话而不会出错。

    【讨论】:

    • 我已经检查过了,但我认为情况并非如此。问题似乎是在谷歌地图 API 请求完成之前循环继续进行。如果我错了,请纠正我。
    • 另外,如果是这样的话,有没有办法绕过速率限制器?
    • 这是此代码中的众多问题之一。我正在进行重构,以解决其他问题。我有一个工作版本的代码,这就是我得到的错误。尝试更改 Factual API 调用以限制为一个结果,您应该不会再看到该错误。
    • 是的,我认为你一定是对的,因为循环循环两次才会产生错误。如果你能重构代码来解决这个问题,那就太好了。
    • 好的,更新了我的第一次重构过程。仍有很多工作可以改善这一点,但它正在发挥作用。
    【解决方案2】:

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

    async docs

    现在发送谷歌地图请求的循环有一个回调。使用传统的 for 循环或 Array.forEach,没有异步回调,因此您必须在异步任务完成后自行处理。

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

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

    我还将表示逻辑(创建 HTML 标记)与数据逻辑(Factual 和 Google Maps API 调用)分开。现在,获取餐厅和路线的代码与数据一起回调,因此您可以重新调整该函数的用途它在你项目的其他地方。

    我重新缩进了两个空格,个人喜欢看更多的嵌套。

    我将实际的 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');
    });
    

    【讨论】:

    • 非常感谢!这看起来很神奇,只要我有适当的机会查看代码,我就会接受这个答案。第一次处理异步代码时似乎有相当长的学习曲线。
    • 是的,完全正确。抱歉,如果有任何错误。如果您遇到任何疯狂的事情,我可以帮助您解决。这应该是组织异步代码的一个很好的起点。如果有什么特别想了解的,请在 cmets 中询问,欢迎来到 node!
    • 嗯...我遇到了和以前一样的错误,所以我想我可能会超过 Google 的 API 速率限制。循环通过 for 循环两次后发生错误。另外,我不应该看到从console.log(buffer) 打印到控制台的东西吗?似乎什么都没有出现。
    • response.on("end", function(err){ 函数中,放置console.log 语句以查看来自google 的响应是什么样的。您可能需要设置 limit:1 实际上进行测试。
    • 是的,我可以确认它在 2 个请求后超过了查询限制。我想下一个问题是如何避免这种情况?
    猜你喜欢
    • 1970-01-01
    • 2021-01-15
    • 2018-02-24
    • 1970-01-01
    • 2015-12-07
    • 1970-01-01
    • 1970-01-01
    • 2016-07-25
    • 1970-01-01
    相关资源
    最近更新 更多