24

【非标题党】SVG 图标看我就够了

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU4MzUzODc3Nw%3D%3D&%3Bmid=2247484157&%3Bidx=1&%3Bsn=80961781e0e74b5145f178bdd54af168
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

640?wx_fmt=png

都 2020 了如果你还没有在项目中使用过 SVG,就好比你没有在项目使用过 REACT 或 VUE 一样。

在不考虑兼容性(IE8+)的情况下,SVG 应该是目前解决项目中 图标问题 的最佳方案,没有之一。

SVG 在变大变小的情况下不会出现失真(出现锯齿或者看到像素点),也可以像 GIF 一样可以动起来。你不再会有因为同一个图标颜色不同,就要切两张图的困扰。

只要你愿意,你甚至可以用 HTML,CSS,JS 其中任何一个语言,来实时修改你的 SVG 图标,即使是前后两个图标可能长得完全不一样。

我们项目在建站初期直接就选择了 SVG 作为我们图标的解决方案。虽然以上众多好处让我们体会到了它的好,然而在实际项目中还是遇到了一些坑。为了不做标题党,这里总结了我们团队近三年对于 SVG 使用的大大小小的问题。

一、SVG 的几种使用方式

  • SVG as Img

  • SVG as Sprite (Iconfont)

  • SVG in HTML

  • SVG in CSS

  • SVG in JS

二、技术方案实际体感

  • SVG as Img & Sprite

  • SVG In HTML & CSS

  • SVG In React

三、SVG In React 交付最佳实践

四、SVG In React 使用最佳实践

五、SVG 的一些坑

一、SVG 的几种使用方式

1. SVG as Img

首先,SVG 可以像 JPG,PNG,GIF 一样,作为图片文件去使用。

<style>
[data-icon] {
display: inline-block;
width: 1em;
height: 1em;
background: no-repeat center/contain;
}
[data-icon][size="24"]{
width: 24px;
height: 24px;
}
[data-icon="hello"] {
background-image: url("./assets/hello.svg");
}
</style>
<i aria-hidden="true" size="24" data-icon="hello"></i>
<img src="./assets/hello.svg" width="24" height="24" alt="hello" />

不管是将它作为背景图片,或者是作为 <img/>src 属性都可以。

还可以作为一些 embed object iframe … 等标签等 src 属性,但因为项目中几乎很少用到这里不多做介绍。

2. SVG as Sprite (Iconfont)

640?wx_fmt=jpeg

我们都知道为了减少图片资源请求数量,会将大多数的小图标合并成 雪碧图 ,然后利用 CSS 控制 background-position 的位置来实现不同图标的显示。

对于 SVG 实现雪碧图,当然首推的是 iconfont [1] 只需要将你的 SVG 文件上传上去,然后点击下载,就能得到合并好的三种不同使用姿势的雪碧图(类似知名的还有 icomoon [2] )。

640?wx_fmt=png

方案 Unicode Font class Symbol 兼容性 IE6+ IE8+ IE9+ 特点 渲染效果不及后两种 上手容易,书写直观 面向未来

基本上现在只需要基于浏览器兼容性,考虑后面两种方案就好。

3. SVG in HTML

SVG 是使用 XML 来描述二维图形和绘图程序的语言。

因为 SVG 本身是 XML 格式的,这个和 HTML 天然类似,所以可以直接将 SVG 内联到 HTML 中。

<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect width="50" height="50" fill="#ff0000"/>
</svg>

比如你想画长宽都是50px 的正方形,你只需要将上面的代码粘贴到 HTML 即可。想要改变颜色,或者尺寸只需要修改对应属性即可,是不是非常的直观和方便。

这也是 SVG 相对于其它图片格式,除开 矢量特性 之外的另一个不可替代的优势。 可是我们的图标往往不是只使用一次,也并不是每个图标的 HTML 代码量都这么少,如果想通过直接复制粘贴来达到复用的效果就比较的麻烦。

既然是对于 HTML 的复用,那么比较最简单的易行的方式就是使用 HTML模版引擎 ,这里以 nunjunks 举例。

<!-- components/Icons/ISquare.html -->
{% macro ISquare(color='red', size='16') %}
<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect width="50" height="50" fill="{{ color }}"/>
</svg>
{% endmacro %}

这里我们在 ISquare.html 文件中,定义一个拥有颜色和大小两个入参的组件。

{% import "../components/Icons/ISquare.html" as ISquare %}
<ul>
<li>{{ ISquare() }} 红色大小为 16 的正方形</li>
<li>{{ ISquare('yellow', 24) }} 黄色大小为 24 的正方形</li>
</ul>

定义好组件之后,我们只需要在使用的地方 import 一下,调用即可。这个和你在 React 中定义了一个组件是一样的。

4.SVG in CSS

然而并不是所有的项目中都一定用了模版引擎,你可能只是一个非常简单静态活动页。

对于这样的项目我们可以换一个思路,将 SVG 内联到 CSS 当中。当然这个也只是一个 老瓶装新酒 的方案,因为很多构建工具都有能力将我们小于 8kb 的图片,转成 base64 的代码内联到我们的 CSS 中。

640?wx_fmt=png

<style>
[data-icon] {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
background: no-repeat center/contain;
}
[data-icon][size="24"]{
width: 24px;
height: 24px;
}
[data-icon="square"] {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGZpbGw9InJlZCIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PC9zdmc+");
}
</style>
<ul>
<li><i data-icon="square"></i> 红色大小为 1em 的正方形</li>
<li><i data-icon="square" size="24"></i> 红色大小为 24px 的正方形</li>
</ul>

同理我们也可以将我们的 SVG 转成 base64 内联到 CSS 当中。虽然可以借助 background-size: contain; 这个属性通过控制容器的大小,来间接控制图标的大小,但也失去了对图标 颜色 的控制。并且这一堆乱码混在 CSS 看起来也是特别的奇怪的。

如果你有跟进张鑫旭老师的博客,你会发现有这样的一篇文章 《学习了,CSS中内联SVG图片有比Base64更好的形式》 [3] 。简单的解释就是 SVG 内联进 CSS 可以不使用 base64 而是直接将 XML 内联进 CSS。

640?wx_fmt=png

<style>
[data-icon] {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
background: no-repeat center/contain;
}
[data-icon][size="24"]{
width: 24px;
height: 24px;
}
[data-icon="square"] {
background-image: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='red' d='M0 0h50v50H0z'/%3E%3C/svg%3E");
}
[data-icon="square"][color="blue"] {
background-image: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='blue' d='M0 0h50v50H0z'/%3E%3C/svg%3E");
}
</style>
<ul>
<li><i data-icon="square"></i> 红色大小为 1em 的正方形</li>
<li><i data-icon="square" size="24" color="blue"></i> 蓝色大小为 24 的正方形</li>
</ul>

可以看到我们利用 CSS 选择器,映射了两个颜色不一样的 SVG 图标,以间接实现了图标的换色,虽然这个方案并不完美(需要复制一份 XML ),但是这个可读性是比使用 base64 更好的,改一个颜色也是很方便的。

5. SVG In JS

可以看到不管是用 HTML 内联,还是 CSS 内联,都并不是那么的完美。于是我们网页 三剑客 就只剩下了 JS 。按照道理来说,能用 HTML 和 CSS 解决的问题我们都不会优先选择 JS ,然而随着目前 React 和 Vue 等 JS 框架的流行,JS 反而成为了我们的首选。

于是就有了 SVG 内联进 JS 的方案。这个逻辑其实和利用模版引擎将 SVG 内联进 HTML 原理一样。只是我们换了一个更高级的模版引擎,这里以 React 中的 JSX 为例。

640?wx_fmt=png

import React from "react";
import "./styles.css";
// import ISquare from "./components/Icons/ISquare";
const ISquare = ({ size = "16", color = "red" }) => (
<svg width={size} height={size} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill={color} />
</svg>
);
export default function App() {
return (
<ul>
<li><ISquare /> 蓝色大小为 24 的正方形</li>
<li><ISquare size="24" color="yellow" />黄色大小为 24 的正方形</li>
</ul>
);
};

可以看到这里我们定义了一个拥有颜色和大小两个属性的图标组件 <ISquare/> 。定义好组件之后,就可以直接使用就可以了,如果你想要这个图标被其它页面使用,只需要将这个组件独立出去即可。并且更可喜的是,你对整个图标里面的所有节点都是有操控能力的。你可以在里面定制任何你想要的逻辑。

二、技术方案实际体感

方案没有好坏,只看它与当下的场景是否贴合

640?wx_fmt=png

上一节我们介绍了五种使用 SVG 的方式,这一节我们会介绍这些方案实际在项目中的使用体验。

1. SVG as Img & Sprite

首先把 SVG 作为普通图片格式来处理, 上手成本 是最低的。

但只发挥了 SVG 矢量的这一特性(放大缩小不会失真),同一个图标不同的颜色还是需要两份文件(可以考虑使用 CSS 滤镜来解决这个问题,但是上手成本就高了)。并且一个图标一个请求,当页面图标较多的时候是一个不小的压力(可能 HTTP2 会让请求数量不再是一个问题 )。

为了解决请求数量我们考虑到了 图片精灵 这个方案。于是我们选择了 Iconfont,它的在线编辑功能还是很好用的,也充当了一个图标管理员的角色,并且可以让设计师也能直接看到我们当前项目中用到的图标,有一种 所见即所得 的感觉。然而一个项目要接入 Iconfont 可能成本比想象中高。

640?wx_fmt=png

首先,Iconfont 中要上传图标是 严格标准 的。处理图标格式这件事情应该谁做就成了一个问题,你说让前端同学去处理吧,又没有几个前端同学了解如何在设计工具中去处理这些规则。你说让设计同学弄吧,这又好像是他们原本不需要做的事情。关键我们不可能把全站用到的 SVG 打包成一个超大的文件,所以多少有一些 代码拆分 的逻辑,这个你还要设计同学了解,他们内心就更拒绝了。

再者当有 图标更新 的时候,我们又得重复走一套,设计工具处理,导出,上传,下载,替换本地文件的套路。还经常出现好不容易处理好了,上传到 Iconfont 中还是失败的问题,你就得回到设计工具再处理一次(说出来都是泪)。

按照以往的经验,对于类似工作,其实应该交给 构建工具 去做更合适。约定一个文件夹格式,然后在这个文件夹中的 SVG 都会自动打包成一个大的图片精灵。

理想很丰满,现实很骨感!

在构建工具中去集成类似图片的处理能力,同样是一堆乱七八糟的事情,特殊依赖装不上不说,还大大拖慢整体项目 构建效率 。然后已经两天过去了,你可能在项目中连 SVG 的影子都还没有看到,全在做 基建 的工作了。

2. SVG In HTML & CSS

迫于 构建压力 ,可能采用内联 HTML 或者内联 CSS 更好一些。

当然在这两者当中,如果条件允许(需要依靠模版引擎来实现复用),首推的还是内联 HTML。优势就在于可以通过传参的方式,来定制我们的 SVG 图标。

当然这两者都还是需要有一个从设计工具中的 SVG 素材转换成特定 XML 代码格式的过程。如果你的项目比较小,这里再次推荐一下,张鑫旭老师做的 SVG在线压缩合并工具 [4]

640?wx_fmt=png

这里如果非要说一个 内联 CSS 比 内联 HTML 更好的地方在于对于 渐变图标 的支持。我们都知道在一个 HTML 页面里 ID 是唯一的。而我们的 SVG 图标实现 SVG 渐变的时候,也是通过 ID 来实现复用的。这就很容易出现两个不同的渐变样式的图标,使用了相同 的 渐变 ID 名(这个 ID 是设计工具动态生成的),导致后一个图标直接使用的是前一个图标的渐变样式。而这个逻辑在我们 CSS 内联中是不存在的。

3. SVG In JS

在 React 的世界里,万物皆组件,万物皆被构建。

前面提到的 SVG 人工转 XML 的工作,可以交给 @svgr/webpack [5] 这个webpack 插件来做。还可以在项目中,引用 svgo [6] 这个 npm 包,来实现对 SVG 的压缩和优化。

然后在项目中,你的实际使用体验是这样的。

640?wx_fmt=png

看到这里你会发现,你虽然可以改变 SVG 尺寸但是你好像不知道要怎么去修改 SVG 图标的颜色。

<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M50 0H0V50H50V0Z" fill="#FF0000"/>
</svg>

首先, <ISquare /> 这个组件,直接暴露给我们的是 svg 这一层, path 相当于其内部组件。想要修改 path 的颜色我们只能通过 CSS 迂回的实现。

svg{
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
svg path[fill] {
fill: currentColor;
fill-opacity: 1;
}
svg path[stroke] {
stroke: currentColor;
stroke-opacity: 1;
}

这个样式的核心就在于 currentColor , 通过这个属性我们就可以实现让 svg 内部 path 的 fill 和 stroke 继承 svg 的 color 。

import React from "react";
import "./style.css";
const Icon = ({ size = 16, Svg, ...otherProps}) => (
<Svg width={size} height={size} {...otherProps}/>
);
export default Icon;

然后我们将这个逻辑封装进另一个名叫 <Icon /> 组件中,还在这个组件当中将 width 和 height 用 size 替代以减少暴露的接口数量。

640?wx_fmt=png

这样我们就得到了现在这样的写法(也可以把颜色这个属性内聚到 <Icon/> 组件内部,这里是为了降低代码演示的逻辑复杂度)。

照理来说,如果这个修改尺寸和颜色的逻辑在 @svgr/webpack 就能实现的话,这里额外封装的 <Icon /> 是多余的。

三、SVG In React 交付最佳实践

前面几节里,我们始终都是在如何优化构建逻辑这个小小圈子里面打转。在实际使用中更影响体验的还有一个难题—— SVG管理交付

640?wx_fmt=png

比如之前提到的,Iconfont 要做图标精灵 代码分割 就很难。通常我们把全局都要用到的图标,打包成一个 SVG 雪碧图,然后其它的按照页面去分别打包。但是往往会出现的问题是,有一个页面它就是很特别,它就只用到了全局那个 SVG 雪碧图中的一个,你怎么办?或者 A 页面 和 B 页面共享了某几个图标,你又怎么办?并且我们这个逻辑,设计同学可能完全没有感知。

640?wx_fmt=png

除开 SVG In CSS 之外,对于 Svg In Html 和 Svg In JS 来说,可以将每一个图标文件集中放到一个文件夹下( ./components/Icons/* ),然后按需加载引用,就没有了分组管理的问题。然而一切还是想得太简单。

比如某次迭代中我们需要一个为别人 点赞 的图标,A 同学导出了这个图标,并取名为了 good.svg ,B 同学也发现自己也需要这个图标,但是他在图标文件夹里没有找到 like.svg ,于是他导出了一个名为 like.svg 的图标。

随着项目推进,设计师又设计了这个图标的另外一个描边版本 good-outline.svg 。然后隔两天又新加入了一个设计师,又设计了一个渐变版本 like-gradient.svg

后面老大看到整个网站说,为啥这个网站同一个点赞图标又这么多样式,给我统一成一个样式。然后设计师又出了一个新版 good-new.svg 让你全局替换。此时我想你是蒙圈的。因为你并不知道这些图标分布在哪个页面的哪个角落,并且他们名字还不一样,你也不敢删你也不敢问。

640?wx_fmt=png

出现这个状况的原因,在于开发人员和设计同学在维护同一套设计资源。并且两端的管理方式还可能不太一致。作为开发者的我们自然会想,既然设计同学已经有一套管理方案了?那我们是否可以直接拿过来使用呢?

这个难题在我看到 使用 Figma + GitHub Actions 完成 SVG 图标的完全自动化交付 这篇文章之后,变成了现实(为原作者打 Call)。

640?wx_fmt=png

简单介绍一下就是,设计师只需要在 Figma (设计工具) 中编辑和修改好图标之后,点击 update 按钮,然后前端 npm install 一下一切就都搞定了,之前繁琐的 管理构建 在使用阶段统统不需要关心(整体的时间延迟,大约2~3分钟)。

具体逻辑可以看原文详情的介绍,这边特别需要指出的是,这个 figma svg 转 npm 的过程,我们是可以在 github 仓库中全程接管的,也就是说可以基于实际项目去定制想要的任何逻辑。这里介绍一下基于我们团队的调性,模改出来的 React 版本:

1. 精简了最后生成组件的样式; 2. 和设计师约定以下划线开头的图标会被忽略不会发不到 npm 中; 3. 相同文件名的图标,只会选择最后一个; 4. 生成的 React component 的名称统一会以大写字母 I 开头,比如 <ICamera /> 5. 生成的组件顺序会按照首字母排序(找起来更容易); 6. 原作者是用的 github pages 来展示的图标,为了更高的自由度,我们这边替换成了用 codesandbox 来作为图标的展示。

想要相同模改版本的同学,可以点击这里 Fork webnovel-icons [7]

可以看到目前我们整个网站,有 183 个图标,然后这个展示的界面,还可以修改图标大小,颜色,以及背景颜色,需相同展示 Demo 的同学可以直接点击链接 Edit on CodeSandBox Fork 使用。

这一切最值得开心的就是,前端同学完全不需要维护这一整套图标,设计同学直接就可以帮我们搞定。我们要做的就是,当图标有更新的时候 npm install 一下。

四、SVG In React 使用最佳实践

经过这一翻操作之后,我们就将图标管理直接 甩锅 给了设计同学,接下来我们就只需要解决 图标使用 问题了。

640?wx_fmt=png

可以看到这里我们兼容了 @svgr/webpackwebnovel-icons 两种图标引用方式。

在我们项目中实际是通过 CSS 作为我们的 Token 来管理颜色的。所以并未在 React Component 中暴露类似 color 属性来修改颜色。这里可以基于自己实际项目定制。

.icon {
display: inline-block;
vertical-align: middle;
}
.icon:not(._self_color) {
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
.icon:not(._self_color) path[fill] {
fill: currentColor;
fill-opacity: 1;
}
.icon:not(._self_color) path[stroke] {
stroke: currentColor;
stroke-opacity: 1;
}

同时为了兼容多色图标,我们额外暴露一个属性 isSelfColor 利用 CSS 选择器去决定是否去掉我们图标默认的颜色。我们项目中单色图标的使用场景是远远超过多色图标的,所以我们默认是单色图标。这样我们就算比较完整的解决了 SVG 使用过程中的几大问题。

1. SVG 管理 2. SVG 压缩,优化 3. SVG 转 React Component 4. SVG 定制大小,修改颜色 5. 多色图标的支持 6. 其它自由定制需求

这边还有一点需要特别提出的是, @svgr/webpackwebnovel-icons 两种方式的虽然都可以做到 SVG 的压缩和优化,但是他们的时间点是不一样的。 @svgr/webpack 是在 webpack 去实时处理的,而 webnovel-icons 是在转成 React component 之前就处理好了。

五、SVG 的一些坑

1. SVG 变形

在使用某些圆形图标的时候,会发现这个图标在设计稿中是圆的,但是在浏览器里面就是不圆。这种情况通常是因为我们的图标里面的 path 路径不是整数造成的。发生这种情况需要麻烦设计师将其改为整数。但是这个是有概率的,只能说是尝试一下,不行就得继续改(这个问题其实有点迷)。

这边猜测可能是压缩工具导致的,当SVG原生尺寸较小的时候,数值可能是 1.233458这样,压缩工具会取合适的小数位数,结果就会导致细节失真。如果 SVG 原始尺寸较大,例如iconfont.cn中图标都是 200 以上,就不容易出现这样的问题。

2. SVG 事件没有触发

在某些浏览器中,如果直接在 SVG 组件上绑定 onClick 事件,这个事件并不会触发。这边我们建议的逻辑是在 <svg/> 标签外再套一层 <i/> 标签或者是 <button /> 标签。然后将这个事件绑定到外容器上。

3. SVG 的颜色只变了一半

这种问题往往是一个 SVG 图标中,同时出现了 fill,stroke 两种类型的 path,这种建议让设计师在设计工具中将图标先 轮廓化 (outline stroke), 然后再 扁平化 (Flatten)。这个可能不同设计工具叫法不一样,可以咨询一下设计师。

4. SVG 边缘被切掉了

640?wx_fmt=png

这个问题在于,设计同学提供的 SVG 贴边了,建议在 SVG 图标和容器之间至少留一像素。

End

到这里我们已经介绍了,在大大小小项目中使用 SVG 图标的各种 姿势 。希望能够尽可能全的帮助到大家,所以体量有一些的大。如果有遗漏的或者是大家有更好的方案,也欢迎大家给我留言,交流。

当然 SVG 的优势还远不止如此,特别是在一些动画能力上是特别方便以及优秀的。这里也是非常推荐大家尝试的。

References

[1] iconfont:  https://www.iconfont.cn/

[2] icomoon:  https://icomoon.io/

[3] 《学习了,CSS中内联SVG图片有比Base64更好的形式》:  https://www.zhangxinxu.com/wordpress/2018/08/css-svg-background-image-base64-encode/

[4] SVG在线压缩合并工具:  https://www.zhangxinxu.com/sp/svgo/

[5] @svgr/webpack:  https://www.npmjs.com/package/@svgr/webpack

[6] svgo:  https://www.npmjs.com/package/svgo

[7] webnovel-icons:  https://github.com/yued-fe/webnovel-icons


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK