SVG导出实战:从原理到前端工程化落地的完整方案
SVG导出乱码、空白、样式丢失?我折腾了两天才搞定
上周在搞一个可视化配置工具,用户画完图要能导出 SVG 文件。本来以为就是 outerHTML 一拿,丢给 Blob 就完事了,结果导出来的文件打开全是乱码,或者干脆一片空白,连个影子都没有。更离谱的是,有些样式明明页面上显示正常,导出后直接没了。折腾了快两天,踩了一堆坑,最后总算搞定了。今天就记一下这个过程,省得下次再翻车。
一开始的“天真”方案:直接 outerHTML
我最开始的想法特别简单:把 SVG 元素整个拷贝出来,转成字符串,然后用 URL.createObjectURL 生成下载链接。代码大概长这样:
const svgElement = document.querySelector('#my-svg');
const svgString = new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'export.svg';
a.click();
看起来没毛病对吧?但实际导出的文件,用浏览器打开要么是白屏,要么文字变成一堆方块(比如中文全变 □□□),更诡异的是,有些内联样式压根没生效。我一度怀疑是不是浏览器兼容性问题,换 Chrome、Safari、Firefox 都试了,结果一样——说明不是浏览器的问题,是我漏了啥。
第一个坑:没声明 XML 和编码
后来我对比了别人能正常打开的 SVG 文件,发现开头都有一行:
<?xml version="1.0" encoding="UTF-8"?>
而我导出的字符串根本没有这行!SVG 虽然是 XML 的一种,但如果你不显式声明编码,很多编辑器或浏览器会默认用其他编码(比如 ISO-8859-1)去解析,中文自然就乱码了。赶紧加上:
const svgString = new XMLSerializer().serializeToString(svgElement);
const fullSvgString = <?xml version="1.0" encoding="UTF-8"?>n${svgString};
这下中文不乱了,但样式还是有问题。有些用 CSS 类写的样式完全没应用上。
第二个坑:CSS 样式没内联
我页面上的 SVG 是用 <style> 标签配合 class 写样式的,比如:
<svg id="my-svg">
<style>
.label { fill: #333; font-family: "PingFang SC", sans-serif; }
</style>
<text class="label" x="10" y="20">你好</text>
</svg>
但导出后,虽然 <style> 标签还在,但很多软件(比如 Illustrator、甚至某些浏览器)根本不认 SVG 里的 <style>,只认内联的 style 属性或 presentation attributes(比如 fill、font-family 直接写在元素上)。
这里我踩了好几次坑。一开始想用 getComputedStyle 去读每个元素的最终样式,然后手动转成属性。但 SVG 的样式属性和 CSS 不完全一一对应,比如 font-family 在 SVG 里可以直接用,但 color 就不行,得用 fill。而且 getComputedStyle 返回的是计算后的值,像 font-family 可能变成 "PingFang SC", "Microsoft YaHei", sans-serif,但 SVG 里如果字体名带空格又不加引号,可能解析失败。
后来试了下发现,其实最稳妥的方式是:**在导出前,把所有需要的样式都写成内联属性**。但这对现有项目改动太大。有没有自动化方案?
终极方案:递归遍历 + 内联关键样式
我最后采用了一个折中办法:导出前,深拷贝整个 SVG 元素,然后遍历所有子节点,把常用的 presentation attributes 从 computed style 里提取出来,写成属性。只处理几个关键属性,比如 fill、stroke、font-family、font-size、opacity 等。
注意:不能直接改原始 DOM,所以先 clone 一份:
function inlineSvgStyles(svgElement) {
const clonedSvg = svgElement.cloneNode(true);
// 获取所有需要处理的元素(排除 <style>, <defs> 等)
const elements = clonedSvg.querySelectorAll('*');
elements.forEach(el => {
if (el.tagName === 'style' || el.tagName === 'defs') return;
const computedStyle = window.getComputedStyle(el);
// fill
const fill = computedStyle.fill;
if (fill && fill !== 'none' && !el.hasAttribute('fill')) {
el.setAttribute('fill', fill);
}
// stroke
const stroke = computedStyle.stroke;
if (stroke && stroke !== 'none' && !el.hasAttribute('stroke')) {
el.setAttribute('stroke', stroke);
}
// font-family
const fontFamily = computedStyle.fontFamily;
if (fontFamily && !el.hasAttribute('font-family')) {
// 清理掉多余的引号和空格,但保留字体名中的空格
let cleanFont = fontFamily.replace(/["']/g, '');
el.setAttribute('font-family', cleanFont);
}
// font-size
const fontSize = computedStyle.fontSize;
if (fontSize && !el.hasAttribute('font-size')) {
el.setAttribute('font-size', fontSize);
}
// opacity
const opacity = computedStyle.opacity;
if (opacity !== '1' && !el.hasAttribute('opacity')) {
el.setAttribute('opacity', opacity);
}
});
return clonedSvg;
}
然后导出流程改成:
const svgElement = document.querySelector('#my-svg');
const styledSvg = inlineSvgStyles(svgElement);
const svgString = new XMLSerializer().serializeToString(styledSvg);
const fullSvgString = <?xml version="1.0" encoding="UTF-8"?>n${svgString};
const blob = new Blob([fullSvgString], { type: 'image/svg+xml;charset=utf-8' });
// ...后续下载逻辑
注意 Blob 的 type 里我也加了 charset=utf-8,双重保险。
还有个小问题:字体 fallback 不生效
虽然加了 font-family,但如果用户电脑没有 “PingFang SC”,理论上应该 fallback 到 sans-serif。但在很多 SVG 查看器里,fallback 链会被忽略,直接显示系统默认字体(可能很丑)。这个问题我暂时没解决,因为 SVG 规范本身对字体支持就比较弱。我的 workaround 是:在导出前,尽量用通用字体(比如 Arial, sans-serif),或者干脆把文字转成 path(但这就超出本文范围了)。
目前这个方案在 Chrome、Safari、Firefox 导出后都能正常显示,用 Adobe Illustrator 打开也基本 OK(除了个别字体问题)。算是达到了可用状态。
核心代码就这几行(整合版)
把上面的逻辑整合成一个函数,方便复用:
function downloadSvgAsFile(svgElement, filename = 'export.svg') {
// 深拷贝并内联样式
const clonedSvg = svgElement.cloneNode(true);
const elements = clonedSvg.querySelectorAll('*');
elements.forEach(el => {
if (['style', 'defs', 'script'].includes(el.tagName.toLowerCase())) return;
const computedStyle = window.getComputedStyle(el);
const setAttrIfNotExists = (attr, value) => {
if (value && value !== 'none' && !el.hasAttribute(attr)) {
el.setAttribute(attr, value);
}
};
setAttrIfNotExists('fill', computedStyle.fill);
setAttrIfNotExists('stroke', computedStyle.stroke);
setAttrIfNotExists('font-family', computedStyle.fontFamily.replace(/["']/g, ''));
setAttrIfNotExists('font-size', computedStyle.fontSize);
if (computedStyle.opacity !== '1') {
setAttrIfNotExists('opacity', computedStyle.opacity);
}
});
// 序列化
const svgString = new XMLSerializer().serializeToString(clonedSvg);
const fullSvgString = <?xml version="1.0" encoding="UTF-8"?>n${svgString};
// 创建下载
const blob = new Blob([fullSvgString], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
调用起来就一行:
downloadSvgAsFile(document.querySelector('#my-svg'), 'chart.svg');
踩坑提醒:这三点一定注意
- XML 声明和 UTF-8 编码必须加,否则中文乱码跑不掉
- 不要依赖 SVG 内部的
<style>标签,大多数非浏览器环境不支持 - 用
cloneNode(true)而不是直接操作原 DOM,避免副作用
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有现成的库能自动处理这些?或者对字体 fallback 有更好的处理方式?我目前这个方案虽然 work,但肯定不是最优雅的。后续如果遇到更复杂的场景(比如滤镜、渐变),可能还得再优化。

暂无评论