主流截图方案
前端截图
若采用前端截图的方案,大致过程如下。将html转图片提交,三次兜底上传至服务端,成功后获得回传id,若三次失败,则提示错误。
前端截图的方案有两个缺陷不能避免,第一转图片比较费时(具体查看方案实践);其次提交的过程受网络情况影响。这两点加起来一次过程可能就要2秒以上。
下面说说前端主流的转图片方案。
canvas转换
大致流程如下图,首先克隆dom树(获取开发者定义的样式等),使用iframe重新渲染一次dom,通过getComputedStyle获得最终每个节点的样式,使用StackingContext模拟层叠上下文,最后绘制到画布上(绘制过程有好几种方法,其中一种就是svg转换)。
svg转换
svg中有一个<foreignObject>
,具体介绍可以查阅MDN。它允许包含来自不同的XML命名空间的元素(即xhtml/html)。利用这个特性,只需要将节点样式转换为内联后,用<foreignObject>
包裹即可。其大致流程如下:
webtrc方式
利用webrtc获取媒体流,通过video的srcObject属性播放媒体流,最后在canvas中画出video中的某一帧。
服务端截图
使用无头浏览器,如phantomJS、puppeteer、chrome headless等,访问资源,直接截图。大致流程如下:
前端截图方案实践
目前知名22.8k star开源项目的 html2canvas 就将上述两种方案进行整合,通过修改配置项foreignObjectRendering的值切换转换方式,默认为false,使用第一种。
dom-to-image 7k star开源项目,就是以svg转换为核心。
canvas方式
- 图片生成需要遍历dom树,获得renderLayers和getComputedStyle,从实践结果来看,这种方式速度并不稳定。
- 因为是通过draw的方法画到画布上,在计算定位时会舍弃后面的小数位,比如 width: calc(100px / 3)结果就会省略
svg方式
这种方式平均速度比上一种方式更快,且更加稳定,因为只需要样式内联转换
webrtc方式
- 需要用户授权
- 不太好控制截取的帧
class ScreenShot {
private readonly videoController: HTMLVideoElement
private readonly canvasController: HTMLCanvasElement
private canvasContext: CanvasRenderingContext2D | undefined
constructor() {
this.videoController = document.createElement('video')
this.videoController.autoplay = true
this.canvasController = document.createElement('canvas')
this.canvasController.height = window.innerHeight
this.canvasController.width = window.innerWidth
}
// 开始捕捉屏幕
async startCapture() {
let captureStream = null;
try {
// @ts-ignore
// 捕获屏幕
captureStream = await navigator?.mediaDevices?.getDisplayMedia();
// 将MediaStream输出至video标签
this.videoController.srcObject = captureStream;
} catch (err) {
throw "浏览器不支持webrtc" + err;
}
return captureStream;
}
// 停止捕捉屏幕
stopCapture() {
const srcObject = this.videoController.srcObject;
if (srcObject && "getTracks" in srcObject) {
const tracks = srcObject.getTracks();
tracks.forEach(track => track.stop());
this.videoController.srcObject = null;
}
};
// 截屏
shotPrint() {
// 开始捕捉屏幕
return this.startCapture().then(() => {
return new Promise(resolve => {
setTimeout(() => {
// 获取截图区域canvas容器画布
const context = this.canvasController?.getContext("2d");
if (this.canvasController == null || context == null) return;
// 赋值截图区域canvas画布
this.canvasContext = context;
// 将获取到的屏幕截图绘制到图片容器里
this.canvasContext
?.drawImage(
this.videoController,
0,
0,
this.canvasController?.width,
this.canvasController?.height
);
// 停止捕捉屏幕
this.stopCapture();
resolve(this.canvasController)
}, 300)
})
});
};
}
export default ScreenShot
服务端截图方案实践
使用puppeteer截图,代码demo如下:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
const startTime = Date.now()
console.log('@@@ time start', startTime)
await page.goto(
'https://www.baidu.com',
{
waitUntil: 'networkidle0'
}
);
await page.screenshot({
path: 'baidu.png', fullPage: true});
console.log('@@@ time end', Date.now())
console.log('@@@ time cost', Date.now() - startTime)
console.log('@@@ -----')
await browser.close();
} catch(e) {
console.log(e)
}
})();
- 截图效果:
方案对比
前端截图
- 优点:
- 在用户侧完成截图全过程,只需依赖后端提供的上传接口
- 主要流程前端控制
- 缺点:
- 样式支持率有限,html2canvas支持的样式如下https://html2canvas.hertzen.com/features,可以看到不支持的css属性也比较多
- 互动题的图片比较多,跨域图片需要设置crossOrigin=“anonymous”,且服务端配合支持cors
- 单位精度丢失,在转换过程中一些单位计算会忽略
- 非常依赖用户的设备硬件情况,无法保持稳定
服务端
- 优点
- 克服前端场景下的缺点
- 缺点
- 比较消耗服务器资源,需要扩容