何为 svg-sprite
css-sprite
在说svg-sprite以前,先聊聊css-sprite,一般我们称之为“雪碧图”。
雪碧图,就是把很多小的图标整合到一张图片中,可以减少向后端请求的次数。

在使用这些图片时,比如让这些图片作背景,便可以调整 background-position
值,使之显示我们需要的部分。
svg-sprite
因此,svg-sprite 同理,就是把很多svg合到一个.svg文件中,开发者使用图标时,每次只使用其中的一部分。
首先,我们需要得到一些.svg文件,可以前往iconfont等网站下载:

之后,可以借助一些工具将这些.svg文件合并,产生svg-sprite.svg:
SVG在线压缩合并工具
icomoon
有了合并后的svg,还需要知道使用方法:
SVG Sprite最佳实践是使用symbol
元素,可以把SVG元素看成一个舞台,而symbol
则是舞台上一个一个组装好的元件,这这些一个一个的元件就是我们即将使用的一个一个SVG图标,然后,使用use来调用它们。
除了symbol,还可以使用defs
。
SVG的元素用于预定义一个元素使其能够在SVG图像中重复使用。
SVG 元素用于定义可重复使用的符号。能够创建自己的视窗,能够应用viewBox和preserveAspectRatio属性。
它们的共同特点是,在其中的元素都不会直接显示出来,需要使用use
调用。
use具有可重复调用和跨SVG调用的特性,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <svg> <defs> <g id="shape"> <rect x="0" y="0" width="50" height="50" /> <circle cx="0" cy="0" r="50" /> </g> </defs> <use xlink:href="#shape" x="50" y="50" /> <use xlink:href="#shape" x="200" y="50" /> </svg>
<svg> <use xlink:href="#shape" x="50" y="50" /> </svg>
|
浏览器中的效果:

因此,当获得了合成后的svg文件,我们可以采用外链引入的方式,如:
1 2 3
| <svg id="svg-sprite"> <use xlink:href="./svg/symbol-defs.svg#icon-game" /> </svg>
|
不过,根据浏览器的安全策略,页面会出现空白,控制台有一行报错信息。
因此,此处的演示选择直接将合成svg代码复制进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> svg { width: 30px; height: 30px; }
#game { fill: red; } #book1 { fill: green; }
#book2 { fill: blue; } </style> </head> <body> <svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <defs> <symbol id="icon-game" viewBox="0 0 32 32"> <path d="M32.." ></path> </symbol> <symbol id="icon-book1" viewBox="0 0 32 32"> <path d="M25..." ></path> <path d="M21..." ></path> </symbol> <symbol id="icon-book2" viewBox="0 0 32 32"> <path d="M27..." ></path> </symbol> </defs> </svg> <svg id="game"> <use xlink:href="#icon-game" /> </svg> <svg id="book1"> <use xlink:href="#icon-book1" /> </svg> <svg id="book2"> <use xlink:href="#icon-book2" /> </svg> </body> </html>
|
效果如图:

Svg-sprite 优点:
- 修改ID就可以改变图标,使用方便。
- 页面代码量小,维护成本低。
- 图标可改变颜色大小,减少重复图片的加载
- 减少图片请求量。
svg-sprite 在框架中的应用
前端项目中常常有使用小图标的需求,以vue为例:
我们需要解决的问题如下:
- 自动打包svg,生成svg-sprite,并将打包好的内容插入html。
- 创建组件,以后在使用图标时,只用传入图标的名称,便可以生成icon。
vue-cli
配置loader
首先,下载svg-sprite-loader
。
1
| npm install svg-sprite-loader -D
|
svg-sprite-loader 的作用是合并一组单个的svg图片为一个sprite,并把合成好的内容,插入到html内,形式是添加svg标签。
指定@/src/icons
,在该目录下新建/svg目录,把svg图全部放在该目录下。
vue-cli对svg文件有默认的url-loader
处理,所以要排除url-loader
对@/src/icons
的处理,指定svg-sprite-loader
处理。
修改vue.config.js
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const path = require('path');
function resolve(dir) { return path.join(__dirname, dir); }
module.exports = { chainWebpack: (config) => { config.module.rule('svg').exclude.add(resolve('src/icons')).end();
config.module .rule('icons') .test(/\.svg$/) .include.add(resolve('src/icons')) .end() .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) .end(); }, };
|
以上代码首先排除了默认svg的loader对icons/
目录下svg文件的处理,然后新增了一个规则让svg-sprite-loader
处理icons/
文件夹下的svg文件,最后设置了icon-
加上经过处理的svg文件名作为symbolId,也就是说在使用book.svg
时可以直接在use标签使用#icon-book
。
此时,在main.js中引入一些svg,这些svg经过处理变为sprite,然后我们就可以使用这些图标了。
1 2
| import '@/icons/svg/book.svg';
|
1 2 3 4 5 6
| <template> <svg :class="svgClass"> <use xlink:href="#icon-book"></use> </svg> </template>
|
这样,sprite就只会把引入的这些图标打包,然后插入html:

不过,一个一个引入组件是很麻烦的,我们希望可以将全部svg直接打包插入html,在icons/下创建index.js:

1 2 3 4 5
|
const req = require.context("./svg", false, /\.svg$/); req.keys().map(req);
|
然后,在main.js中引入:
1
| import '@/icons/index.js';
|
这样,处于icons/svg下的所有svg文件都会被打包,在template中直接使用即可:

组件
创建
上边的方式还是不够优雅,我们在/components目录下创建SvgIcon.vue
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <template> <svg :class="svgClass"> <use :xlink:href="iconName" /> </svg> </template>
<script> import { computed, toRefs } from 'vue';
export default { name: 'SvgIcon', props: { iconClass: { type: String, required: true, }, className: { type: String, default: '', }, }, setup(props) { // 如果不使用toRefs,可能丢掉效应式的特性 const { className, iconClass } = toRefs(props) const iconName = computed(() => `#icon-${iconClass.value}`); const svgClass = computed(() => { if (className.value) { return 'svg-icon ' + className.value; } else { return 'svg-icon'; } });
return { iconName, svgClass, }; }, }; </script> <style scoped> .svg-icon { width: 2em; height: 2em; fill: currentColor; } </style>
|
注册
在main.js中,注册为全局组件:
1 2 3 4 5 6 7
| import { createApp } from 'vue'; import App from './App.vue'; import SvgIcon from './components/Svg-icon.vue'; import '@/icons/index.js';
createApp(App).component('svg-icon', SvgIcon).mount('#app');
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <template> <div> <!--使用SvgIcon组件--> <svg-icon :iconClass="'book'" :className="'icon-book'" /> <svg-icon :iconClass="'game'" :className="'icon-game'" /> </div> </template>
<script> export default { name: 'App', }; </script>
<style scoped> /* 设置图标样式 */ .icon-book { color: blue; }
.icon-game { color: green; transition: all .3s; }
/* 还可以使用媒体查询 */ @media only screen and (max-width: 800px) { .icon-game { color: red; } } </style>
|
vite
(此处演示使用vite + vue3 + ts)
到了 Vite
上,由于不再使用 webpack
进行打包,配置方式有所变化。
依然先安装依赖:
1
| npm install svg-sprite-loader
|
配置

在@/src/plugins下创建svgBuilder.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| import { Plugin } from 'vite'; import { readFileSync, readdirSync } from 'fs'; let idPerfix = ''; const svgTitle = /<svg([^>+].*?)>/; const clearHeightWidth = /(width|height)="([^>+].*?)"/g;
const hasViewBox = /(viewBox="[^>+].*?")/g;
const clearReturn = /(\r)|(\n)/g;
function findSvgFile(dir): string[] { const svgRes = []; const dirents = readdirSync(dir, { withFileTypes: true, }); for (const dirent of dirents) { if (dirent.isDirectory()) { svgRes.push(...findSvgFile(dir + dirent.name + '/')); } else { const svg = readFileSync(dir + dirent.name) .toString() .replace(clearReturn, '') .replace(svgTitle, ($1, $2) => { let width = 0; let height = 0; let content = $2.replace(clearHeightWidth, (s1, s2, s3) => { if (s2 === 'width') { width = s3; } else if (s2 === 'height') { height = s3; } return ''; }); if (!hasViewBox.test($2)) { content += `viewBox="0 0 ${width} ${height}"`; } return `<symbol id="${idPerfix}-${dirent.name.replace( '.svg', '' )}" ${content}>`; }) .replace('</svg>', '</symbol>'); svgRes.push(svg); } } return svgRes; }
export const svgBuilder = (path: string, perfix = 'icon'): Plugin => { if (path === '') return; idPerfix = perfix; const res = findSvgFile(path);
return { name: 'svg-transform', transformIndexHtml(html): string { return html.replace( '<body>', ` <body> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0"> ${res.join('')} </svg> ` ); }, }; };
|
配置vite.config.ts
1 2 3 4 5 6 7 8
| import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { svgBuilder } from './src/plugins/svgBuilder';
export default defineConfig({ plugins: [vue(), svgBuilder('./src/assets/svg/')], });
|
组件
注册和使用方式同 vue-cli,注意此时不再需要引入刚刚的 index.js。
1 2 3 4 5 6 7
|
import { createApp } from 'vue'; import App from './App.vue'; import SvgIcon from './components/Svg-icon.vue';
createApp(App).component('svg-icon', SvgIcon).mount('#app');
|
参考资料
未来必热:SVG Sprites技术介绍
Vue项目中优雅使用icon
vite中使用svg图标