业务场景介绍
点击浏览器右上角已安装的chrome插件图标,这个时候会出现一个界面,我们称这个界面为popup,界面上有个"从页面获取产品信息"按钮,单机它会对当前标签页面内容进行截图,最后将截图的图片转成base64发送至xx接口
部分核心代码解读:截取当前可视区域的图片,为了能够截图足够多信息,图片向上衍生300px,向下衍生300px,那图片高度= 300 + 可视区域 + 300
const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const threshold = 300; const scrollY = window.scrollY; const y = scrollY > threshold ? scrollY - threshold : 0; const h =scrollY > threshold? viewportHeight + threshold * 2: viewportHeight + threshold + scrollY; window.html2canvas(document.querySelector('body'), {width: viewportWidth,height: h,x: window.scrollX,y: y,scrollX: 0,scrollY: 0,useCORS: true // 如果有跨域图片})
抛出问题
问题1. 经过测试,概率上百分之99%的页面都能抓取,部分页面不能抓,抓取效果如下图(页面出现了大片空白白板),如:http://www.sinochemheb.com/s/18959-55112-274763.html
解决思路
简单分析一波:调试模式查看页面元素没有什么特殊DOM,也没什么iframe嵌套
猜想1
可能是因为html2canvas插件问题,让gpt给出可平替方案
dom-to-image
这个库也可以将 DOM 元素渲染成图像,支持各种格式(如 PNG、JPEG 等)。它支持的特性包括:背景透明、SVG 图像支持、渐变和阴影等。
官网:https://github.com/tsayen/dom-to-image
canvas2image
canvas2image 是一个将 <canvas> 转换为图像的库。它比较轻量,简单易用,支持将 <canvas> 输出为图片(PNG、JPEG 或 BMP 格式)。
官网:https://github.com/hongru/canvas2image
rasterizeHTML.js
这个库支持将 HTML 页面渲染成图像。它能够解析 HTML 中的样式并将它们转换为一个 Canvas 图像。与 html2canvas 类似,它支持从 DOM 生成图像。
官网:https://github.com/cburgmer/rasterizeHTML.js
puppeteer
puppeteer 是一个更为强大的库,它是一个 Node.js 库,提供了一个高层次的 API 来控制 Chrome 或 Chromium 浏览器。可以用它来截图整个网页或页面的特定部分,并能处理更复杂的渲染问题,支持更复杂的 CSS 和 JavaScript 执行。
官网:https://github.com/puppeteer/puppeteer
html-to-image
这是一个简单的 JavaScript 库,用于将 HTML 元素转换为图像。它基于 canvas,并支持各种格式导出,如 PNG 和 JPEG。与 html2canvas 相比,它的 API 更简洁易用。
尝试性使用:dom-to-image
截图空白区域问题得到了解决,但抛出了更大的问题,百分之90%以上的站点都截图失败
尝试性使用:rasterizeHTML.js
很多UI样式丢失,截出来的图没发看
继续思考
最直接的方法没有效果,那就回归初始状态,研究html2canvas插件原理
为啥选择html2canvas呢?
第一:它截图出来的效果更接近与预期
第二:github的star数更最多,前人栽树后人乘凉的原理,应该没错吧
于是打开 https://github.com/niklasvh/html2canvas,翻了几页查看issues,没有找到自己想要的答案,接着看插件原理
插件底层原理
html2canvas
是一个将 HTML 页面内容转换为画布(Canvas)图像的 JavaScript 库。它通过遍历 HTML 元素,解析元素的样式、字体、颜色等信息,并通过调用 Canvas API 来绘制图像。底层原理包括以下步骤:
- 遍历 DOM 树:
html2canvas
会递归遍历 DOM 元素,分析每个元素的计算样式。 - 计算布局:计算元素的尺寸、位置,处理复杂的布局(如定位、浮动等)。
- 渲染样式:获取元素的背景色、边框、字体等信息,并转化为画布可识别的格式。
- 处理图像和媒体:对于图像、视频等元素,
html2canvas
会加载并绘制到画布上,可能会通过跨域处理(CORS)来获取外部资源。 - 绘制到 Canvas:最终通过 Canvas API 渲染所有计算后的元素,生成最终图像
看到这底层原理,直呼牛逼,原来是将html转成canvas渲染,最终形成图片,那么就接着再去看下要截图的网页,分析下除了没有特殊的iframe之外还有没有特殊的内容,刷了几次页面之后,突然发现点蛛丝马迹
这网站有个从底部向上上升的一个过度效果,看了下元素
于是大胆猜测,插件在执行截图时,将html的计算样式1:1的绘制到了canvas上,而开始时,这些带动画效果的属性是不可见的,只有在出现到可视区域才会触发显示,而canvas显然是没有这个逻辑,于是我在截图之前把这些属性都给去掉或者修改元素属性改为可见,ok,真相了,完美截图了,达到了预期效果
最终的解决方案
导致部分网站截图失败原因是因为html2canvas在往canvas绘制内容时,默认隐藏的内容也会被绘制到canvas上,导致截图出来是空白的,解决方案就是在截图之前把这些隐藏的元素给展现出来,动画的过滤效果去掉,元素显示通用代码如下
const node = document.createTreeWalker(window.document.body,NodeFilter.SHOW_ELEMENT,{acceptNode: function (node: HTMLElement) {// 如果节点是 div 元素,则接受它if (node?.tagName === 'DIV') {return NodeFilter.FILTER_ACCEPT;}return NodeFilter.FILTER_SKIP; // 如果不是 div,则跳过 }} ); let nextNode; while ((nextNode = node.nextNode() as HTMLDivElement)) {const tagName = nextNode?.parentElement?.tagName;if (tagName !== 'SCRIPT' && tagName !== 'STYLE' && tagName !== 'CODE') {nextNode.style.opacity = '1';nextNode.style.visibility = 'visible';nextNode.style.animationDuration = '0s';nextNode.style.animationDelay = '0s';} }
后续思考
除了使用document.createTreeWalker API 之外,动态往页面中插入css样式强制让div显示去除动画效果应该也是可行的