聊一聊网页截图

大家好,我是雷布斯。

今天将给大家介绍网页截图中的常用技术方案,并且会结合一个真实的项目,给大家介绍服务器端截图实现中的部分难点。


相比于第三方截图工具截图,网页截图是集成在系统内部的一项截图功能,能将网页部分区域保存为图片,是用户记录和分享页面信息的有效手段,在各种营销推广等形式的活动页面中尤为常见。在 B端系统 中,网页截图并不是一个非常高频的需求,但在一些特殊场景中使用能够提升用户交互体验。

零、本次分享将会涉及

  • 网页截图的基本流程
  • 几种网页截图的实现 & 对比
  • 网页截图技术选型推荐

一、使用场景

以下是在业务中遇到的两个场景:

  • 分享海报:截取页面中的部分素材进行分享
  • 页面错误截图:用户反馈当前页面的问题时,将当前页面截图上传

二、截图基础准备

  • DOM结构 -> 画布 -> 图片

    • canvas
    • svg
  • 浏览器截图

    F12->command+shift+P->screenshot

    • 浏览器插件
    • 浏览器 DevTools

PS:浏览器截图的方式还有很多,上面只是简单介绍了两种。

三、几种工具截图介绍 & 实现 & 对比

1. html2canvas

用法也很简单:

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)
    })
}

2. dom-to-image

用法也类似:

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)
    })
}

3. 服务端截图 (Puppeteer-无界面Chrome工具)

四、服务端截图案例

在某个项目的实践中,我们采用了“服务端截图”的方式实现,下面介绍部分具体细节。

1. 解析DOM树

为了还原DOM页面,前后端需要实现 DOM 树的获取和重新构建,我们在内部封装的 web-snapshot 包括以下两个特性:

  • snapshot- 用来序列化 DOM 为增量快照
import { snapshot } from 'web-snapshot'

const [node] = snapshot(document)
  • rebuild - 重新构建 DOM(服务端)

2. DOM 树节点数据压缩

由于DOM树节点数量庞大,数据在传输过程中耗时较长,导致截图速度慢,通过 Poko.js 对数据压缩:

  • 普通编码
  • 普通编码的截图速度:
  • 压缩后:体积-81%
  • 压缩后:截图速度:+ 61%
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();
}

3. 启动无头浏览器实现截图

  • 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前端 + 服务端节点数量 + 服务响应良好高度还原对还原度要求高

最后

顺便也给我们的辅导服务打个广告,现在报名支持指定导师哦~