在 Vue 应用程序中管理自定义图标集合有时可能具有挑战性。字体图标易于使用,但对于自定义,您必须依赖第三方字体生成器,并且由于字体是二进制文件,因此合并冲突可能难以解决。
使用 SVG 文件可以消除这些痛点,但我们如何确保它们同样易于使用,同时又能轻松添加或删除图标呢?
这是我理想的图标系统的样子
- 要添加图标,只需将其放入指定的
icons
文件夹中。如果您不再需要某个图标,只需将其删除。 - 要在模板中使用 rocket.svg 图标,语法非常简单,只需
<svg-icon icon="rocket" />
。 - 可以使用 CSS 的
font-size
和color
属性(就像字体图标一样)来缩放和着色图标。 - 如果页面上出现同一图标的多个实例,则不会每次都复制 SVG 代码。
- 无需编辑 webpack 配置。
这就是我们将通过编写两个小型单文件组件来构建的内容。此实现有一些具体的要求,尽管我相信你们中的许多高手可以将此系统重新用于其他框架和构建工具
- webpack:如果您使用 Vue CLI 构建了您的应用程序,那么您已经在使用 webpack 了。
- svg-inline-loader:这允许我们加载所有 SVG 代码并清理我们不需要的部分。从终端运行
npm install svg-inline-loader --save-dev
以开始使用。
SVG 雪碧图组件
为了满足我们不重复页面上每个图标实例的 SVG 代码的要求,我们需要构建一个 SVG“雪碧图”。如果您以前从未听说过 SVG 雪碧图,可以将其想象成一个隐藏的 SVG,其中包含其他 SVG。在需要显示图标的任何地方,我们都可以通过引用 <use>
标签内图标的 id 从雪碧图中将其复制出来,如下所示
<svg><use xlink:href="#rocket" /></svg>
这段代码基本上就是我们的 <SvgIcon>
组件的工作原理,但让我们先创建 <SvgSprite>
组件。这是整个 SvgSprite.vue
文件;其中一些内容一开始可能看起来令人生畏,但我将对其进行分解。
<!-- SvgSprite.vue -->
<template>
<svg width="0" height="0" style="display: none;" v-html="$options.svgSprite" />
</template>
<script>
const svgContext = require.context(
'!svg-inline-loader?' +
'removeTags=true' + // remove title tags, etc.
'&removeSVGTagAttrs=true' + // enable removing attributes
'&removingTagAttrs=fill' + // remove fill attributes
'!@/assets/icons', // search this directory
true, // search subdirectories
/\w+\.svg$/i // only include SVG files
)
const symbols = svgContext.keys().map(path => {
// get SVG file content
const content = svgContext(path)
// extract icon id from filename
const id = path.replace(/^\.\/(.*)\.\w+$/, '$1')
// replace svg tags with symbol tags and id attribute
return content.replace('<svg', `<symbol id="${id}"`).replace('svg>', 'symbol>')
})
export default {
name: 'SvgSprite',
svgSprite: symbols.join('\n'), // concatenate all symbols into $options.svgSprite
}
</script>
在模板中,我们唯一的 <svg>
元素将其内容绑定到 $options.svgSprite
。如果您不熟悉 $options
,它包含直接附加到 Vue 组件的属性。我们可以将 svgSprite
附加到组件的 data
,但我们实际上并不需要 Vue 为此设置响应性,因为我们的 SVG 加载器仅在我们构建应用程序时运行。
在我们的脚本中,我们使用 require.context
来检索所有 SVG 文件并在检索过程中对其进行清理。我们调用 svg-inline-loader
并使用与查询字符串参数非常相似的语法传递几个参数。我已将其分解成多行,以便于理解。
const svgContext = require.context(
'!svg-inline-loader?' +
'removeTags=true' + // remove title tags, etc.
'&removeSVGTagAttrs=true' + // enable removing attributes
'&removingTagAttrs=fill' + // remove fill attributes
'!@/assets/icons', // search this directory
true, // search subdirectories
/\w+\.svg$/i // only include SVG files
)
我们在这里基本上是在清理位于特定目录(/assets/icons
)中的 SVG 文件,以便在需要时可以在任何地方使用它们。
removeTags
参数删除我们不需要的图标标签,例如 title
和 style
。我们尤其希望删除 title
标签,因为这些标签会导致不需要的工具提示。如果您想保留图标中任何硬编码的样式,则添加 removingTags=title
作为附加参数,以便仅删除 title
标签。
我们还告诉我们的加载器删除 fill
属性,以便我们稍后可以使用 CSS 设置自己的 fill
颜色。您可能希望保留 fill
颜色。如果是这种情况,则只需删除 removeSVGTagAttrs
和 removingTagAttrs
参数即可。
最后一个加载器参数是 SVG 图标文件夹的路径。然后,我们向 require.context
提供另外两个参数,以便它搜索子目录并仅加载 SVG 文件。
为了将所有 SVG 元素嵌套到 SVG 雪碧图中,我们必须将它们从 <svg>
元素转换为 SVG <symbol>
元素。这就像更改标签并为每个标签提供唯一的 id
一样简单,我们从文件名中提取该 id
。
const symbols = svgContext.keys().map(path => {
// extract icon id from filename
const id = path.replace(/^\.\/(.*)\.\w+$/, '$1')
// get SVG file content
const content = svgContext(path)
// replace svg tags with symbol tags and id attribute
return content.replace('<svg', `<symbol id="${id}"`).replace('svg>', 'symbol>')
})
我们如何使用这个 <SvgSprite>
组件?我们将其放置在页面上任何依赖它的图标之前。我建议将其添加到 App.vue
文件的顶部。
<!-- App.vue -->
<template>
<div id="app">
<svg-sprite />
<!-- ... -->
图标组件
现在让我们构建 SvgIcon.vue
组件。
<!-- SvgIcon.vue -->
<template>
<svg class="icon" :class="{ 'icon-spin': spin }">
<use :xlink:href="`#${icon}`" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
icon: {
type: String,
required: true,
},
spin: {
type: Boolean,
default: false,
},
},
}
</script>
<style>
svg.icon {
fill: currentColor;
height: 1em;
margin-bottom: 0.125em;
vertical-align: middle;
width: 1em;
}
svg.icon-spin {
animation: icon-spin 2s infinite linear;
}
@keyframes icon-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
</style>
此组件要简单得多。如前所述,我们利用 <use>
标签来引用雪碧图中的 id。该 id
来自组件的 icon
prop。
我在那里添加了一个 spin
prop,它可以切换 .icon-spin
类作为可选的动画位,如果我们需要的话。例如,这可能对加载微调器图标很有用。
<svg-icon v-if="isLoading" icon="spinner" spin />
根据您的需要,您可能希望添加其他 prop,例如 rotate
或 flip
。如果您愿意,可以将类直接添加到组件中而无需使用 prop。
我们组件的大部分内容都是 CSS。除了旋转动画之外,大部分内容都用于使我们的 SVG 图标更像字体图标¹。为了将图标与文本基线对齐,我发现应用 vertical-align: middle
以及 0.125em
的底部边距在大多数情况下都能奏效。我们还将 fill
属性值设置为 currentColor
,这允许我们像文本一样为图标着色。
<p style="font-size: 2em; color: red;">
<svg-icon icon="exclamation-circle" /><!-- This icon will be 2em and red. -->
Error!
</p>
就是这样!如果您想在应用程序的任何地方使用图标组件而无需将其导入到每个需要它的组件中,请确保在 main.js
文件中注册该组件
// main.js
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
Vue.component('svg-icon', SvgIcon)
// ...

最终思考
以下是一些改进的想法,我故意将其省略,以使此解决方案易于理解
- 缩放具有非正方形尺寸的图标以保持其比例
- 将 SVG 雪碧图注入页面,而无需额外的组件。
- 使其与 vite 协同工作,vite 是 Vue 创建者 Evan You 推出的一款新的、快速的(且无需 webpack)构建工具。
- 利用 Vue 3 的 组合式 API。
如果您想快速试用这些组件,我创建了一个演示应用程序,该应用程序基于默认的 vue-cli 模板。我希望这可以帮助您开发一个适合您的应用程序需求的实现!
¹ 如果您想知道为什么我们想要使其表现得像字体图标时却使用 SVG,请查看 将两者相互比较的经典文章。
在
icon-spin
动画中使用rotate(359deg)
而不是360deg
有什么原因吗?如果您告诉图标旋转到
360deg
,它实际上在结束时将已经回到0deg
,因此它将在每个后续循环的第一个关键帧中不旋转。我们也有类似的东西,但我们不得不回到字体图标。似乎在 2020 年,渲染 SVG 仍然是手机上的性能问题,尤其是在 Safari 上。如果您需要重复显示图标,最终会导致滚动体验滞后。使用 SVG 雪碧图无济于事,在后台它仍然会克隆和重新创建源 SVG 的节点。我们有时生活在一个悲伤的新世界……
感谢您的提醒!在我们工作中使用此功能的地方,我们还没有看到或听说过任何滚动延迟(至少目前还没有)。我们可能一次渲染的图标数量不足以看到性能下降,但了解这一点很好。
现在是 7 月 29 日,距离您的帖子发布五天,我真的很需要这个。:) 我正在开发一个应用程序,该应用程序使用 electron、vue、ionic 和 cordova/capacitor 来构建跨平台应用程序。Ionic 的 svg 图标在 ios 或 android 中不起作用。使用您的组件并从其 svg 中编辑
stroke:#000
,我现在有可用的图标了。谢谢!
感谢 @kevinleedrum 撰写了这篇很棒且及时的文章!如果您还没有,可以查看 SVG Spritemap Webpack 插件 来生成雪碧图。
Kevin,看起来 vue-svg-loader 已经不再维护了。他们 建议 使用 raw-loader 和 image-minimizer-webpack-plugin 的组合来替代。只是好奇您(或任何人)是否已经尝试过这种方法。
感谢您撰写了这篇简洁且实用的文章。
您能否分享一个演示,演示如何“在无需额外组件的情况下将 SVG 雪碧图注入页面”。
看起来 Webpack 5 不再支持
svg-inline-loader
了。我正在尝试找出如何使用其他加载器来实现相同的 SVG 清理,但还没有找到任何解决方案!