大家好,我是雷布斯。
今天将给大家介绍网页截图中的常用技术方案,并且会结合一个真实的项目,给大家介绍服务器端截图实现中的部分难点。
相比于第三方截图工具截图,网页截图是集成在系统内部的一项截图功能,能将网页部分区域保存为图片,是用户记录和分享页面信息的有效手段,在各种营销推广等形式的活动页面中尤为常见。在 B端系统
中,网页截图并不是一个非常高频的需求,但在一些特殊场景中使用能够提升用户交互体验。
以下是在业务中遇到的两个场景:
DOM结构 -> 画布 -> 图片
浏览器截图
F12->command+shift+P->screenshot
PS:浏览器截图的方式还有很多,上面只是简单介绍了两种。
用法也很简单:
import html2canvas from 'html2canvas'
export const html2canvasHandle = (eleId) => {
html2canvas(document.getElementById(eleId))
.then((canvas) => {
const imgData = canvas.toDataURL('image/jpeg')
const a = document.createElement('a')
a.href = imgData
a.download = 'test'
a.click()
})
.catch((err) => {
console.log('html2canvas err', err)
})
}
用法也类似:
import domToImage from 'dom-to-image-more'
export const domToImageHandle = (eleId) => {
domToImage
.toPng(document.getElementById(eleId))
.then(function (dataUrl) {
const a = document.createElement('a')
a.href = dataUrl
a.download = 'test'
a.click()
})
.catch(function (error) {
console.error('生成失败', error)
})
}
在某个项目的实践中,我们采用了“服务端截图”的方式实现,下面介绍部分具体细节。
为了还原DOM页面,前后端需要实现 DOM 树的获取和重新构建,我们在内部封装的 web-snapshot
包括以下两个特性:
snapshot
- 用来序列化 DOM 为增量快照import { snapshot } from 'web-snapshot'
const [node] = snapshot(document)
rebuild
- 重新构建 DOM(服务端)由于DOM树节点数量庞大,数据在传输过程中耗时较长,导致截图速度慢,通过 Poko.js
对数据压缩:
let dataString = ''
// 压缩结果为Uint8Array,需要再次组装
const compressedStr = pako.deflate(JSON.stringify(data))
// 组装成新的字符串
for (let i = 0; i < compressedStr.length; ++i) {
dataString += String.fromCharCode(compressedStr[i])
}
const resStr = encodeURIComponent(Base64.encode(dataString))
Inflater
解压缩/**
*
* @param inputByte待解压缩的字节数组
* @return 解压缩后的字节数组
* @throws IOException
*/
public static byte[] uncompress(byte[] inputByte) throws IOException {
int len;
Inflater inflater = new Inflater();
inflater.setInput(inputByte);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] outByte = new byte[1024];
try {
while (!inflater.finished()) {
// 解压缩并将解压缩后的内容输出到字节输出流bos中
len = inflater.inflate(outByte);
if (len == 0) {
break;
}
bos.write(outByte, 0, len);
}
inflater.end();
} catch (Exception e) {
log.error("op=uncompress | desc=解压缩异常", e);
} finally {
bos.close();
}
return bos.toByteArray();
}
Puppeteer
启动无头浏览器@PostConstruct
private void launchChrome() {
try {
// 设置无头浏览器启动参数
LaunchOptions options = new LaunchOptions();
options.setArgs(launchArgs);
options.setHeadless(true);
File chrome = new File(launchPath);
BrowserFetcher.downloadIfNotExist("884014");
// 启动无头浏览器
browser = Puppeteer.launch(options);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException(ApplicationErrorCodeEnum.CHROME_DOWNLOAD_FAIL);
} catch (Exception e) {
log.error("desc=浏览器启动异常", e);
throw new BusinessException(ApplicationErrorCodeEnum.CHROME_START_FAIL);
}
}
webSnapshot
和无头浏览器重新渲染页面 + 截图// 打开新的页面
Page page = browser.newPage();
Viewport viewPort = new Viewport();
viewPort.setDeviceScaleFactor(viewportDpr);
viewPort.setHeight(Integer.parseInt(String.valueOf(viewport.get("height"))));
viewPort.setWidth(Integer.parseInt(String.valueOf(viewport.get("width"))));
// 设置浏览器窗口
page.setViewport(viewPort);
page.setCacheEnabled(true);
ScriptTagOptions scriptTagOptions = new ScriptTagOptions();
File webJs = new File(webJsPath);
scriptTagOptions.setUrl("https://xxxx/screenshot/web-snapshot.min.js");
page.addScriptTag(scriptTagOptions);
// 调用 webSnapshot 重新渲染dom树
String parseNodeFunc = "(node) => {webSnapshot.rebuild(node, document);}";
page.evaluate(parseNodeFunc, Collections.singletonList(map.get("node")));
Thread.sleep(scrollSleep);
ScreenshotOptions screenshotOptions = new ScreenshotOptions();
screenshotOptions.setType("png");
// 调用无头浏览器页面截图功能
String content = page.screenshot(screenshotOptions);
page.close();
工具 | 开发难度 | 性能 | 浏览器兼容性 | 效果 | 推荐使用 |
---|---|---|---|---|---|
html2canvas | 前端 | 取决于节点数量 | 良好 | 部分CSS不兼容,复杂页面需要调整,图片跨域问题(canvas跨域问题) | 还原度要求不高 |
dom-to-image | 前端 | 取决于节点数量 | 不兼容Safari | 复杂页面需要调整,图片跨域问题 | html2canvas的备用方案 |
puppeteer | 前端 + 服务端 | 节点数量 + 服务响应 | 良好 | 高度还原 | 对还原度要求高 |
顺便也给我们的辅导服务打个广告,现在报名支持指定导师哦~