最近钻研了下如何利用JavaScript实现网页截屏,包含在浏览器运行的JS,以及在后盾运行的nodeJs的办法。次要看了以下几个:

  1. PhantomJS
  2. Puppeteer(chrome headless)
  3. SlimerJS
  4. dom-to-image
  5. html2canvas

测试的网页应用了WebGL技术,所以上面的总结会和WebGL相干。

名词定义

headless browser

无界面浏览器,多用于网页自动化测试、网页截屏、网页的网络监控等。

PhantomJS

PhantomJS是能够通过JS进行编程的headless浏览器,应用的是QtWebKit内核。

实现截屏的代码,假如文件名为github.js:

// 创立一个网页实例var page = require('webpage').create();// 加载页面page.open('http://github.com/', function () {  // 给网页截屏,保留到github.png文件中  page.render('github.png');  phantom.exit();})

运行:

phantomjs github.js

一般的页面没有问题,然而如果运行蕴含WebGL的页面,发现截屏不对。通过一些考察,发现不反对WebGL,github issue。

总结:

  1. PhantomJs曾经进行保护了,所以不太倡议持续应用。进行保护的一个起因是chrome公布的headless版本对它造成了肯定冲击。
  2. 不反对WebGL。然而,还是有开发者说能够本人给PhantomJS增加WebGL反对,不过,这个计划目前超出我的常识范畴了,就没有持续钻研。

Puppeteer(chrome headless)

Puppeteer是一个Node库,提供了管制chrome和chromium的API。默认运行headless模式,也反对界面运行。

实现截屏的代码example.js:

const puppeteer = require('puppeteer');(async () => {  const browser = await puppeteer.launch();  const page = await browser.newPage();  await page.setViewport({ // 设置视窗大小    width: 600,    height: 800  });  await page.goto('https://example.com'); // 关上页面  await page.screenshot({path: 'example.png'}); // path: 截屏文件保留门路  await browser.close();})();

运行:

node example.js

接下来看下screenshot办法的实现原理:

screenshot的源码位于lib/cjs/puppeteer/common/Page.js文件中,是一个异步办法:

async screenshot(options = {}) {  // ...  return this._screenshotTaskQueue.postTask(() => this._screenshotTask(screenshotType, options));}async _screenshotTask(format, options) {  // ...  const result = await this._client.send('Page.captureScreenshot', {    format,    quality: options.quality,    clip,  });  // ...}

这个this._client.send又是个什么货色?别急,咱们从新看下Puppeteer的定义:

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol.

看到最初面那个DevTools Protocol了吗?这是个什么货色:

The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers.

具体的解释能够看这篇博客。

简略来说,Puppeteer就是通过WebSocket给浏览器发送遵循Chrome Devtools Protocol的数据,命令浏览器去执行一些操作。而后,浏览器再通过WebSocket把后果返回给Puppeteer。这个过程是异步的,所以看源代码会发现好多async/await。

所以screenshot办法是调用了Chrome Devtools Protocol的captureScreenshot。

总结:

  1. 反对WebGL。
  2. 网页比较复杂的话,截屏工夫也挺长的,我测试的页面是几百毫秒。
  3. Puppeteer是对(CDP)Chrome Devtools Protocol性能的封装。大部分性能都是通过WebSocket传输给CDP解决的。

SlimerJS

SlimerJS和PhantomJS相似。不同点是SlimerJS是基于火狐的浏览器引擎Gecko,而不是Webkit。

SlimerJS能够通过npm装置,最新版本是1.x。不过兼容的火狐版本是53.0到59.0。我看当初火狐最新版本都82了。因为我本机是装置了火狐最新版本的,所以我还得装置一个老版本的火狐,比方59.0。能够参考这篇装置旧版本的火狐浏览器。我是mac零碎,感觉装置还是挺容易的。

实现截屏的代码screenshot.js:

var page = require('webpage').create();page.open("http://slimerjs.org", function (status) {  page.viewportSize = { width:1024, height:768 };  page.render('screenshot.png');});

运行

// mac操作系统设置火狐门路export SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox./node_modules/.bin/slimerjs screenshot.js // 我是部分装置的slimer包

须要留神的是SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox启动的是火狐默认的装置门路,因为我一开始就有火狐浏览器,所以启动的是最新版本的浏览器,而后就报错了,说不兼容。在后面我装置过一个59版本的火狐,那么这个火狐浏览器的门路是什么?

在应用程序外面我把这个旧版本的火狐命名为Firefox59,而后这个门路就是/Applications/Firefox59.app/Contents/MacOS/firefox。从新设置SLIMERJSLAUNCHER为59版本的火狐浏览器之后,发现就能胜利了。

不过,Puppeteer默认会关上浏览器界面,也就是non-headless模式。如果要应用headless模式,能够

./node_modules/.bin/slimerjs --headless screenshot.js

不过,headless模式下,不反对WebGL。

我在写例子的时候,发现的一个显著的不同就是Puppeteer截屏是异步函数,而SlimerJS截屏是同步函数?好奇心驱使下,看了下源码(src/modules/slimer-sdk/webpage.js):

render: function(filename, options) {  // ...  let canvas = webpageUtils.getScreenshotCanvas(    browser.contentWindow,    finalOptions.ratio,    finalOptions.onlyViewport, this);  }  canvas.toBlob(function(blob) {    let reader = new browser.contentWindow.FileReader();    reader.onloadend = function() {      content = reader.result;    }    reader.readAsBinaryString(blob);  }, finalOptions.contentType, finalOptions.quality);  // ...}

webpageUtils.getScreenshotCanvas(src/modules/webpageUtils.jsm):

getScreenshotCanvas : function(window, ratio, onlyViewport, webpage) {  // ...  // create the canvas  let canvas = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");  canvas.width = canvasWidth;  canvas.height = canvasHeight;  let ctx = canvas.getContext("2d");  ctx.scale(ratio, ratio);  ctx.drawWindow(window, clip.left, clip.top, clip.width, clip.height, "rgba(0,0,0,0)");  ctx.restore();  return canvas;}

要害代码就是那行ctx.drawWindow。what?JS原生API还反对间接截屏?
CanvasRenderingContext2D.drawWindow():只有火狐反对,曾经被废除掉的非标准定义的规范API。

总结

  1. 1.0版本反对的火狐版本是53.0到59.0。不保障最新版本火狐可用。
  2. headless模式下,不反对WebGL。

dom-to-image

dom-to-image:前端截屏的开源库。工作原理是:
SVG的foreignObject标签能够包裹任意的html内容。那么,为了渲染一个节点,次要进行了以下步骤:

  1. 递归地拷贝原始dom节点和后辈节点;
  2. 把原始节点以及后辈节点的款式递归的利用到对应的拷贝后的节点和后辈节点上;
  3. 字体解决;
  4. 图片解决;
  5. 序列化拷贝后的节点,把它插入到foreignObject外面,而后组成一个svg,而后生成一个data URL;
  6. 如果想得到PNG内容或原始像素值,能够先应用data URL创立一个图片,应用一个离屏canvas渲染这张图片,而后从canvas中获取想要的数据。

测试的时候,发现内部资源不能加载,所以简略的理解了后就放弃了。

html2canvas

html2canvas。网上查了下感觉有一篇文章写的挺好的:浅析 js 实现网页截图的两种形式。感兴趣的能够看下。

未验证的猜测

尽管前面这两种是前端的实现形式,然而联合后面讲的headless库,也是能够实现后端截屏的。以Puppeteer的API为例,能够首先应用page.addScriptTag(options)往网页中增加前端截屏的库,而后在page.evaluate(pageFunction[, ...args])中的pageFunction函数外面写相应的截屏代码就能够了,因为pageFunction的执行上下文是网页上下文,所以能够获取到document等对象。

总结

对截屏的一个简略钻研,写篇博客整顿下思路。如果文中有叙述谬误的中央,欢送评论留言。