如何在Vue项目中使用SVG Icon

发布于 大漠

Web中对于Icon的使用已经是非常频繁的一件事情了,而且很有图标的使用会让你的Web应用程序或Web网页面变得更具可交互性和可使用性。早前在《Web中的图标》一文中和大家一起探讨了如何在Web中使用图标。其中不同的使用方式都具有各自的优势,但随着技术的革新,其中SVG的图标在Web中的使用也越来越频繁,并且其具备的优势也越来越明显。正因如此,在自己的项目中使用SVG的图标的场景也越来越多,因此,今天想和大家一起聊聊如何在Vue的项目中更好的使用SVG图标。

单个Icon组件的使用

在Vue官方网站专门有一节内容,向大家介绍了如何在项目中更好的使用SVG图标系统,而且这个系统是可具编辑的。先来看最基础的一个示例,这个示例是基于Vue CLI3的基础上构建的。通过官网提供的命令,使用CLI命令创建一个Vue项目,比如:

vue create svg-icon-app

采用默认的模板来构建了一个svg-icon-app项目。我们先跟着官网提供的思路来一步一步往下走,在components目录下创建一个新的目录icons。并将相应的SVG图标以一种标准化的方式命名,比如:

components/icons/IconBox.vue
components/icons/Facebook.vue
components/icons/Twitter.vue
components/icons/GooglePlus.vue

其中IconBox.vue是SVG图标的一个基础图标组件,在这个组件中使用了slot,其模板如下:

<!-- IconBox.vue -->
<template>
    <svg xmlns="http://www.w3.org/2000/svg"
        :width="width"
        :height="height"
        viewBox="0 0 18 18"
        :aria-labelledby="iconName"
        role="presentation"
    >
        <title
            :id="iconName"
            lang="en"
        >{{ iconName }} icon</title>
        <g :fill="iconColor">
            <slot />
        </g>
    </svg>
</template>

你可以像上面这样使用这个基础图标,唯一可能要做的就是根据你图标的viewBox来更新其viewBox。在基础图标组件里会有widthheighticonColor以及iconNameprops,这样我们就可以通过props对其动态更新。iconName将会同时用在<title>的内容及其用于提供可访问性的id上。

<!-- IconBox.vue -->
<script>
    export default {
        name: 'IconBox',
        props: {
            iconName: {
                type: String,
                default: 'box'
            },
            width: {
                type: [Number, String],
                default: 32
            },
            height: {
                type: [Number, String],
                default: 32
            },
            iconColor: {
                type: String,
                default: 'currentColor'
            }
        },
        data () {
            return {

            }
        }
    }
</script>

currentColor会成为fill的默认值,于是图标就会继承color的值(currentColor是CSS的一个很优秀的属性)。我们也可以根据需求传递一个不一样的颜色值。

特别声明,viewBox是SVG中的一个重要的概念,如果你从未接触过的话,建议你花点时间对该概念进行了解。你可以点击这里、或这里、还有这里进行了解。

IconBox组件完成了之后,接下来把对应的图标组件完善。

示例中的几个图标,都来自于Font Awesome提供的SVG图标。

除此之外,还可以通过iconmonstrIcoMoon平台,甚至还可以使用NucleoApp来获取SVG图标。特别是NucleoApp来管理你的SVG图标:

我们通过编辑器打开其中一个.svg文件,比如facebook.svg,其代码会是这样的:

<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="facebook-square" class="svg-inline--fa fa-facebook-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
    <path d="M29,0 L3,0 C1.35,0 0,1.35 0,3 L0,29 C0,30.65 1.35,32 3,32 L16,32 L16,18 L12,18 L12,14 L16,14 L16,12 C16,8.69375 18.69375,6 22,6 L26,6 L26,10 L22,10 C20.9,10 20,10.9 20,12 L20,14 L26,14 L25,18 L20,18 L20,32 L29,32 C30.65,32 32,30.65 32,29 L32,3 C32,1.35 30.65,0 29,0 Z" id="Path"></path>   
</svg>

我们只需要将其中<path>部分代码放到图标组件的<template>中,比如facebook.svg的放到对应的Fackbook.vue组件:

<!-- Facebook.vue -->
<template>
    <path d="M29,0 L3,0 C1.35,0 0,1.35 0,3 L0,29 C0,30.65 1.35,32 3,32 L16,32 L16,18 L12,18 L12,14 L16,14 L16,12 C16,8.69375 18.69375,6 22,6 L26,6 L26,10 L22,10 C20.9,10 20,10.9 20,12 L20,14 L26,14 L25,18 L20,18 L20,32 L29,32 C30.65,32 32,30.65 32,29 L32,3 C32,1.35 30.65,0 29,0 Z" id="Path"></path>      
</template>

<script>
    export default {
        name:'Facebook'
    }
</script>

对于GooglePlus.vueTwitter.vue组件类似。

为了更好的演示官网提供的示例,将在components目录中新创建一个.vue文件,将引用我们创建的FacebookTwitterGooglePlus组件。该文件名暂时取名为VueOfficialSvgIcons.vue,接下来我们就可以这样使用这几个SVG图标:

<!-- VueOfficialSvgIcons.vue -->
<template>
    <div class="icons">
        <IconBox icon-name="facebook"><Facebook /></IconBox>
        <IconBox icon-name="twitter"><Twitter /></IconBox>
        <IconBox icon-name="google plus"><GooglePlus /></IconBox>
    </div>
</template>

<script>
    import IconBox from './icons/IconBox'
    import Facebook from './icons/Facebook'
    import Twitter from './icons/Twitter'
    import GooglePlus from './icons/GooglePlus'
    
    export default {
        name: 'VueOfficialSvgIcons',
        components: {
            IconBox,
            Facebook,
            Twitter,
            GooglePlus
        }
    }
</script>

现在看到的效果,都是默认的尺寸32px x 32px

如果我们需要多种尺寸的图标,可以像下面这样做:

<!-- VueOfficialSvgIcons.vue -->
<IconBox 
    width="16"
    height="16"
    icon-name="facebook"><Facebook /></IconBox>

<IconBox icon-name="twitter"><Twitter /></IconBox>

<IconBox 
    width="48"
    height="48"
    icon-name="google plus"><GooglePlus /></IconBox>

得到的效果如下:

如果你需要不同的图标颜色,可以通过iconColor传一个图标的颜色:

<!-- VueOfficialSvgIcons.vue -->
<IconBox 
    icon-color="#f36"
    icon-name="facebook"><Facebook /></IconBox>

<IconBox 
    icon-color="#f0987a"
    icon-name="twitter"><Twitter /></IconBox>

<IconBox icon-name="google plus"><GooglePlus /></IconBox>

效果如果如下:

这是Vue项目中使用SVG图标最基本的方式。通过上面的示例我们可以看出来,如果你还需要更多的控制SVG图标,可以在IconBox基本组件中的props设置更多的属性。在使用的时候按需传入即可。

SVG Sprite的使用

上面这种方式,每个Icon都需要一个独立的组件。如果你的Web应用有许多图标用在不同的地方时,这种方式较为适合。如果你只在一个页面上重复使用相同图标多次,那么使用SVG Sprite更为理想。

有关于SVG Sprites的使用可以阅读《SVG Sprite》一文。

简单地说,可以通过SVG的<symbol>标签把所以SVG图标合并在一起,然后使用<use>标签来调用<symbol>中指定的图标。即 <use>中的xlink:href属性值与<symbol>标签指定的id相同。

在这里,我们来看看在Vue中怎么使用SVG Sprites来创建图标。基于上面的示例,我们先创建一个名为Icon.vue组件:

<!-- Icon.vue -->
<template>
    <svg :width="width" :height="height" :style="{color: iconColor}">
        <use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="iconId"></use>
    </svg>
</template>

<script>
    export default {
        name: 'Icon',
        props: {
            iconName: {
                type: String,
                default: 'box'
            },
            width: {
                type: [Number, String],
                default: 32
            },
            height: {
                type: [Number, String],
                default: 32
            },
            iconColor: {
                type: String,
                default: 'currentColor'
            }
        },
        computed: {
            iconId() {
                return `#icon-${this.iconName}`
            }
        }
    }
</script>

现在Icon组件是功能性的了,我们需要在src/mail.js中注册到Vue实例中:

<!-- src/main.js -->
import iconComponent from './components/svgSprites/Icon'
Vue.component('icon', iconComponent)

为了区分前面的示例,这里为SVG Sprites单独创建一个独立的组件,比如SvgSpritesIcon.vue组件,然后再这个组件中调用:

<icon icon-name="facebook"></icon>
<icon icon-name="twitter"></icon>
<icon icon-name="google"></icon>

但在页面中并不会看到有任何的Icon。这主要是因为我们还没有创建SVG Sprites。暂时我们人肉创建一个SVG Sprites,比如在App.vue文件中创建一个SVG Sprites:

<!-- App.vue -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
    <symbol viewBox="0 0 32 32" id="icon-facebook" fill="currentColor">
        <path d="M29,0 L3,0 C1.35,0 0,1.35 0,3 L0,29 C0,30.65 1.35,32 3,32 L16,32 L16,18 L12,18 L12,14 L16,14 L16,12 C16,8.69375 18.69375,6 22,6 L26,6 L26,10 L22,10 C20.9,10 20,10.9 20,12 L20,14 L26,14 L25,18 L20,18 L20,32 L29,32 C30.65,32 32,30.65 32,29 L32,3 C32,1.35 30.65,0 29,0 Z"></path>      
    </symbol>
    <symbol viewBox="0 0 32 32" id="icon-twitter" fill="currentColor">
        <path d="M28.5714286,0 L3.42857143,0 C1.53571429,0 0,1.53571429 0,3.42857143 L0,28.5714286 C0,30.4642857 1.53571429,32 3.42857143,32 L28.5714286,32 C30.4642857,32 32,30.4642857 32,28.5714286 L32,3.42857143 C32,1.53571429 30.4642857,0 28.5714286,0 Z M25.0785714,11.3428571 C25.0928571,11.5428571 25.0928571,11.75 25.0928571,11.95 C25.0928571,18.1428571 20.3785714,25.2785714 11.7642857,25.2785714 C9.10714286,25.2785714 6.64285714,24.5071429 4.57142857,23.1785714 C4.95,23.2214286 5.31428571,23.2357143 5.7,23.2357143 C7.89285714,23.2357143 9.90714286,22.4928571 11.5142857,21.2357143 C9.45714286,21.1928571 7.72857143,19.8428571 7.13571429,17.9857143 C7.85714286,18.0928571 8.50714286,18.0928571 9.25,17.9 C7.10714286,17.4642857 5.5,15.5785714 5.5,13.3 L5.5,13.2428571 C6.12142857,13.5928571 6.85,13.8071429 7.61428571,13.8357143 C6.30917726,12.9675977 5.52600528,11.5031734 5.52857143,9.93571429 C5.52857143,9.06428571 5.75714286,8.26428571 6.16428571,7.57142857 C8.47142857,10.4142857 11.9357143,12.2714286 15.8214286,12.4714286 C15.1571429,9.29285714 17.5357143,6.71428571 20.3928571,6.71428571 C21.7428571,6.71428571 22.9571429,7.27857143 23.8142857,8.19285714 C24.8714286,7.99285714 25.8857143,7.6 26.7857143,7.06428571 C26.4357143,8.15 25.7,9.06428571 24.7285714,9.64285714 C25.6714286,9.54285714 26.5857143,9.27857143 27.4285714,8.91428571 C26.7928571,9.85 25.9928571,10.6785714 25.0785714,11.3428571 Z"></path>        
    </symbol>
    <symbol viewBox="0 0 32 32" id="icon-google" fill="currentColor">
        <path d="M28.5714286,0 L3.42857143,0 C1.53571429,0 0,1.53571429 0,3.42857143 L0,28.5714286 C0,30.4642857 1.53571429,32 3.42857143,32 L28.5714286,32 C30.4642857,32 32,30.4642857 32,28.5714286 L32,3.42857143 C32,1.53571429 30.4642857,0 28.5714286,0 Z M11.7142857,23.1428571 C7.76428571,23.1428571 4.57142857,19.95 4.57142857,16 C4.57142857,12.05 7.76428571,8.85714286 11.7142857,8.85714286 C13.6428571,8.85714286 15.25,9.55714286 16.5,10.7285714 L14.5642857,12.5928571 C14.0357143,12.0857143 13.1142857,11.4928571 11.7214286,11.4928571 C9.28571429,11.4928571 7.3,13.5071429 7.3,16.0071429 C7.3,18.5 9.28571429,20.5214286 11.7214286,20.5214286 C14.55,20.5214286 15.6071429,18.4857143 15.7785714,17.4428571 L11.7142857,17.4428571 L11.7142857,14.9857143 L18.4571429,14.9857143 C18.5285714,15.3428571 18.5714286,15.7071429 18.5714286,16.1714286 C18.5714286,20.25 15.8357143,23.1428571 11.7142857,23.1428571 Z M27.4285714,17.3 L25.3571429,17.3 L25.3571429,19.3714286 L23.2714286,19.3714286 L23.2714286,17.3 L21.2,17.3 L21.2,15.2142857 L23.2714286,15.2142857 L23.2714286,13.1428571 L25.3571429,13.1428571 L25.3571429,15.2142857 L27.4285714,15.2142857 L27.4285714,17.3 Z"></path> 
    </symbol>
</svg>

此时,你刷新你的页面,可以看到图标,如下图所示:

和前面的示例一样,我们可以通过props中的widthheighticonColor来设置图标的大小和颜色,比如:

<icon icon-name="facebook"></icon>
<icon icon-name="twitter" width="48" height="48"></icon>
<icon icon-name="google" width="64" height="64" icon-color="green"></icon>

此时的效果如下:

上面的示例中,虽然达到我们想要的效果。但SVG Sprite是我们人肉生成的。其实,我们可以借助相关的工具链,自动生成SVG Sprite。比如:

这些工具会在编译时打包 SVG,但是在运行时编辑它们会有一些麻烦,因为 <use> 标签在处理一些复杂的事情时存在浏览器兼容问题。同时它们会给你两个嵌套的 viewBox 属性,这是两套坐标系。所以实现上稍微复杂了一些。

接下来我们以svg-sprite-loader为例,看看怎么自动生成SVG Sprite。我先从网上下载几个SVG的图标放在项目src/assets/icons/下:

注意,我们的环境是Vue Cli3下。

先进行安装:

npm i svg-sprite-loader -D

现在我们需要做一些配置。Vue Cli内部是利用webpack-chain插件来修改Webpack配置的。所以我们需要在项目根目录下创建一个Vue.config.js文件,在该文件中利用webpack-chain来修改相关的Webpack配置。比如:

// Vue.config.js
const path = require('path')

function resolve(dir) {
    return path.join(__dirname, '.', dir)
}

module.exports = {
    chainWebpack: config => {
        config.module.rules.delete('svg')

        config.module
            .rule('svg-sprite-loader')
            .test(/\.svg$/)
            .include
            .add(resolve('src/assets/icons')) // svg图标的路径
            .end()
            .use('svg-sprite-loader')
            .loader('svg-sprite-loader')
            .options({
                symbolId: 'icon-[name]' // 设置svg中symbol中id的值
            })
    }
}

前面也提到过了,使用SVG Sprite,需要有一个模板,比如/src/components/svgSprites/Icon.vue组件:

 <!-- /src/components/svgSprites/Icon.vue -->
<template>
    <svg :width="width" :height="height" :style="{color: iconColor}">
        <use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="iconId"></use>
    </svg>
</template>

<script>
    export default {
        name: 'Icon',
        props: {
            iconName: {
                type: String,
                default: 'box'
            },
            width: {
                type: [Number, String],
                default: 32
            },
            height: {
                type: [Number, String],
                default: 32
            },
            iconColor: {
                type: String,
                default: 'currentColor'
            }
        },
        computed: {
            iconId() {
                return `#icon-${this.iconName}`
            }
        }
    }
</script>

虽然配置和模板都有了,但要使用还是需要人肉的引用.svg文件。所以我们借助Webpack的一些功能,来自动帮我们引入。接下来在/src目录下创建一个utils/svg-icons目录,并在该目录下创建一个index.js

// src/utils/svg-icons/index.js
import Vue from 'vue'

import iconSpriteLoadComponent from '../../components/svgSprites/Icon.vue'

Vue.component('icon',iconSpriteLoadComponent)

const requireAll = requireContext => requireContext.keys().map(requireContext)
const req = require.context('../../assets/icons', false, /\.svg$/)

requireAll(req)

上面这段代码主要是用于注册Icon组件和批量引入.svg文件。有了这些之后,我们就可以像下面这样使用:

<icon icon-name="close"></icon>
<icon icon-name="love"></icon>
<icon icon-name="star" width="48" height="48"></icon>

将看到的效果如下:

通过Icon.vue组件中的props定义的属性,将相关的值传入进去。同样可以设置大小,但这里有一个小细节,如果你想像前面的示例通过iconColor来给图标传递相应的颜色时会失效,比如:

<icon icon-name="location" width="24" height="24" icon-color="red"></icon>
<icon icon-name="collection" width="48" height="48" icon-color="lime"></icon>

颜色并没有重置过来:

那是因为,我们的.svg文件中path标签中的fill有一个默认值:

如果要让iconColor值生效,我们需要将.svg中的fill(和stroke)属性的值设置为**currentColor**。修改之后,你就能看到图标有相应的颜色了:

这里是通过人肉去修改每个.svg文件中fillstroke属性,即将其值修改为currentColor。不知道有没有相应的工具,能在合并成SVG Sprite时,自动将fillstroke的值更换为currentColor。找了一圈,没找到。如果您在这方面有相应的经验,欢迎分享出您的方案。

上面是在Vue Cli3环境中的使用,如果你是自己的构建系统,那么可以借助Webpack相关配置,达到相应的效果。只需要修改/build/webpack.base.conf.js文件:

{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    exclude: [resolve('src/assets/icons')], // 新增加
    loader: 'url-loader',
    options: [
        limit: 10000,
        name: utils.assetsPath('img/[name].[hash:7].[ext]')
    ]
}

// 另外增加
{
    test: /\.svg$/,
    loader: 'svg-sprite-loader',
    include: [resolve('src/assets/icons')],
    options: [
        symbolId: 'icon-[name]'
    ]
}

其他地方可以不修改。

注册事项

虽然SVG图标在Web中的运用越来越广泛,但是在使用和设计的时候还是需要注意一些细节,比如.svg文件的优化。大家都知道,用文本编辑器打开一个.svg文件,将看到一大坨的代码,如果你不知道什么可删除,什么不可删除,那么可以借助SVGOMG来优化:

代码上的变化如下:

另外有一点也需要特别注意,那就是SVG 图示的绘制。虽然在很多平台上能直接下载到自己需要的SVG图标,但有时候为了更适合自己的项目,设计师会为自己提供一些更具个性化的SVG图标,这个时候,需要一套较为标准的绘制规则和流程。有关于这方面可以参考iconfont上的图标绘制规则和制作流程

写在最后

有关于在Vue项目中使用SVG的方案和相关的组件库在Github上非常的多。大家可以选择适合自己的组件,如果你喜欢Font Awesome图标的话,那么@E0 大大的Vue-Awesome就是一个不错的选择。

最后再说一点,这篇文章主要是亲测了在Vue项目中怎么灵活或者更好的使用SVG图标。文章开头跟着Vue官网提供的方式,做了一些尝试,感觉较为简单,特别适合那种,项目中运用图标不多的场景,可以为每个图标创建一个独立的组件,不需要全局注册,也可以在任何需要的地方使用。如果图标较多时,管理起来有点蛋疼而以。文章第二部分,介绍如何自动化生成SVG Sprite,并且在相应的脚手架中运用。

可能文章中有不对或者不好的地方,如果你有这方面的经验,欢迎拍正和分享。文章中示例代码可以在Github中获取到Sneaker Bar Detroit (SBD)