首页 > 解决方案 > 加载附加的 SVG 文件中包含的 javascript 库

问题描述

我正在尝试构建一个基于浏览器的 SVG 光栅器。SVG 文件可以包含影响输出的 javascript 代码(例如,随机更改元素的颜色)并使用库(例如chroma.js)来执行此操作。

我试图实现的过程是:

  1. 加载 SVG 文件
  2. 加载 SVG 文件中链接的库
  3. 执行脚本标签中包含的 javascript
  4. 显示在画布上
  5. 将画布保存为 PNG

这是必要的,因为将 SVG 附加到 HTML 元素不会运行 SVG 中包含的 JS。

一切正常 - 除了加载外部库(第 2 点)。执行此操作的代码段如下

$.get('/some_svg_file.svg', function(d) {
      // this will be replaced with file drag/drop later
      var getLibs = Array.from(d.querySelectorAll('script'))
        .filter(p => p.getAttribute('xlink:href'))
        .map(p => {
          return axios.get(p.getAttribute('xlink:href'))
        })

      // only run embedded js after libraries have been downloaded
      Promise.all(getLibs).then((values) => {
            values.forEach(d => {
              try {
                eval(d.data);
              } catch (e) {
                console.log(e)
              }
            });

            Array.from(d.querySelectorAll('script'))
              .forEach(p => {
                if (p.textContent.length) {
                  try {
                    eval(p.textContent)
                  } catch (e) {
                    console.log(e)
                  }
                }
              })
           // code that writes to canvas etc goes here
     })
})

因此,如果我将链接 chroma.js 放在 html 文件头中,一切正常。如果我下载它eval,SVG文件中的脚本会失败ReferenceError: chroma is not defined

我做的另一个尝试是使用脚本标记技术,如下所示:

$.get('/foo_with_js.svg', function(d) {
      var getLibs = Array.from(d.querySelectorAll('script'))
        .filter(p => p.getAttribute('xlink:href'))
        .map(p => {
          var s = document.createElement('script');
          s.src = p.getAttribute('xlink:href');
          s.type = "text/javascript";
          s.async = false;
          document.querySelector('head').appendChild(s);
        })
   // load scripts and save to canvas
})

它也以同样的方式失败(尽管 chroma.js 被整齐地包含在 head 部分中。

那么我怎样才能让它工作 - 以便我可以加载 SVG,将其附加到 HTML 并在其中运行脚本,而无需预先链接 HTML 中的所有可能脚本?

啊 - 如果你问为什么不使用其他转换过程,答案是“缺乏对 SVG 过滤器的一致支持”

标签: javascriptsvg

解决方案


如果您可以使用嵌入在 SVG 中的任何JavaScript 库使其工作,这是一个很好的问题,但这里我有一个示例,我按照您的建议使用chroma.js 。

对 SVG 文档的引用被添加到<object>. 这里是静态的,但我想它可以动态更新。现在 SVG 正在做它的事情,比如<rect><circle>. 在任何时候,我都可以获取 的 contentDocument (contentDocument.documentElement.outerHTML) <object>,即特定点的 SVG 文档。如果文档中的值发生变化,这将影响 contentDocument。现在我可以获取字符串 (outerHTML) 并将其转换为数据 URI。数据 URI 可以设置为图像对象上的 src,并且图像可以在<canvas>元素。在outerHTML中你可以看到还有对JavaScript库和我写的JavaScript代码的引用,但是当渲染成<canvas>.

SVG 文档:

<?xml version="1.0"?>
<svg viewBox="0 0 200 200" width="200" height="200" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <filter id="drop-shadow">
            <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"/>
            <feoffset in="blur" dx="4" dy="4" result="offsetBlur"/>
            <feMerge>
                <feMergeNode in="offsetBlur"/>
                <feMergeNode in="SourceGraphic"/>
            </feMerge>
        </filter>
    </defs>
    <script href="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.2/chroma.min.js"/>
    <rect x="0" y="0" width="200" height="200" />
    <circle cx="40" cy="40" r="20" fill="blue" style="filter: url(#drop-shadow);"/>
    <script>
        // <![CDATA[
        var cy = 40;
        var speed = 5;
        document.querySelector('rect').style.fill = chroma('hotpink');
        setInterval(function(){
            if(cy > 160 || cy < 40) speed *= -1;
            cy += speed;
            document.querySelector('circle').attributes['cy'].value = cy;
        }, 500);
        // ]]>
    </script>
</svg>

HTML 文档:

<!DOCTYPE html>
<html>
    <head>
        <title>SVG</title>
        <script type="text/javascript">
            var object, canvas, img, loop;

            document.addEventListener('DOMContentLoaded', e => {
                object = document.querySelector('object');
                canvas = document.querySelector('canvas');
                img = new Image();

                img.addEventListener('load', e => {
                    canvas.getContext('2d').drawImage(e.target, 0, 0);
                });

                object.addEventListener('load', e => {
                    loop = setInterval(function(){
                        var svg64 = btoa(e.target.contentDocument.documentElement.outerHTML);
                        var b64Start = 'data:image/svg+xml;base64,';
                        img.src = b64Start + svg64; 
                    }, 500);
                });
            });
        </script>
    </head>
    <body>
        <object type="image/svg+xml" width="200" height="200" data="test.svg"></object>
        <canvas width="200" height="200"></canvas>
    </body>
</html>

这两个文件需要从服务器运行,所以不能直接从文件系统运行。

根据我的测试,SVG 动画CSS 动画在此设置中不起作用。只有 JavaScript 中用于操作 DOM 的“动画”才有效。


推荐阅读