PhantomJs 2 Headless Chrome

PhantomJS is dead, long live headless browsers

这是一个从PhantomJs走到Headless Chrome的故事,趟过了Highcharts的性能问题的坑,掉入过中文官方文档的错误的坑,尝试过依赖库的源码修改,最后的结果却是发现了Headless Chrome的新大陆。

Highcharts性能问题的解决


事情得从一个bug说起,手头的项目有一个很简单的功能:下载PDF格式的月度报表,报表里有三个scatter chart,这里我们使用的是 Highcharts 去实现的。我们的策略是点击下载的时候,在后台用 PhantomJs 实时生成PDF文件。

PhantomJs 2 Headless Chrome_第1张图片
chart

一切都很完美,稳定性和性能的表现都很好,直到如上图的chart,一共有一万多个点,整个报表有三个这样的chart,总共差不多五万个数据点,这个时候就发现Highcharts的性能直线下降了,结果就是报表因为超时无法下载。
我们的实现如下:

//report-to-pdf.js
var page = require('webpage').create();
var system = require('system');
var address = system.args[1];
var renderTo = system.args[2]

page.viewportSize = { width:1190, height:1684 };

// guarantee the charts loaded!
// https://stackoverflow.com/questions/11340038/phantomjs-not-waiting-for-full-page-load
page.onConsoleMessage = function(msg, lineNum, sourceId) {

    if(msg=='hey lets take screenshot')
    {
        window.setInterval(function(){
            try
            {
                 var sta= page.evaluateJavaScript("function(){ return jQuery.active;}");
                 if(sta == 0)
                 {
                    window.setTimeout(function(){
                        page.render(renderTo);
                        clearInterval();
                        phantom.exit();
                    },1000);
                 }
            }
            catch(error)
            {
                console.log(error);
                phantom.exit(1);
            }
       },1000);
    }
};


page.open(address, function (status) {
    if (status !== "success") {
        console.log('Unable to load url');
        phantom.exit();
    } else {
       page.setContent(page.content.replace('',''), address);
    }
});

#audit_report_controller.rb
  def download
    report = AuditReport.find_by(token: params[:token])
    if report && report.file_path && File.exist?(report.file_path)
      output_file = report.file_path
    else
      file_name = "REPORT_#{report.project_name}_#{report['started_at'].strftime('%Y%m')}_#{params[:token]}.pdf"
      js_path = "#{Rails.root}/app/assets/javascripts/headless-chrome-pdf.js"
      output_file = "#{Rails.root}/tmp/#{file_name}"
      url = audit_report_preview_url(token: params[:token])
       Phantomjs.run(js_path, url, output_file)
      report.update({file_path: output_file})
    end
    send_file(output_file, :filename => file_name, :type => "application/pdf")
  end

这个是我们尝试,用浏览器直接打开用来生成pdf的html页面,发现效率极差:


load time

经过调研,得知Highcharst确实存在效率问题,对于几万个点的scatter它是无能为力的,但是好在有 解决方案,我们可以使用 Highchart-boost module.
这一方案使用WebGL实现图表,替换标准解决方案中的SVG,最终获得了百万点位毫秒级加载的效果。当然需要注意,这也造成了一些效果上的限制:

The boost module is a stripped down version of the SVG renderer. As such, certain features are not available for boosted charts. Most of these features deals with interactivity, such as animation support. But there are a few that relates to visuals as well.

如是我们引入了boost.js, 然后设置了seriesThreshold参数为2000,也就是当点位数量达到2000以上时候,才开启boost 模式。在浏览打开预览页面时候数据如下:


loading time

这是在development环境下,未压缩assets时候的速度,基本chart的渲染无障碍,实现了秒内加载。

Boost && PhantomJs的坑


接下来我们尝试使用PhantomJs将预览页面转化为PDF,在terminal使用命令:

phantomjs report-to-pdf.js http://dev/audit_report_preview\?token\=EeHwf7Gi3fO6S2qzfM test.pdf

果然实现了快速下载,但是结果却是很惨烈的,chart完全没有render出来,chart区白茫茫的一片。WTF!
继续查找问题的所在,想一想boost module对chart的实现方式有什么特点,用chrome打开报表预览页面,查看html元素,可以看到boost.js使用了Embeded Base64 image的方式,使用WebGL画好图片,使用base64 encode之后嵌入页面相应 image tag的Data url属性,怀疑是Embeded image在PhantomJs里打开有问题,搜索了PhantomJs的github issues,确实看到了相关问题,还是个open的issue,已经一年半了,同时也看到PhantomJS 浩浩荡荡的未解决的issues,到写下此段时候,open issues的总共数目是:1812。这让我嗅到了一丝绝望的气息:首先说明PhantomJs确实有渲染Embed image的问题,未解决。同时这个项目可能维护状态不佳。
绝望这下寻求了rails里的其他的一些常用pdf 生成插件,比如:wicked_pdf,pdfkit,但是结果竟然还是一样,生成的PDF里面chart区域白茫茫的一片。此时不得不考虑,真的是因为Embed Base64 image的问题?还是得验证一下问题,于是在浏览器上copy下预览页面的的html,手动设置为report-to-pdf.js 中page的content,生成了PDF之后,竟然发现chart出现了,所以说明,白茫茫的chart的,还真不是embed base64 image的锅,那么可能在PhantomJs环境下,可能chart根本没有画出来,而不是画出来了显示不了,就想使用如下代码查看PhantomJs打印出来的page content:

//Javascript
var page = require('webpage').create();
var address='http://dev/audit_report_preview?token=EeHwf7Gi3fO6S2qzfMm';
page.onConsoleMessage = function(msg, lineNum, sourceId) {
  console.log('I am here: ' + msg)
};

page.open(address, function (status) {
  if (status !== "success") {
    console.log('Unable to load url');
  } else {
    var content = page.content;
    console.log('Content: ' + content);
  }
  phantom.exit();
})

结果发现真的是完全没有image element,更惊喜的是细看之下 ,打印内容第一行竟然给出了error信息:

I am here: Highcharts error #26: www.highcharts.com/errors/26

搜索了一下Highcharts的文档,error#26说的很直白了:

PhantomJs 2 Headless Chrome_第2张图片
error#26 zh

原来是因为PhantomJs不支持 WebGL,在谷歌上做相关搜索,结果确实如此,并且开发者表示,此后也不会支持WebGL。这个时候尝试了上面文档里给出的方案,在boost.js 之后引入了boost-convas.js模块,让在不支持WebGL的时候fallback到canvas,但是结果依然是白茫茫的一片,再次打出content log信息,竟然还是同样的错误,有点怀疑人生了,这次直接点开错误信息给出的链接,走到英文文档的地址,给出的信息是:

PhantomJs 2 Headless Chrome_第3张图片
error#26 en

竟然是要求把boost-canvas.js模块放在boost.js 之前引入,所以这里被坑爹中文文档带偏了!okay,按照文档的要求做了调整,果然不再有#26 error了,不过不幸的是chart只有数轴,但是散点图中的点都没有显示出来,再次回到浏览器预览页面,去做调试,在console里进入源码,修改代码 禁用WebGL,强制fallback到canvas,在浏览器上看到效果也是一样,也是没有画出来散点,然后看到浏览器console里有一些关于boost-canvas.js的报错,是一些类似undefined的初级问题,一一针对性的修改源码做出解决,但是在没有报错情况下依旧没有画出散点,翻看源码,想找出问题所在,同时也好奇google出品的代码质量怎么会这样,如是怀疑是不是版本问题,发现自己使用的Highcharts的版本比较老,和使用的boost 以及 boost-canvas不匹配,更换版本之后终于可以显示正常的chart,性能表现也不错。

PhantomJs is dead


再次回到命令行,尝试服务端PhantomJs生成PDF,结果却是大跌眼镜,浏览器端的飞速性能又没了,依旧是分钟级的时间花费才完成生成动作。此时再回头去求助google,以及翻看PhantomJs的issues,看到很多类似的情况,在PhantomJs中使用canvas的效率低下问题,page.open()在面对体量比较大的页面的时候,效率问题也一直为人所诟病,最后此类讨论给出的结论都是 效率问题在PhantomJs中是无解的,只能去求助别的方案。最让人伤心的是看到了这条issue: Archiving the project: suspending the development,创始人表示PhantomJs已经结束开发了。想起之前尝试几种rails的gem生成PDF方案,效果都不好,感觉开发陷入了困境。

拯救者Headless Chrome


柳暗花明又一村,无意中翻看关于PhantomJs 性能问题的各种相关文章的时候看到 benchmark headless chrome vs phantomjs,想到为啥不去尝试一下Headless Chrome方案呢,按照官方文档做起来尝试,几分钟后发现竟然在这么一条命令下面,完美的解决了问题!

chrome --headless --print-to-pdf=file.pdf http://localhost:3000/dev\?token\=EeHwf7Gi3fO6S2qzfMm

画面渲染正常,chart正常,效率也很好,接下来需要做的就是将Headless Chrome的方案和服务端Rails项目集成的问题:第一个,服务器系统是linux,必须调研下安装使用的问题,第二个是在Rails里调用的话,是否有成熟稳定的方案。
现在有稳定版本的Node API可以使用:

Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome or Chromium.

于是对于第一个问题,默认安装的时候,Puppeteer会一起默认下载安装一个 Chromium内核,同时需要注意对于不同发型版的一些依赖库,我们这里使用的是Ubuntu,可以提前安装好依赖,再安装Puppeteer:

sudo apt-get install  gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
npm instal puppeteer

对于第二个问题,找到一篇guidline,这里的方案是将我们自己的Rails项目文件夹初始化为一个node项目,也就是生成一个package.json和node_module文件夹做依赖管理,这样在部署或者团队开发中,只要有node环境和npm install一下就可以正常使用node api的相关功能,然后在Rails代码中是调用命令行node命令的方式执行。按照这个方案做了集成,开发环境下尝试功能没有问题,剩下的考虑怎么部署问题。我们使用的是mina,在deploy.rb中加入以下任务模块:

namespace :npm do
  desc 'Install node modules using Npm.'
  task install: :environment do
    queue %{echo "-----> Installing node modules using Npm"}
    queue "mkdir -p #{deploy_to}/#{shared_path}/node_modules"
    queue "ln -s #{deploy_to)}/#{shared_path}/node_modules" "node_modules"
    queue "npm install"
  end
end
...
 deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    ....
    invoke :'bundle:install'
    invoke :'npm:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'
    invoke :'deploy:cleanup'

     ....

    end
  end

PhantomJS is dead, long live headless browsers


最后,填好坑回到题记的话题,这几年各种headless的browser崛起,让老一代的server端的解决方案失去了市场,包括 PhantomJSSelenium IDE for Firefox 等,随着 PhantomJs的Maintainer Vitaly Slobodin 宣布不再维护该项目,标志PhantomJs的时代已经远去,使用Headless Chrome等新一代的解决方案有如下优点:

  • They are real browsers with a broad feature support (PhantomJS uses a very old version of WebKit – and in the meanwhile Chrome switched to Blink anyway)
  • They are faster and more stable (PhantomJS has a lot of open issues)
  • They use less memory
  • They can be started non-headless, which allows easier debugging
  • No more goofy PhantomJS binary installation with NPM

这样,我们也可以入了Headless Chrome的坑了,可以用来当做爬虫,用来作为测试的Javascript driver方案等。

  • Generate screenshots and PDFs of pages.
  • Crawl a SPA and generate pre-rendered content (i.e. "SSR").
  • Automate form submission, UI testing, keyboard input, etc.
  • Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features.
  • Capture a timeline trace of your site to help diagnose performance issues.

欢迎入坑!

你可能感兴趣的:(PhantomJs 2 Headless Chrome)