一个用于Vue的类似于字体的SVG图标系统

有时在Vue应用程序中管理图标的自定义集合很困难。图标字体易于使用,但是要进行自定义,必须依靠第三方字体生成器,并且合并冲突可能很难解决,因为字体是二进制文件。

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

理想的图标系统的外观:

  • 要添加图标,只需将它们放入指定的icons文件夹中。如果不再需要图标,只需删除它即可。
  • 要在模板中使用robot.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 Sprite组件

为了满足对页面上每个图标实例不重复SVG代码的要求,需要构建一个SVG“sprite”。如果以前从未听说过SVG sprite,请将其视为包含其他SVG的隐藏SVG。在需要显示图标的任何地方,都可以通过引用<use>标签内的图标ID来将其复制到sprite之外,如下所示:

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

在模板中,我们的lone <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
)

我们在这里基本上要做的是清理位于特定目录中的SVG文件,以(/assets/icons使它们处于良好的状态以在需要的任何地方使用。

removeTags参数剔除标签,我们并不需要为我们的图标,如不titlestyle。我们特别想删除title标签,因为这些标签可能会导致不必要的工具提示。如果要在图标中保留任何硬编码的样式,请添加removingTags=title作为附加参数,以便仅title删除标记。

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

最后一个加载程序参数是SVG图标文件夹的路径。然后require.context,我们提供了另外两个参数,以便它搜索子目录并仅加载SVG文件。

为了将所有SVG元素嵌套在SVG精灵中,我们必须将它们从<svg>元素转换为SVG <symbol>元素。这就像更改标签并为每个标签赋予唯一性一样简单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道具。

spin在那里添加了一个道具,可以根据需要切换.icon-spin类作为动画的可选位。例如,这对于加载微调器图标可能很有用。

<svg-icon v-if="isLoading" icon="spinner" spin />

根据需要,可能需要添加其他道具,例如rotateflip。您可以根据需要直接将类直接添加到组件中,而无需使用道具。

我们组件的大部分内容是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)
// ...