何为 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调用-->
<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,其中包含三个图标-->
<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>
<!--利用use的跨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 优点:

  1. 修改ID就可以改变图标,使用方便。
  2. 页面代码量小,维护成本低。
  3. 图标可改变颜色大小,减少重复图片的加载
  4. 减少图片请求量。

svg-sprite 在框架中的应用

前端项目中常常有使用小图标的需求,以vue为例:

我们需要解决的问题如下:

  1. 自动打包svg,生成svg-sprite,并将打包好的内容插入html。
  2. 创建组件,以后在使用图标时,只用传入图标的名称,便可以生成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 = {
// 一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例
// 允许对内部的 webpack 配置进行更细粒度的修改
chainWebpack: (config) => {
// 配置svg默认规则排除icons目录中svg文件处理
config.module.rule('svg').exclude.add(resolve('src/icons')).end();

// 新增icons规则,设置svg-sprite-loader处理icons目录中svg文件
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
// main.js
import '@/icons/svg/book.svg';
1
2
3
4
5
6
<!--template中使用-->
<template>
<svg :class="svgClass">
<use xlink:href="#icon-book"></use>
</svg>
</template>

这样,sprite就只会把引入的这些图标打包,然后插入html:

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

1
2
3
4
5
// index.js

// icons图标自动加载
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({
// 指定存放svg文件的目录为 src/assets/svg/
plugins: [vue(), svgBuilder('./src/assets/svg/')],
});

组件

注册和使用方式同 vue-cli,注意此时不再需要引入刚刚的 index.js。

1
2
3
4
5
6
7
// main.ts

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图标