tag: Web; JavaScript; SVG; DOM; 动画 SVG
最近在项目中遇到了「带动画 SVG 图标」与 「image」标签结合使用的场景,使用过程中发现水还是有点深,因此整理出来,供有相似场景的童鞋以参考。
问题背景
我们这里有一个的带动画 SVG 文件
这是一个水波纹效果的 SVG,动画时长是固定的,但是我们希望不同的标记动画的播放时长可以略有不同,进而产生错落交错的感觉。期待效果如下:
其中控制动画时长的属性是写死在 SVG 文件中的(animate 标签的 dur 属性)。SVG 部分内容如下:
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="1.5s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="1.5s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
另外受限于组件要求,仅能使用 image
标签进行加载 SVG。因此只能从 image.src 属性作为突破入口。
解题思路
先确定基本思路:在 SVG 被插入到 image 标签前,修改 SVG 文件中动画 animate 标签的属性 dur,以达到修改动画时间的目的。
那么我们先来看下 image 加载 SVG 文件几种的方式。
1. image 的 src 直接指定文件路径
<img src="./assets/rings.svg">
这种方式加载出来的 SVG 内容没法被 JS 获取到,更不要提修改属性,因此该方案 放弃。
2. 直接加载 base64 或者 Blob URL 字符串
这种方式中,依靠的是 image 标签支持 base64 和 Blob 类型 URL 的特性。
考虑可以先将 SVG 文件转换为对应的字符串,然后通过正则表达式将动画属性修改,然后传入 src 中来实现。不过这种方式需要编写复杂的正则表达式,并且字符串形式的 SVG 内容可读性较差,因此这种方式也 不是最优的。
如果可以先以 dom 形式修改 SVG 的动画属性,再将 dom 转换为字符串,最后再将字符串转换为 base64 或者 Blob 类型,就可以实现以上的需求了。
看起来这个思路靠谱,那么就按照这个方向继续探索。
处理过程
1. 获取 dom
获取 SVG 的方式有很多,但获取方式不在本文重点,故只给出原生 JS 实现方式。
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
const resXML = xhr.responseXML;
const svgDom = resXML.documentElement.cloneNode(true);
});
xhr.open('GET', './rings.svg');
xhr.send();
2. 修改 dom
上文获取的 svgDom 就是可操作的 dom 节点,接下来和操作 dom 一样来操作它。
// 获取 svg 中的 animate 标签,使用 setAttribute 进行修改 dur、begin 等属性,以下代码仅为示例
// 获取 animation 节点
const ani = svgDom.children[0];
// 修改节点上的动画时长 dur 属性
ani.setAttribute('dur', Math.random() + 2 + 's');
3. 转换 domString
接来下便需要将 dom 转换成字符串:通过 XMLSerializer
将 XML 转换成 String 类型。
const svgStr = new XMLSerializer().serializeToString(svgDom);
4. 转换 URL
但是上面的 svgStr 是没法直接传给 image.src 的,需要将其转换为 image 可以解析的 base64 或者 Blob URL 类型。
方案 A:
这里我选择了 Blob 类型进行转换,先用 new Blob([svgStr])
转换成 Blob 类型,再通过 URL.createObjectURL(blob)
方法将字符串转换成 Blob
类型 URL 传入。
const blob = new Blob([svgStr], {
type: 'image/svg+xml'
});
const blobStr = URL.createObjectURL(blob);
const template = `<img src="${blobStr}">`;
// 最后插入模板
方案 B:
当然除了使用 Blob 数据类型外,我们也可以使用window.btoa()
方法将 svgStr 转换为 base64 类型传入。
const base64 = window.btoa(svgStr);
const template = `<img src="data:image/svg+xml;base64,${base64}">`
// 最后插入模板
结论
通过「读入 SVG -> 修改属性 -> 转换 URL -> 传入 image」 这样一个流程,最终实现了我们的预期。
以上思路希望对有类似场景的同学们有所启发。 最后强调的是使用环境为 Chrome 浏览器,其他浏览器未做测试。
参考资料
XMLHttpRequest
SVG Animate
XMLSerializer
Blob
URL.createObjectURL()
Using object URLs to display images
window.btoa()