前端截图 将 DOM 转换为图片 魔改 html-to-image 源码

需求

将这个 DOM 转换成 PNG

  • 需要将圆角转换为直角
  • 且需要去除容器内部文本元素的边框

DOM:

图片:

方案

使用 html-to-image js 库,地址:github.com/bubkoo/html...

选择这个库的原因是因为这个库比较新(相对于其他库来说),还有很多其他类似的库可供选择:

截图方法 screenshot 的代码:

js 复制代码
// 截图
async function screenshot(dom) {
    try {
        // 动态加载 html-to-image 库
        const {
            toPng
        } = await import('html-to-image');

        // 截图
        toPng(dom, {
            quality: 1,
            style: {
                borderRadius: 0
            }
        })
            .then(dataUrl => {
                console.log('dataUrl', dataUrl);
            });
    } catch (error) {
        console.error('oops, something went wrong!', error);
    }
}

通过 style 参数可以控制最外层的 DOM 的样式,传入 borderRadius: 0 即可解决圆角转换为直角的问题。

但是并不能控制里面的 DOM 元素的样式。

所以为了解决去除容器内部文本元素的边框这个问题,有两种方案:

  1. 文本元素的边框,不再使用父元素的 border 属性实现;而是单独使用一个绝对定位的子元素实现;这样可以通过 html-to-image 的 filter API 来过滤;即将边框单独作为一个子元素实现,和文本元素同层。

    • 至于为什么不直接过滤是因为过滤掉父元素子元素也会被一起过滤
    • 这个需要改现有的实现方式
  2. 修改 html-to-image 源代码,增加能控制子元素样式的方法;或将 style 应用于所有元素而不只是最外层 DOM

感觉第一个方案比较 trick;这次是边框能这么改,如果以后是其他呢?不是很通用也没啥挑战性

直接改 html-to-image 源码吧!

修改 html-to-image 源码,增加自定义样式的功能,而不仅限于根元素

首先 fork 一份代码到自己的 github 里

github.com/BadWaka/htm...

clone 下来,看下逻辑

首先在 package.json 里看入口文件

打开 index.js 文件

找到 toPng 方法

可以看到是先调用了 toCanvas 方法,获取到 Canvas 对象,再调用 canvas 的 toDataURL 转换成图片 base64 字符串

看一下 toCanvas 方法

  • 首先调用 getImageSize 方法获取图片宽高
  • 然后调用 toSvg 方法获取 svg 图片
  • 接着调用 createImage 创建图片元素
  • 创建 canvas 对象
  • 计算比例 ratio
  • 设置 canvas 宽高
  • 判断是否跳过自动缩放 if (!options.skipAutoScale)
  • 判断是否有背景色 if (options.backgroundColor)
  • 调用 drawImage 在 canvas 上绘制图片

整个流程还是很清晰的

着重看一下这个 svg 图片是怎么获取的,看下 toSvg 方法的实现

  • 首先也是计算宽高
  • 然后调用 cloneNode 方法复制 DOM 节点 node
  • 嵌入 web 字体
  • 嵌入图片
  • 应用样式
  • 调用 nodeToDataURL 方法把 node 转换成 datauri base64 字符串

所以 html-to-image 内部其实已经进行过一次 cloneNode 了

  • 源码中会判断元素类型是否是 Canvas 或者是 Video 或者是 IFrame 以进行不同的处理
  • 如果是普通 DOM 节点会调用 node.cloneNode 方法,且参数是 false 浅复制,不复制子节点

猜测传入的 style 参数会在 applyStyle 这个方法里调用

全局搜一下 options.style

果然是在这里

观察 applyStyle 方法的实现

  • 首先从 cloneNode 中取出 style 对象
  • 判断背景色和宽高并赋值
  • 遍历传入的 options.style 的每一个 key,并将其值赋值给 node.style,这个 node 是 cloneNode,也就是根元素

开始调试

调试方法为直接修改 node_modules/html-to-image/es/apply-style.js 文件

我是使用的 vite 作为构建工具,直接修改 node_modules 下面的文件不会直接生效,需要手动删除缓存 rm -rf node_modules/.vite

(当然还有其他方法,我是懒得搞了,直接删了缓存重启开发服务完事)

修改思路

在 toPng 的第二个参数 options 配置项里新增一个 preprocess 预处理方法,用于在把 clonedNode 添加到 canvas 之前修改它的样式

js 复制代码
        toPng(dom, {
            // 参数为 clonedNode
            preprocess: (clonedNode) => {
                // 修改以后记得把 clonedNode 返回给 html-to-image
                return clonedNode;
            }
        })
            .then(dataUrl => {
                console.log('dataUrl', dataUrl);
            });

这样我们在业务侧就可以拿到 clonedNode 并进行样式修改,随便怎么改样式,自由度很高

在设计 API 的时候需要考虑其扩展性和兼容性,以及 API 是否合理。有很多其他的方法也可以满足需求,但我个人觉得添加一个预处理方法比较通用

例如:还可以将 style 应用到每个子节点上;但这种方式就不太合理,每个元素的样式本来就是不同的,都覆盖是不符合用户通用认知的,所以就废弃了这种想法

在 html-to-image 的 index.ts 里

在 toSvg 方法里新增判断

  • 首先把 clonedNode 由 const 改为 let
  • 接着判断下用户是否传了 preprocess 方法
  • 如果用户传了,就调用这个方法,把 clonedNode 传进去并将返回值赋值给 clonedNode

这样 html-to-image 的源码就修改完成了!是不是很简单

而且通过新增 API 的方式,只要做好判断,就能大大降低对现有功能的影响,避免影响到已使用的用户,降低线上风险;只需要在文档中标明新增 API 的版本号就好

接下来修改业务侧代码

首先实现一个递归获取 dom 元素所有子元素的方法 getAllChildNodes

js 复制代码
function getAllChildNodes(node) {
    let allNodes = [];
    if (node.hasChildNodes()) {
        node.childNodes.forEach(child => {
            allNodes.push(child);
            allNodes = allNodes.concat(getAllChildNodes(child));
        });
    }
    return allNodes;
}

接着扩写 preprocess 方法

js 复制代码
        toPng(dom, {
            // 参数为 clonedNode
            preprocess: (clonedNode) => {
                // 拿到所有子元素,放入 children 数组中
                const children = getAllChildNodes(clonedNode);
                // 遍历 children 数组
                children.forEach(node => {
                    // 找到带边框的 dom 节点
                    if (
                        node
                        && node.classList
                        && node.classList.value === 'editable draggable textbox'
                    ) {
                        // 通过 js 设置 style 的方式把边框颜色透明度设为 0
                        node.style.borderColor = 'rgba(255, 255, 255, 0)';
                    }
                });
                // 修改以后记得把 clonedNode 返回给 html-to-image
                return clonedNode;
            }
        })
            .then(dataUrl => {
                console.log('dataUrl', dataUrl);
            });

测试下截图效果

完美!边框已经被去除了

接下来的事就是怎么使用修改后的 html-to-image 了

有三种方法:

  1. 发布 pr,等 html-to-image 的作者审核完,中途可能会经过很多沟通交流和修改,最终合并至 html-to-image 官方库的 master,成为一个大家都可以使用的正式功能

    • 这种当然是最好的,但是周期会很长,也需要付出很多额外的精力,例如英文和作者沟通什么的;推荐有额外精力的去试试,能很大提升对开源社区的参与度
  2. 发布自己修改后的 npm 包版本

    • 一般是这种做法,因为开源协议为 MIT,自由度很高,所以可以基于原始的版本发布任意自定义的版本,改个名字就行
    • 如果是公司项目的话,可以只把 npm 包发在内网,并且在内网建一个 fork 版本的代码库,与同事们一起维护
  3. 直接编译,把编译后的代码内嵌到业务代码中

    • 这种比较简单粗暴,优点是很简单,缺点是不方便维护

按需求自由选择即可

好啦这篇文章就到这,感谢观看!


附:一些被排除的方案问题记录

  1. 为什么不能先改变样式,再截图?

    • 这样会导致样式变更,用户会看到变化,不想让用户看到变化
  2. 先将要截图的 DOM 使用 cloneNode 复制,再将 clone 后的 Node 放入页面中不让用户看到,再截图可以吗?

    • 试过这种方法,但尝试了很多布局方式,截图都是灰色的色块,没有成功
    • 以下是一部分测试代码
js 复制代码
async function screenshot() {
    try {
        // 动态加载 html-to-image 库
        const {
            toPng
        } = await import('html-to-image');
        console.log('container.value', container.value);

        // 深复制 dom 节点,因为截图需要去除圆角,去除文案边框,去除更换图片按钮,所以需要改变 dom 样式
        // 为了避免影响当前展示效果,需要 cloneNode
        // const clonedDom = container.value.cloneNode(true);
        // console.log('clonedDom', clonedDom);

        // 获取目标元素的样式
        // const computedStyle = window.getComputedStyle(container.value);
        // Array.from(computedStyle).forEach((key) => {
        //     clonedDom.style[key] = computedStyle[key];
        // });

        // 将克隆的 DOM 添加到文档中(隐藏状态)
        // clonedDom.style.position = 'absolute';
        // clonedDom.style.left = '100vw';
        // clonedDom.style.top = '100vh';
        // editor.value.appendChild(clonedDom);

        // 保存原始样式
        // const originalborderRadius = container.value.style.borderRadius;

        // 修改样式
        // clonedDom.style.borderRadius = '0';
        // container.value.style.borderRadius = '0';

        // setTimeout(async () => {
        //     // 截图
        //     const dataUrl = await domtoimage.toPng(clonedDom);
        //     console.log('dataUrl', dataUrl);
        // }, 300);

        // setTimeout(() => {
        // 截图
        const startTime = new Date().getTime();
        toPng(container.value, {
            quality: 1,
            // height: props.imgHeight,
            // width: props.imgWidth,
            // canvasHeight: props.imgHeight,
            // canvasWidth: props.imgWidth,
            style: {
                borderRadius: 0,
                border: null
            },
            filter: (node) => {
                console.log('filter node', node.classList);
                // if (node && node.classList && node.classList.value === 'editable draggable textbox') {
                //     return false;
                // }
                // const clonedDom = node.cloneNode(true);
                // if (clonedDom && clonedDom.style) {
                //     clonedDom.style.fontSize = '10px';
                // }
                return true;
            }
        })
            .then(dataUrl => {
                console.log('dataUrl', dataUrl);

                // 恢复样式
                // container.value.style.borderRadius = originalborderRadius;

                // 计算耗时
                const endTime = new Date().getTime();
                console.log('diffTime', endTime - startTime);
            });
        // }, 5000);

    } catch (error) {
        console.error('oops, something went wrong!', error);
    }
}
相关推荐
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9152 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼3 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風7 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#