写点什么

JavaScript 实现网页截屏五种方法

作者:锋享前端
  • 2022 年 3 月 18 日
  • 本文字数:3362 字

    阅读完需:约 11 分钟

最近研究了下如何利用 JavaScript 实现网页截屏,包括在浏览器运行的 JS,以及在后台运行的 nodeJs 的方法。主要看了以下几个:


PhantomJS
Puppeteer(chrome headless)
SlimerJS
dom-to-image
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。


总结:


支持WebGL。
网页比较复杂的话,截屏时间也挺长的,我测试的页面是几百毫秒。
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.0 版本支持的火狐版本是 53.0 到 59.0。不保证最新版本火狐可用。

headless 模式下,不支持 WebGL。


dom-to-image


dom-to-image:前端截屏的开源库。工作原理是:


SVG 的 foreignObject 标签可以包裹任意的 html 内容。那么,为了渲染一个节点,主要进行了以下步骤:


递归地拷贝原始 dom 节点和后代节点;

把原始节点以及后代节点的样式递归的应用到对应的拷贝后的节点和后代节点上;

字体处理;

图片处理;

序列化拷贝后的节点,把它插入到 foreignObject 里面,然后组成一个 svg,然后生成一个 data URL;

如果想得到 PNG 内容或原始像素值,可以先使用 data URL 创建一个图片,使用一个离屏 canvas 渲染这张图片,然后从 canvas 中获取想要的数据。


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


html2canvas

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

未验证的猜想

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

- End -

用户头像

锋享前端

关注

还未添加个人签名 2021.10.11 加入

还未添加个人简介

评论

发布
暂无评论
JavaScript实现网页截屏五种方法_锋享前端_InfoQ写作平台