SVG sprite方案

奴止
Jun 7, 2021
Last edited: 2022-10-6
type
Post
status
Published
date
Jun 7, 2021
slug
svg-sprite
summary
前端开发中 SVG 图标管理的一种解决方案:SVG sprite。
tags
前端开发
category
技术随手记
icon
password
Property
Oct 6, 2022 03:13 AM
 
本文例子基于 React,其它库可以查找相应工具参考实现。标题叫图标,其实对一般复杂的静态svg图也适用。
  • sprite: 基于拼贴的 svg-sprite 方式
  • inline: 类似 svgr 转为组件使用的方式
  • 需要高度定制(如动画、颜色、局部样式)的推荐使用inline
  • 大量使用的无定制图标建议使用sprite方式(具体原因见文末分析)

简单看下 Web Icon 历史

  • <img src="xx.png" /> 
  • image-sprite
  • font-icon
类似 <img src="xx.png" /> HTTP请求太多放大会失真一个简单的hover样式就要再请求另一张图 Image Sprite(雪碧图) 放大失真问题依然存在 Font Icons(字体图标) 字体加载成功时闪烁问题有些图标会有锯齿自定义字体支持性样式设置局限性(位置等)成本略高(开发中从文件或编码看不出图形,有图标变更时的可靠维护) SVG-Sprite & inline SVG
方案
问题
类似 <img src="xx.png" />
HTTP请求太多放大会失真一个简单的hover样式就要再请求另一张图
Image Sprite(雪碧图)
放大失真问题依然存在
Font Icons(字体图标)
字体加载成功时闪烁问题有些图标会有锯齿自定义字体支持性样式设置局限性(位置等)成本略高(开发中从文件或编码看不出图形,有图标变更时的可靠维护)
SVG-Sprite & inline SVG

inline SVG

将 svg 当作独立组件来维护,这样可以最大程度的定制svg。
CRA 配置中自带了 svg 转为 ReactComponent 的配置,因此你可以直接这么使用:
import { ReactComponent as Logo } from './logo.svg'; <Logo width="4em" height="4em" />
这种还不够高度定制化,可以将svg直接放在组件源码中进行管理、定制:
function Logo({extraCls, fill, weatherColor, ...}) { return (<svg className={extraCls} fill={fill}> <path fill={weatherColor} .../> // ... </svg>); }
通过工具 @svgr/cli 可以将 svg 源文件直接转为 react 组件。
更高级的使用配置,可以参考 SVGR doc: https://github.com/gregberge/svgr

SVG-Sprite

对于一些不需要特别定制的图片,如完全按照原图展示,或者只是改变大小、颜色等简单的定制,可以在svg中通过fill="currentColor"、css变量等实现。

基本原理

SVG 的入门知识可以参考: https://developer.mozilla.org/en-US/docs/Web/SVG
  • symbol + use
<svg style="width:0; height:0; visibility:hidden;position:absolute;z-index:-1"> <symbol viewBox="0 0 24 24" id="tomato"> <path d=""></path> </symbol> </svg> <svg> <use xlink:href="#tomato"/> </svg
一个在线示例(图标来自阿里iconfont:https://www.iconfont.cn/collections/detail?cid=21867):
 
上面的例子只是把三个图标合并到一个 SVG 中,并内联到了页面,目前存在几个问题:
  • 实际开发中,拿到的图标都是分散的,不能手动来合并 SVG
  • 一般设计师切出的图标都是有大量冗余内容
  • 内联的 SVG 无法缓存,需要做成一个外部文件
下面在 React 项目中具体解答。

Coding

合并 SVG

工具有很多,我们使用的是 webpack,利用 svg-sprite-loader 实现。
其它的如 gulp-svg-sprite,可以按关键词去搜索。
在 Create-React-App 生成的项目里,需要将 sprite-loader 加载最前面,防止被其它 loader 处理,下面是 craco 的配置,如果是 eject 了配置,或者使用的 rewired 等,可以相应处理,注意 CRA 3.0 是在 rules.oneOf  中,不是 rules 里
const path = require('path'); function resolve(dir) { return path.join(__dirname, '.', dir); } module.exports = function ({ env }) { const webpack = { configure: (webpackConfig, { env, paths }) => { const oneOfRule = webpackConfig.module.rules.find((rule) => Array.isArray(rule.oneOf) ); oneOfRule.oneOf.unshift({ test: /\.svg$/i, include: resolve('src/assets/icons'), // 只处理需要处理的文件 use: [ { loader: 'svg-sprite-loader', options: { symbolId: 'svg-icon-[name]', // 设置 <symbol> 中的 id <svg><symbol id={symbolId} /></svg> }, } ], }); return webpackConfig; }, }; return { webpack, }; };
封装一个简单额 Icon 组件,并使用:
// Icon.js import React from 'react'; import '../assets/icons'; function Icon({name}) { return ( <svg> // 这里注意和 svg-sprite-loader 配置中的 symbolId 对应 <use xlinkHref={`#svg-icon-${name}`}></use> </svg> ); } export default Icon; // ../assets/icons/index.js // 引用 icons 下所有 svg 文件,然后交给 svg-sprite-loader 去处理 const requireAll = requireContext => requireContext.keys().map(requireContext) const req = require.context('.', false, /\.svg$/i) requireAll(req) // App.js import Icon from './components/Icon'; <Icon name="tomato" />
start 项目看看效果:
notion image

去除冗余

安装压缩去冗余信息的 loader yarn add svgo-loader -D ,引入 webpack 即可。
{ test: /\.svg$/i, include: resolve('src/assets/icons'), // 只处理需要处理的文件 use: [ { loader: 'svg-sprite-loader', options: { symbolId: 'svg-icon-[name]', // 设置 <symbol> 中的 id <svg><symbol id={symbolId} /></svg> }, }, 'svgo-loader', ], }

分离为外部文件

通过 svg-sprite-loader 配置实现。
// loader { test: /\.svg$/i, include: resolve('src/assets/icons'), use: [ { loader: 'svg-sprite-loader', options: { symbolId: 'svg-icon-[name]', extract: true, }, }, 'svgo-loader', ], } // plugins webpackConfig.plugins.push(new SpriteLoaderPlugin()); // 在 index.html 添加引用 sprite 文件 <img style="display: none;" src="sprite.svg" />
记得同步修改 Icon 组件中 xlink:href 的值为 sprite.svg#svg-icon-${name}
notion image

inline组件方式和sprite对比

假设有1K条需要展示图标的列表。对比一下两者的性能:
inline方式(scripting 耗时2300ms
notion image
 
sprite方式(scripting 耗时700ms
notion image
 
因为组件化的图标需要经历一堆createElement的过程,当图标比较复杂(如wikipedia那个图标)时,更能表现出差异来。
另外,sprite方式和inline方式在dom结构上还有区别,前者只有2层(svg > use),后者是全量图标结构。
notion image
notion image

参考

  1. https://www.lambdatest.com/blog/its-2019-lets-end-the-debate-on-icon-fonts-vs-svg-icons/
  1. https://www.robinwieruch.de/react-svg-icon-components
github仓库添加在线页面styled-components原理