SVG中linearGradient的id冲突的显隐问题深度解析

背景&现状

该文章不介绍SVG相关基础知识和使用的场景,仅作为遇到的linearGradient id相同时引发的表现问题的现象和原理分析。如果大家也遇到了相似的显隐问题,可以参考此文章的解法。

在某次重构中,我们遇到了一个诡异的SVG渲染问题:当一个包含相同渐变ID的SVG组件被 display: none隐藏时,其他同类组件的渐变效果会同步消失

问题分析

一开始我们以为这个是个偶现的问题,且在排查中也怀疑是我们组件写的问题,直到发现了多次渲染的SVG组件会跟着第一个SVG状态同步变化时,我们才定位到可能是SVG本身的渲染机制问题。

问题复现

首先,我们先看一个非常简单的SVG的代码,渲染个渐变的三角形,具体代码如下所示:

ini 复制代码
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="myGradient" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" style="stop-color: red; stop-opacity: 1" />
      <stop offset="100%" style="stop-color: yellow; stop-opacity: 1" />
    </linearGradient>
  </defs>

  <polygon
    points="50,10 90,90 10,90"
    style="fill: url(#myGradient); stroke: black; stroke-width: 2"
  />
</svg>;

可能有些同学会看不懂上面的SVG在做什么,不过这个不是特别重要,大家只需要关注到,我们用的linearGradient标签现在有一个id属性,这个是后面的重点。

现在,我们将这个组件复制2次,得到3个渐变的三角形,具体展示如下:

到这里看起来一切都还非常正常,但是接下来我们就会让大家看到一个"违背认知"的现象。

如果这个时候我们给第一个元素设置display: none,那么第二个元素和第三个元素的渐变也没了,具体表现如下:

大家可以看到,第一个元素因为CSS设置原因,从页面中隐藏,这个是符合我们的认知的,但是为什么第二个和第三个SVG的渐变效果会消失呢?然后还留下了一个黑色的边框。

原理分析

首先,我们一句话解释下这个问题的原因:id 必须在当前SVG文档中是唯一的,不能与其他元素的 id 重复。 fill:url(#id) 会优先使用首个匹配的渐变定义。当首个定义被隐藏( display: none ),所有引用该ID的渐变效果将失效。

我们先解释下:<linearGradient> 是SVG中用于定义线性渐变的元素。它允许你创建一个颜色渐变效果,并可以将其应用到SVG图形(如矩形、圆形、多边形等)的填充或描边中。线性渐变的特点是颜色沿着一条直线(称为渐变线)从一个颜色平滑过渡到另一个颜色。

其中的id是用于唯一标识这个渐变,以便在图形中通过 fill:url(#id) 引用。

如果id存在重复,那么就会出现我们上面遇到的那个问题,所有SVG中同一个id的<linearGradient> 的表现,都会跟第一个渲染的<linearGradient> 一样,大家可以简单理解为所有相同的id的标签都指向了同一个引用。

在这种情况下,如果第一个<linearGradient> 的显隐发生了改变,那么其他的<linearGradient> 的显隐会同步变化,也就出现了我们上面的那个情况:第一个SVG隐藏后,<linearGradient> 也消失了,所以其他SVG里面的<linearGradient> 也变成了消失的状态。

使用visibility: hidden的话,不存在这个问题,因为第一个引用并没有从DOM中移除,不存在找不到的问题。

常见场景

在我们日常使用中,最常见的场景其实就是我遇到的那种,一个需要多次渲染的组件引用了同一个SVG组件,这个组件有没有做特殊的处理,然后我们在组件上使用display: none来进行控制。例如我们常见的多Tab的场景。

例如我现在有2个Tab,每个Tab都是用到了同一个SVG组件来做ICON等,那么当第一个Tab被display: none隐藏时,第二个Tab即使内容展示出来,SVG组件也是不完整甚至是不可见的。具体代码示例如下:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <script>
    document.addEventListener("DOMContentLoaded", () => {
      const btn1 = document.getElementById("btn1");
      const btn2 = document.getElementById("btn2");
      const panel1 = document.getElementById("panel1");
      const panel2 = document.getElementById("panel2");

      btn1.addEventListener("click", () => {
        panel1.style.display = "block";
        panel2.style.display = "none";
      });

      btn2.addEventListener("click", () => {
        panel1.style.display = "none";
        panel2.style.display = "block";
      });
    });
  </script>

  <body>
    <button id="btn1">打开Tab1</button>
    <button id="btn2">打开Tab2</button>
    <div id="panel1" style="display: none">
      <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
        <!-- 定义线性渐变 -->
        <defs>
          <linearGradient id="myGradient" x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" style="stop-color: red; stop-opacity: 1" />
            <stop offset="100%" style="stop-color: yellow; stop-opacity: 1" />
          </linearGradient>
        </defs>

        <!-- 使用渐变填充三角形 -->
        <polygon
          points="50,10 90,90 10,90"
          style="fill: url(#myGradient); stroke: black; stroke-width: 2"
        />
      </svg>
    </div>
    <div id="panel2" style="display: none">
      <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
        <!-- 定义线性渐变 -->
        <defs>
          <linearGradient id="myGradient" x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" style="stop-color: red; stop-opacity: 1" />
            <stop offset="100%" style="stop-color: yellow; stop-opacity: 1" />
          </linearGradient>
        </defs>

        <!-- 使用渐变填充三角形 -->
        <polygon
          points="50,10 90,90 10,90"
          style="fill: url(#myGradient); stroke: black; stroke-width: 2"
        />
      </svg>
    </div>
  </body>
</html>

效果如下:

解决方案

常规解决方案

既然我我们知道了我们需要控制id唯一,那么我们只需要在使用SVG时,给每一次渲染设置一个唯一标记id即可。下面我用我们常见的React组件封装作为例子,来给大家说下这个问题应该怎么解决。

javascript 复制代码
import {v4 as uuid} from 'uuid'; // https://www.npmjs.com/package/uuid
import {useId} from 'React'; // 如果你用的是React 18,那么也可以用React提供的id
const SVGIcon = memo(() => {
    const id = uuid();
    
    return (
        <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
          <defs>
            <linearGradient id={id} x1="0%" y1="0%" x2="100%" y2="100%">
              <stop offset="0%" style="stop-color: red; stop-opacity: 1" />
              <stop offset="100%" style="stop-color: yellow; stop-opacity: 1" />
            </linearGradient>
          </defs>
        
          <polygon
            points="50,10 90,90 10,90"
            style="fill: url(#myGradient); stroke: black; stroke-width: 2"
          />
        </svg>
    );
});

如果大家对React的memo不熟悉的话可能还会问,我们直接memo住这个组件的话,那么前后多次渲染会不会有影响,比如我在多个组件里引用了SVG组件,是不是还会出现id相同的情况。

React组件的memo其实是会根据上下文进行生效,如果你在不同的上下文中同时引入多个SVG组件,那么这个SVG组件是单独的memo实例,不会互相影响的。只有当上下文一样且props浅层比较一致的情况下,才会复用上次的渲染结果(即相同的id)。在这种情况下,是符合我们预期的。

相关推荐
银之夏雪29 分钟前
Vue 3 vs Vue 2:深入解析从性能优化到源码层面的进化
前端·vue.js·性能优化
还是鼠鼠32 分钟前
Node.js 的模块作用域和 module 对象详细介绍
前端·javascript·vscode·node.js·web
拉不动的猪32 分钟前
刷刷题36(uniapp高级实际项目问题-1)
前端·javascript·面试
-代号952738 分钟前
【CSS】一、基础选择器
前端·css
神仙别闹41 分钟前
基于Python+SQLite实现(Web)验室设备管理系统
前端·python·sqlite
爱嘿嘿的小黑42 分钟前
宇宙厂学到的思维模型,工作学习必备
前端·人工智能·面试
勘察加熊人1 小时前
angular打地鼠
前端·javascript·angular.js
柒@宝儿姐1 小时前
如何判断一个项目用的是哪个管理器
前端·javascript·vue.js·vue3
齐尹秦2 小时前
什么是 HTML?
前端
uhakadotcom2 小时前
Sentry:你的应用程序的守护者
前端·面试·github