UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • WebPack

  • 浏览器相关

  • 工程化相关

  • 工作中遇到的问题以及解决方案

  • Git

  • Vite

  • 一些小工具

    • 二次封装axios
    • 妙用svg实现dom转image
      • 前言
      • XML和HTML
      • html转xml
      • 弊端
        • 将class样式转内联样式
        • 将网络资源转为base64格式
      • 结语
      • 参考文献
    • 小程序实现全局状态管理
  • 算法

  • 服务器

  • HTTP

  • 技术
  • 一些小工具
hanhanbuku
2023-11-01
目录

妙用svg实现dom转image

# 前言

移动端和pc端偶尔都会遇到需要生成海报的场景,通常此类场景都是通过将html转成canvas然后通过canvas去转base64拿到最终生成的图片。流程大概是html→canvas→base64。下面介绍一种与众不同的方案来实现这个功能

当前痛点

此前实现上述功能一般都是使用html2canvas这个库,他就是将dom元素一个一个的绘制到canvas上然后生成图片,这种方法需要适配非常非常多的情况。所以导致他的代码包特别的大,如果我们能通过更轻量的方式去实现那就再好不过了。

启发

偶然间看到一篇文章,关于 web截屏功能 (opens new window) 的,文中提到了img标签支持解析svg,并且svg可以内嵌xml。这样一来我们的问题不就得到解决了嘛,只需要将html转为xml内嵌到svg中,然后用img解析svg,最终就可以通过canvas去渲染img图片并完成截图了。

# XML和HTML

下面先科普一下什么是xml,他和html有什么区别

XML即ExtentsibleMarkup Language(可扩展标记语言),是用来定义其它语言的一种元语言,其前身是SGML(标准通用标记语言)。它没有标签集(tagset),也没有语法规则(grammatical rule),但 是它有句法规则(syntax rule)。任何XML文档对任何类型的应用以及正确的解析都必须是良构的(well-formed),即每一个打开的标签都必须有匹配的结束标签,不得含有次序颠倒的标签,并且在语句构成上应符合技术规范的要求。XML文档可以是有效的(valid),但并非一定要求有效。所谓有效文档是指其符合其文档类型定义(DTD)的文档。如果一个文档符合一个模式(schema)的规定,那么这个文档是模式有效的(schema valid)。

xml和html的区别主要有以下几点

  • 语法要求不同
  • 标记不同
  • 作用不同

具体的这里就不描述了,可以自行查阅资料。重要的是html可以转为xml,并且svg可以渲染xml。下面我们就去找找如何将html转为xml。

# html转xml

根据MDN记载,html和xml是支持相互转换的, 文档 (opens new window)

文中有提到XMLSerializer接口提供serializeToString() (en-US) 方法来构建一个代表 DOM 树的 XML 字符串

接下来就是拼接一下svg标签,将转译好的xml插入到svg中

<!DOCTYPE html>
<html>
  <head>
    <title>渲染SVG字符串</title>
  </head>
  <body>
    <div id="render" style="width: 100px; height: 100px; background: red"></div>
    <br />
    <div id="svg-container">
      <!-- 这里是将SVG内容渲染到<img>标签中 -->
      <img id="svg-image" alt="SVG图像" />
    </div>

    <script>
      const perfix =
        "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>";
      const surfix = "</foreignObject></svg>";

      const render = document.getElementById("render");

      render.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");

      const string = new XMLSerializer()
        .serializeToString(render)
        .replace(/#/g, "%23")
        .replace(/\n/g, "%0A");

      const image = document.getElementById("svg-image");

      const src = perfix + string + surfix;

      console.log(src);

      image.src = src;
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这一步比较简单,所以代码我就直接copy参考文献中的例子了。其实到这一步,就基本实现我们的需求了。只不过svg内嵌xml并不是万能的,他也有以下几个弊端。

# 弊端

  • 只能识别内联样式
  • 无法识别通过http请求加载的资源,如图片,字体等。

以上两个问题都是比较致命且严重影响使用的,下面我们来针对讲一下解决办法

# 将class样式转内联样式

既然只能识别内联样式,那我们就想办法将class的样式都提取出来,内联到标签上

    function setDomLineStyles(node) {
        return new Promise((resolve) => {
            const parentDom = document.querySelector(node)
            // 获取所有样式表
            const styles = document.styleSheets 
            // 遍历样式表
            for (let key in styles) { 
                const {cssRules} = styles[key]
                //遍历样式表中存储样式的对象
                for (let cssKey in cssRules) {
                    //获取到样式对象以及这个样式对应的dom选择器
                    const {style, selectorText} = cssRules[cssKey]
                    //查询出所有符合选择器的dom
                    const domList = document.querySelectorAll(selectorText)
                    //判断dom是否处于目标区域内,或者是否是目标本身
                    if (parentDom === domList[0] || parentDom.contains(domList[0])) {
                        //遍历获取到的所有dom元素
                        domList.forEach(d => { 
                            //遍历样式表
                            for (let s in style) {
                                //因为样式表中获取到的是所有的样式,但你没设置的样式是空值,我们可以借此过滤
                                if (style[s]) {
                                    //将满足条件的样式内联到标签上
                                    d.style[s] = style[s]
                                }
                            }
                        })
                    }
                }
            }
            resolve()
        })
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

上面的代码主要是做了获取到页面所有的样式表(包括外部加载的css等),然后对目标区域的元素进行内联样式的设置。注释已经写的很明白了

# 将网络资源转为base64格式

svg虽然无法识别网络资源,但是可以识别base64。因此,我们可以将网络图片转为base64格式设置到img标签上。

 function getUrlBase64(Node, ext = 'png') {
        return new Promise(async (resolve, reject) => {
            // 先将所有样式转为内联样式
            await setDomLineStyles(Node)
            console.log('样式添加完毕,开始转图片')
            //获取跟标签内所有img元素
            const imgList = document.querySelectorAll('#app img')
            var canvas = document.createElement("canvas");   //创建canvas DOM元素
            var ctx = canvas.getContext("2d");
            var img = new Image;
            img.crossOrigin = 'Anonymous';
            let i = 0
            function loadImg (i){
                img.src = imgList[i].src;
                img.onload = function () {
                    canvas.height = imgList[i].height;
                    canvas.width = imgList[i].width;
                    ctx.drawImage(img, 0, 0, imgList[i].width, imgList[i].height);
                    imgList[i].src = canvas.toDataURL("image/" + ext);
                    i++
                    if (i === imgList.length) {
                        canvas = null;
                        resolve()
                    }else{
                        loadImg(i++)
                    }
                };
                img.onerror = function () {
                    i++
                    console.warn('有图片加载失败,序号:' + i)
                    if (i === imgList.length) {
                        canvas = null;
                        resolve()
                    }else{
                        loadImg(i++)
                    }
                }
            }
            loadImg(i)
        })
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

代码比较简单,就是获取目标区域所有的img标签,然后逐一加载他们。最终全部转成base64后将promise置为成功。

解决了上述两个棘手的问题,我们的html to img就大功告成啦。下面贴一下完整可运行的代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #app {
            width: 200px;
            height: 200px;
            background: aqua;
        }

        #app > div {
            color: #fff;
        }

        .image {
            width: 150px;
            height: 150px;
        }

        #ss {
            border-radius: 50px;
            color: red;
            width: 30px;
            height: 30px
        }
        .s2-1{
            width: 30px;
            height: 30px
        }
    </style>
</head>
<body>
<div id="app">
    <div>1233654</div>
    <div>666 <img class="s2-1" src="1.png"></div>
    <img id="ss" src="1.png">
</div>
<img src="" id="image">
<script>
    function setDomLineStyles(node) {
        return new Promise((resolve) => {
            const parentDom = document.querySelector(node)
            const styles = document.styleSheets // 获取所有样式表
            for (let key in styles) {
                const {cssRules} = styles[key]
                for (let cssKey in cssRules) {
                    const {style, selectorText} = cssRules[cssKey]
                    const domList = document.querySelectorAll(selectorText)
                    if (parentDom === domList[0] || parentDom.contains(domList[0])) {
                        domList.forEach(d => {
                            for (let s in style) {
                                if (style[s]) {
                                    d.style[s] = style[s]
                                    console.log('在添加样式')
                                }
                            }
                        })
                    }
                }
            }
            resolve()
        })
    }

    function getUrlBase64(Node, ext = 'png') {
        return new Promise(async (resolve, reject) => {
            // 先将所有样式转为内联样式
            await setDomLineStyles(Node)
            console.log('样式添加完毕,开始转图片')
            //获取跟标签内所有img元素
            const imgList = document.querySelectorAll('#app img')
            var canvas = document.createElement("canvas");   //创建canvas DOM元素
            var ctx = canvas.getContext("2d");
            var img = new Image;
            img.crossOrigin = 'Anonymous';
            let i = 0
            function loadImg (i){
                img.src = imgList[i].src;
                img.onload = function () {
                    canvas.height = imgList[i].height;
                    canvas.width = imgList[i].width;
                    ctx.drawImage(img, 0, 0, imgList[i].width, imgList[i].height);
                    imgList[i].src = canvas.toDataURL("image/" + ext);
                    i++
                    if (i === imgList.length) {
                        canvas = null;
                        resolve()
                    }else{
                        loadImg(i++)
                    }
                };
                img.onerror = function () {
                    i++
                    console.warn('有图片加载失败,序号:' + i)
                    if (i === imgList.length) {
                        canvas = null;
                        resolve()
                    }else{
                        loadImg(i++)
                    }
                }
            }
            loadImg(i)
        })
    }

    getUrlBase64('#app').then(res => {
        let s = new XMLSerializer(); // 获取xml对象
        let d = document.getElementById('app');
        d.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
        // 将html转为xml
        let str = s.serializeToString(d).replace(/#/g, "%23")
            .replace(/\n/g, "%0A");
        const src =`data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>${str}</foreignObject></svg>` // 拼接svg内容
        const container = document.getElementById('image')
        container.src = src
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");
        canvas.height = container.height;
        canvas.width = container.width;
        console.log(container.src)
        ctx.drawImage(container, 0, 0, container.width, container.height);
        setTimeout(()=>{
            console.log(canvas.toDataURL("image/" + 'png'))
        })
    })
</script>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

# 结语

上文通过svg可以内嵌xml并且img支持解析svg的优势。实现了一个简易的web端截图工具。但是工具中还有很多边界情况没有考虑,比如转出的图片是否会失真,字体文件如何解决以及一些始料未及的场景。但这个方案却是个比较高效率的方案,大家可以抛砖引玉借此特性去实现更多好玩使用的工具。最后感叹一句canvas真是个好东西,牛逼!

# 参考文献

  • 产品:请给我实现一个在web端截屏的功能! (opens new window)
  • dom-to-image (opens new window)
编辑 (opens new window)
上次更新: 2023/11/01, 17:25:49
二次封装axios
小程序实现全局状态管理

← 二次封装axios 小程序实现全局状态管理→

最近更新
01
小程序实现全局状态管理
07-09
02
前端检测更新,自动刷新网页
06-09
03
swiper渲染大量数据的优化方案
06-06
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式