PhantomJS 是否在多个请求之间共享内存?

Does PhantomJS share memory between multiple requests?

我正在使用 PhantomJS + d3 渲染美国邮政编码地图作为后端进程。渲染和邮政编码计算需要足够长的时间,以至于将 html 和 d3 js 放入浏览器需要一分钟才能加载并导致其他问题,因此我们将其移至后端。

如果我通过 curl 向 PhantomJS 启动的节点服务器发送一个请求,没问题。如果我 space 我们的多个地图请求之间间隔大约 15 秒,也没有问题。但是,如果我非常快地启动几个 curl 请求,渲染的图像最终看起来是一样的(也就是将相同的图像写入多个文件。)这是幻影脚本:

可变端口, 服务器, 服务, 页, url, svgDrawer;

fs     = require('fs');
port   = 9494;
server = require('webserver').create();
page   = require('webpage').create();

service = server.listen(port, function (request, response) {
    var drawerPayload = null;
    try{
        drawerPayload=JSON.parse(request.post);
    } catch(e){
        response.statusCode = 417;
        response.write("Error : Invalid Input JSON");
        response.close();
        return;
    }

    url = 'file:///' + fs.absolute('./'+drawerPayload.inFile);
    page.open(url, function (status) {
        if(status=="success"){
            page.evaluate(function(data){
                $("body").on( "click", data, chartBuilder );
                $("body").click();

                var maxtimeOutMillis = 15000,
                    start = new Date().getTime(),
                    condition = false,
                    interval = setInterval(function() {
                        if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                            condition = $("svg.chart").hasClass("done"); //< defensive code
                        } else {
                            if(!condition) {
                                clearInterval(interval)
                            } else {
                                page.render(drawerPayload.outFile);
                                clearInterval(interval); //< Stop this interval
                            }
                        }
                    }, 250); //< repeat check every 250ms
            });

            response.statusCode = 200;
        } else {
            response.statusCode = 404;
            response.write("Not Found"+url);
        }
        response.close();
        return;
    });

    page.onError = function (msg, trace) {
        console.log(msg);
        trace.forEach(function(item) {
            console.log('  ', item.file, ':', item.line);
        })
        response.statusCode = 417;
        response.write("Error : "+msg);
        response.close();
        return;
    }
});

并且 html+d3 看起来像这样:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.zip {
    stroke: none;
}
.chart {
    fill: white;
    width: 1000px;
    height: 500px;
}
</style>
<body>
    <div id="chart-container">
        <svg class="chart"></svg>
    </div>
</body>

<script src="./jquery-min.js"></script>
<script src="./d3.min.js"></script>
<script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script>
<script>

function chartBuilder(e){
    var zip_data = e.data;
    $.getJSON("zips_us_topo.json", function(us){
        console.log("rendering...\n");
        var width = 1000;
        var height = 500;
        var projection = d3.geo.albersUsa()
            .scale(width)
            .translate([width / 2, height / 2]);

        var path = d3.geo.path().projection(projection);
        var color = d3.scale.log().domain([1,zip_data.max+1]).range(["#cccccc","#f63337"]);

        var svg = d3.select("svg.chart")
            .attr("width", width)
            .attr("height", height)
            .style({margin: "10px 100px"})
            .append("g")
            .attr("class", "counties")
            .selectAll("path")
            .data(topojson.feature(us, us.objects.zip_codes_for_the_usa).features)
            .enter()
                .append("path")
                    .attr("class", "zip")
                    .style({fill: function(d){
                        return color(zip_data.counts[d.properties.zip] ? zip_data.counts[d.properties.zip]+1 : 1);
                    }})
                    .attr("d", path);
        svg.classed("done", true);
    });
}
</script>

如果同时对所有请求进行 curled,看起来它正在将一个图像写入所有输出文件。 PhantomJS 是为每个请求创建一个新页面,还是每次都加载相同的请求?

对于所有请求,您只有一个 page 实例。当新请求进入并劫持当前 page.open() 请求时,这可能会产生一些竞争条件。根据您的首选方案,基本上有两种方法可以解决此问题。

多个"tabs"

简单的解决方法是为每个请求创建一个新的 page 实例,它们在同一浏览器中将是本质上不同的选项卡。因此,如果 cookie 或 localStorage 是个问题,那么这不适合您。

page = require('webpage').create(); 移到 server.listen 回调中,不要忘记 close() 使用后的 page 实例。

一次只能请求一个

由于这是一个不那么短的 运行 过程,您可以在 page.open() 当前不是 运行 时启动一个 page.open() 并将所有传入请求放入队列,只要 page.open()还没写完。完成后,保存响应,遍历请求队列并用相同的响应响应所有请求。

如果确实有很多并发请求,这当然比第一个解决方案在内存消耗方面要好得多。


但是您的代码还有其他问题。您在 page.evaluate() 中使用 setInterval(),这会中断控制流。 response.statusCode = 200; 很可能会在页面呈现之前设置。

page.evaluate() 里面的 page.render() 是另一个问题。 page.evaluate() 是沙盒页面上下文。它无法访问在其外部定义的变量,包括 pagerequire。 (Solution 对于这个孤立的问题)

这两个问题可以通过在页面上下文外部等待页面上下文内部的渲染条件来一击解决。我建议使用 waitFor from the examples:

if(status=="success"){
    page.evaluate(function(data){
        window._finishIndicationVariable = false;
        $("body").on( "click", data, chartBuilder );
        $("body").click();

        var maxtimeOutMillis = 15000,
            start = new Date().getTime(),
            condition = false,
            interval = setInterval(function() {
                if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                    condition = $("svg.chart").hasClass("done"); //< defensive code
                } else {
                    if(!condition) {
                        clearInterval(interval)
                    } else {
                        window._finishIndicationVariable = true;
                        clearInterval(interval); //< Stop this interval
                    }
                }
            }, 250); //< repeat check every 250ms
    });
    waitFor(function check(){
        return page.evaluate(function(){
            return window._finishIndicationVariable;
        });
    }, function onReady(){
        page.render(drawerPayload.outFile);
        response.statusCode = 200;
        response.close();
    });
} else {
    response.statusCode = 404;
    response.write("Not Found"+url);
    response.close();
}

注意response.close();被使用了两次,因为if分支之一是异步的,而另一个不是。