Vue 的类字体 SVG 图标系统

Avatar of Kevin Lee Drum
Kevin Lee Drum

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

在 Vue 应用程序中管理自定义图标集合有时可能具有挑战性。字体图标易于使用,但对于自定义,您必须依赖第三方字体生成器,并且由于字体是二进制文件,因此合并冲突可能难以解决。

使用 SVG 文件可以消除这些痛点,但我们如何确保它们同样易于使用,同时又能轻松添加或删除图标呢?

这是我理想的图标系统的样子

  • 要添加图标,只需将其放入指定的 icons 文件夹中。如果您不再需要某个图标,只需将其删除。
  • 要在模板中使用 rocket.svg 图标,语法非常简单,只需 <svg-icon icon="rocket" />
  • 可以使用 CSS 的 font-sizecolor 属性(就像字体图标一样)来缩放和着色图标。
  • 如果页面上出现同一图标的多个实例,则不会每次都复制 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 参数删除我们不需要的图标标签,例如 titlestyle。我们尤其希望删除 title 标签,因为这些标签会导致不需要的工具提示。如果您想保留图标中任何硬编码的样式,则添加 removingTags=title 作为附加参数,以便仅删除 title 标签。

我们还告诉我们的加载器删除 fill 属性,以便我们稍后可以使用 CSS 设置自己的 fill 颜色。您可能希望保留 fill 颜色。如果是这种情况,则只需删除 removeSVGTagAttrsremovingTagAttrs 参数即可。

最后一个加载器参数是 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,例如 rotateflip。如果您愿意,可以将类直接添加到组件中而无需使用 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,请查看 将两者相互比较的经典文章