笔者开源了一个 Web 思维导图,在做导出为图片的性能时走了挺多弯路,所以通过本文来记录一下。
思维导图的节点和连线都是通过 svg
渲染的,作为一个纯 js
库,咱们不思考通过后端来实现,所以只能思考如何通过纯前端的形式来实现将 svg
或html
转换为图片。
应用 img 标签联合 canvas 导出
咱们都晓得 img
标签能够显示 svg
,而后 canvas
又能够渲染 img
,那么是不是只有将 svg
渲染到 img
标签里,再通过 canvas
导出为图片就能够呢,答案是必定的。
const svgToPng = async (svgStr) => {
// 转换成 blob 数据
let blob = new Blob([svgStr], {type: 'image/svg+xml'})
// 转换成 data:url 数据
let svgUrl = await blobToUrl(blob)
// 绘制到 canvas 上
let imgData = await drawToCanvas(svgUrl)
// 下载
downloadFile(imgData, '图片.png')
}
svgStr
是要导出的 svg
字符串,比方:
而后通过 Blob 构造函数创立一个类型为 image/svg+xml
的blob
数据,接下来将 blob
数据转换成data:URL
:
const blobToUrl = (blob) => {return new Promise((resolve, reject) => {let reader = new FileReader()
reader.onload = evt => {resolve(evt.target.result)
}
reader.onerror = err => {reject(err)
}
reader.readAsDataURL(blob)
})
}
其实就是 base64
格局的字符串。
接下来就能够通过 img
来加载,并渲染到 canvas
里进行导出:
const drawToCanvas = (svgUrl) => {return new Promise((resolve, reject) => {const img = new Image()
// 跨域图片须要增加这个属性,否则画布被净化了无奈导出图片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = async () => {
try {let canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
let ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, img.width, img.height)
resolve(canvas.toDataURL())
} catch (error) {reject(error)
}
}
img.onerror = e => {reject(e)
}
img.src = svgUrl
})
}
canvas.toDataURL()
办法返回的也是一个 base64
格局的 data:URL
字符串:
最初就能够通过 a
标签来下载:
const downloadFile = (file, fileName) => {let a = document.createElement('a')
a.href = file
a.download = fileName
a.click()}
实现很简略,成果也不错,不过这样就没问题了吗,接下来咱们插入两张图片试试。
解决存在图片的状况
第一张图片是应用 base64
的data:URL
形式插入的,第二张图片是应用一般 url
插入的:
导出后果如下:
能够看到,第一张图片没有问题,第二张图片裂开了,可能你感觉同源策略的问题,但实际上换成同源的图片,同样也是裂开的,解决办法很简略,遍历 svg
节点树,将图片都转换成 data:URL
的模式即可:
// 操作 svg 应用了 @svgdotjs/svg.js 库
const transfromImg = (svgNode) => {let imageList = svgNode.find('image')
let task = imageList.map(async item => {
// 获取图片 url
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 曾经是 data:URL 模式不必转换
if (/^data:/.test(imgUlr)) {return}
// 转换并替换图片 url
let imgData = await drawToCanvas(imgUlr)
item.attr('href', imgData)
})
await Promise.all(task)
return svgNode.svg()// 返回 svg html 字符串}
这里应用了后面的 drawToCanvas
办法来将图片转换成data:URL
,这样导出就失常了:
到这里,将纯 svg
转换为图片就根本没啥问题了。
解决存在 foreignObject 标签的状况
svg
提供了一个 foreignObject
标签,能够插入 html
节点,实际上,笔者就是应用它来实现节点的富文本编辑成果的:
接下来应用后面的形式来导出,后果如下:
明明显示没有问题,导出时 foreignObject
内容却产生了偏移,这是为啥呢,其实是因为默认款式的问题,页面全局革除了 margin
和padding
,以及将 box-sizing
设置成了border-box
:
那么当 svg
存在于文档树中时是没有问题的,然而导出时应用的是 svg
字符串,是脱离于文档的,所以没有这个款式笼罩,那么显示天然会呈现问题,晓得了起因,解决办法有两种,一是遍历所有嵌入的 html
节点,手动增加内联款式,留神肯定要给所有的 html
节点都增加,只给 svg
、foreignObject
或最外层的 html
节点增加都是不行的;第二种是间接在 foreignObject
标签里增加一个 style
标签,通过 style
标签来加上款式,并且只有给其中一个 foreignObject
标签增加就能够了,两种形式看你喜爱哪种,笔者应用的是第二种:
const transformForeignObject = (svgNode) => {let foreignObjectList = svgNode.find('foreignObject')
if (foreignObjectList.length > 0) {foreignObjectList[0].add(SVG(`<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>`))
}
return svgNode.svg()}
导出后果如下:
能够看到,一切正常。
对于兼容性的问题,笔者测试了最新的chrome
、firefox
、opera
、safari
、360 急速浏览器
,运行都是失常的。
踩坑记录
后面介绍的是笔者目前采纳的计划,看着实现其实非常简单,然而过程漫长且崎岖,接下来,开始我的表演。
foreignObject 标签内容在 firefox 浏览器上无奈显示
对于 svg
的操作笔者应用的是 svg.js 库,创立富文本节点的外围代码大抵如下:
import {SVG, ForeignObject} from '@svgdotjs/svg.js'
let html = `<div> 节点文本 </div>`
let foreignObject = new ForeignObject()
foreignObject.add(SVG(html))
g.add(foreignObject)
SVG
办法是用来将一段 html
字符串转换为 dom
节点的。
在 chrome
浏览器和 opera
浏览器上渲染十分失常,然而在 firefox
浏览器上 foreignObject
标签的内容齐全渲染不进去:
查看元素也看不出有任何问题,并且神奇的是只有在控制台元素里编辑一下嵌入的 html
内容,它就能够显示了,百度搜寻了一圈,也没找到解决办法,而后因为 firefox
浏览器占有率并不高,于是这个问题就搁浅了。
应用 img 联合 canvas 导出图片里 foreignObject 标签内容为空
chrome
浏览器尽管渲染是失常的:
然而应用后面的形式导出时 foreignObject
标签内容却是跟在 firefox
浏览器里显示一样是空的:
firefox
能忍这个不能忍,于是尝试应用一些将 html
转换为图片的库。
应用 html2canvas、dom-to-image 等库
应用html2canvas
:
import html2canvas from 'html2canvas'
const useHtml2canvas = async (svgNode) => {let el = document.createElement('div')
el.style.position = 'absolute'
el.style.left = '-9999999px'
el.appendChild(svgNode)
document.body.appendChild(el)// html2canvas 转换须要被转换的节点在文档中
let canvas = await html2canvas(el, {backgroundColor: null})
mdocument.body.removeChild(el)
return canvas.toDataURL()}
html2canvas
能够胜利导出,然而存在一个问题,就是 foreignObject
标签里的文本款式会失落:
这应该是 html2canvas
的一个 bug,不过看它这 issues
数量和提交记录:
指望 html2canvas
改是不事实的,于是又尝试应用dom-to-image
:
import domtoimage from 'dom-to-image'
const dataUrl = domtoimage.toPng(el)
发现 dom-to-image
更不行,导出齐全是空白的:
并且它上一次更新工夫曾经是五六年前,所以没方法,只能回头应用html2canvas
。
起初有人倡议应用 dom-to-image-more,粗略看了一下,它是在 dom-to-image
库的根底上批改的,尝试了一下,发现的确能够,于是就改为应用这个库,而后又有人反馈在一些浏览器上导出节点内容是空的,包含 firefox
、360
,甚至chrome
之前的版本都不行,笔者只能感叹,太难了,而后又有人倡议应用上一个大版本,能够解决在 firefox
上的导出问题,然而笔者试了一下,在其余一些浏览器上仍旧存在问题,于是又在思考要不要换回html2canvas
,尽管它存在肯定问题,但至多不是齐全空的。
解决 foreignObject 标签内容在 firefox 浏览器上无奈显示的问题
用的人多了,这个问题又有人提了进去,于是笔者又尝试看看能不能解决,之前始终认为是 firefox
浏览器的问题,毕竟在 chrome
和opera
上都是失常的,这一次就想会不会是 svgjs
库的问题,于是就去搜它的 issue
,没想到,还真的搜进去了 issue,粗心就是因为通过SVG
办法转换的 dom
节点是在 svg
的命名空间下,也就是应用 document.createElementNS
办法创立的,导致局部浏览器渲染不进去,归根结底,这还是不同浏览器对于标准的不同实现导致的:
你说 chrome
很强吧,的确,然而无形中它阻止了问题的裸露。
晓得了起因,那么批改也很简略了,只有将 SVG
办法第二个参数设为 true
即可,或者本人来创立节点也能够:
foreignObject.add(document.createElemnt('div'))
果然,在 firefox
浏览器上失常渲染了。
解决 img 联合 canvas 导出图片为空的问题
解决了在 firefox
浏览器上 foreignObject
标签为空的问题后,天然会狐疑之前应用 img
联合 canvas
导出图片时 foreignObject
标签为空会不会也是因为这个问题,同时理解了一下 dom-to-image
库的实现原理,发现它也是通过将 dom
节点增加到 svg
的foreignObject
标签中实现将 html
转换成图片的,那么就很搞笑了,我自身要转换的内容就是一个嵌入了 foreignObject
标签的 svg
,应用dom-to-image
转换,它会再次把传给它的 svg
增加到一个 foreignObject
标签中,这不是套娃吗,既然 dom-to-image-more
能通过 foreignObject
标签胜利导出,那么不必它必然也能够,到这里根本确信之前不行就是因为命名空间的问题。
果然,在去掉了 dom-to-image-more
库后,从新应用之前的形式胜利导出了,并且在 firefox
、chrome
、opera
、360
等浏览器中都不存在问题,兼容性反而比 dom-to-image-more
库好。
总结
尽管笔者的实现很简略,然而 dom-to-image-more
这个库实际上有一千多行代码,那么它到底多做了些什么呢,点个关注,咱们下一篇文章再见。