妙用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>
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()
})
}
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)
})
}
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>
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
真是个好东西,牛逼!