vue引入iconfont的优雅实践

3,443 阅读11分钟

前言

本文撰写的初衷是为了向组内成员推行使用svg sprites的方式管理项目的图标,由于实际工作中很多项目仍然采用font class的方式,这样不自觉带来一个痛点.

当项目一期开发完毕后,过段时间进入到项目二期。新增的开发需求不可避免的会增加新的图标,而font class需要全量打包图标的字体文件.

哪怕新需求只添加了一个图标,而前端同学却要将旧图标和新图标融合后重新打包生成一次字体文件,这样的结果让人无法接受.

svg sprites能完美的解决这一问题.整体思路是先将项目中每一个图标都生成一个svg文件与之对应,那么有多少个svg文件就相当于对应了多少个图标.

以后如果想新增一个图标,那么只需要添加一个新svg文件即可.那些已经存在的图标和svg文件则不需要再参与进来.

本文接下来将以vue3为基础框架,iconfont为图标库,一步步实践图标引入,使用以及管理的整个流程.另外在文章的后半部分,还会介绍一下多主题变色模式下svg图标的相应处理.

生成SVG

svg sprites简介

svg sprites这项技术很早就出来了,具体详情可以点击查看张鑫旭在2014年写的文章 未来必热:SVG Sprites技术介绍.

我们这里做一下简单介绍就进入实践阶段.svg sprites主要基于两个标签元素:<symbol><use>.

<symbol>对元素进行分组,它不会显示在界面上,相当于定义一个模板.<use>元素用于引用并渲染图标.

例如存在以下某个svg图标(代码如下),它是一个爱心的形状.

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24">
        <path fill="#E86C60" d="M17,0c-1.9,0-3.7,0.8-5,2.1C10.7,0.8,8.9,0,7,0C3.1,0,0,3.1,0,7c0,6.4,10.9,15.4,11.4,15.8 c0.2,0.2,0.4,0.2,0.6,0.2s0.4-0.1,0.6-0.2C13.1,22.4,24,13.4,24,7C24,3.1,20.9,0,17,0z"></path>
</svg>

现在使用symbol标签将上面的path内容包裹一层,代码如下:

<svg>
    <symbol viewBox="0 0 24 24" id="heart">
        <path fill="#E86C60" d="M17,0c-1.9,0-3.7,0.8-5,2.1C10.7,0.8,8.9,0,7,0C3.1,0,0,3.1,0,7c0,6.4,10.9,15.4,11.4,15.8 c0.2,0.2,0.4,0.2,0.6,0.2s0.4-0.1,0.6-0.2C13.1,22.4,24,13.4,24,7C24,3.1,20.9,0,17,0z"></path>
    </symbol>
</svg>

接下来把symbol包裹后的代码放入页面中(代码如下),再添加一个display: none隐藏起来.这就相当于在页面上注册了一个id名为heart的图标.

此时页面的其他部分就可以引用这个图标,引用方式是在svg标签里面放入一个use标签,use标签的xlink:href填上要引用的图标id,界面就会渲染出爱心的形状.

<body>
    <svg style="display: none;">
        <symbol viewBox="0 0 24 24" id="heart">
            <path fill="#E86C60" d="M17,0c-1.9,0-3.7,0.8-5,2.1C10.7,0.8,8.9,0,7,0C3.1,0,0,3.1,0,7c0,6.4,10.9,15.4,11.4,15.8 c0.2,0.2,0.4,0.2,0.6,0.2s0.4-0.1,0.6-0.2C13.1,22.4,24,13.4,24,7C24,3.1,20.9,0,17,0z"></path>
        </symbol>
    </svg>

    <svg>
        <use xlink:href="#heart"/> <!-- 使用图标 -->
    </svg>
</body>

获取项目图标

前端同学拿到设计图之后,通常会整体浏览一遍整个项目需要用到的所有图标.

iconfont是阿里巴巴体验团队倾力打造的矢量图标库,里边包含海量的图标可供前端工程师选择和使用.

浏览器点击打开 iconfont官网 ,选择好自己的项目中要用到的图标,鼠标移动到图标上点击添加入库.

图标收集完后点击头部右侧导航栏的购物车,出现弹出框,点击添加至项目.所有图标确定添加到项目后,页面会自动跳转到导航栏资源管理--我的项目下的页面(如下图).

1.png

我们的目标是为了生成图标对应的svg文件,这里要做一下设置.打开上图中的项目设置选项,弹框打开后如下图.

2.png

弹框字体格式的那一栏,只保留SVG勾选项,其他都取消,设置好后点击保存按钮.

页面此时会刷新一遍,然后点击页面上的下载至本地的按钮,将所有图标的svg文件下载下来并解压,解压后的文件结构如下图.

3.png

观察上图中的文件结构,我们发现所有svg图标的代码全部都写在iconfont.svg这一个文件,这并不符合预期.我们希望的结果是一个图标对应一个svg文件,而不是像现在一样全部揉进了一个文件内.

虽然iconfont目前没有提供文件分离的机制,但是我们可以借助其他平台帮我们将融合的svg文件分离成单个文件. iconmoon 网站便具备这个功能,它也是一家和iconfont类似的图标库网站.

浏览器点击打开 iconmoon官网 ,选择顶部导航栏右侧的IcoMoon App进入图标选择页面,点击页面头部导航栏左侧的Imports Icons,将从iconfont下载的iconfont.svg文件导入,结果如下图.

iconfont那一栏可以看到我们导进去的图标展现到了页面上,接下来使用鼠标单击导进去的图标将其标记为选中,再点击页面左下角的Generate SVG & More按钮(如下图).

4.png

按钮点击后页面跳转,此时依旧点击左下角的Download按钮(如下图)下载图标.

5.png

下载完成后解压目录,解压后的目录下出现了SVG文件夹,打开该文件夹会发现所有图标都被分离成了单个文件(如下图).

6.png

项目设置

svg文件顺利获取到了,现在在vue3项目目录结构src -> assets文件夹下新建文件夹fonts和子文件夹fonts/svg,将上面生成的所有svg单文件扔到fonts/svg下面.

文件的设置完成,现在开始项目的配置,让vue3能顺利的管理和使用图标.

  • 第一步在项目根目录下打开命令行运行npm i svg-sprite-loader -D.我们之所以要安装依赖svg-sprite-loader,因为它能将svg文件的代码自动塞到一个个symbol标签中.

  • 第二步项目根目录下新建文件vue.config.js,熟悉vue的同学应该知道vue.config.js用来配置构建环境.

vue.config.js详细配置参数可点击查询 vue-cli官网 ,我们这里只需要知道如何配置svg-sprite-loader就可以.

众所周知,vue-cli的构建环境基于webpack,我们通过在vue.config.js文件中添加各类配置参数,vue-cli最终会将这些参数合并到webpack的配置里.

如此一来我们通过vue.config.js就能达到配置开发环境的目的,而不用直接去操作webpack的配置文件.

当前已经安装了依赖svg-sprite-loader,现在要把这个loader载入到webpack的配置中,通过在vue.config.js填写下面代码便可实现.

const resolve = require("path").resolve;

module.exports = {
   chainWebpack(config){
        //引入图标
        config.module.rule("svg").exclude.add(resolve("./src/assets/fonts/svg"));
        config.module.rule("icon").test(/\.svg$/)
        .include.add(resolve("./src/assets/fonts/svg")).end()
        .use("svg-sprite-loader")
        .loader("svg-sprite-loader")
        .options({
            symbolId:'icon-[name]'
        });
   }
}

系统学习过webpack配置的同学很容易能看出来上面代码的含义,上方代码首先将rule中设置的svg规则排除"./src/assets/fonts/svg"目录.

然后新增加一条规则icon"./src/assets/fonts/svg"目录包含了进去,这个目录就是我们存放所有svg文件的文件夹.

代码接下来使用.use.loadersvg-sprite-loader配置到项目环境里,并设置symbolIdicon-[name].

这里的symbolId关乎到<symbol>标签生成的id名称.如果设置symbolIdicon-[name],那么最后页面上<use>标签引用图标时就会使用icon-加上文件名.

  • 第三步在assets/fonts下面新建文件index.js(文件结构如下图),并填写下面两行代码.

这两行代码主要使用了webpack中的 require.context函数,它可以帮助我们自动引入文件模块.

require.context第一个参数代表目标文件目录,第二个参数是否应用于子文件夹,第三个参数匹配文件格式.

const load = require.context("./svg",false,/\.svg$/);
load.keys().map(load);

require.context执行完毕后返回结果load,返回值load本身就是一个引入模块的函数,另外它还包含一个keys属性,执行load.keys()返回结果如下.

 ["./arrow.svg", "./arrowon.svg", "./downarrow.svg", "./jiantou.svg", "./trash.svg", "./yiwenicon.svg"]

我们从这里可以看出load.keys()会返回fonts/svg文件夹下所有图标的相对路径,再使用loda函数去加载这些路径的文件,这样便实现了动态引入fonts/svg文件夹下的所有以.svg结尾的文件.

那么以后如果出现新增一个图标的需求,先在iconfont网站上下载单个svg文件,下载完成后直接丢到fonts/svg文件夹下就可以完成自动引入了.

7.png

  • 最后一步在项目的入口文件main.js调用第三步新建的index.js,执行所有svg文件的自动引入(代码如下).
import { createApp } from 'vue';
import App from './App.vue'; // 根组件
import "@/assets/fonts/index"; // 执行自动引入
import router from '@/router/index'; // 路由

createApp(App).use(router).mount('#app');

通过以上四步基本完成了项目的配置,整个运行过程可以做一下简单的梳理.

入口文件main.js启动后,执行assets/fonts/index.js启动所有svg文件的自动引入.

svg-sprite-loader一旦监听到项目中引入了以.svg结尾的文件,它就会把这些svg的代码内容全部都封装到一个个<symbol>标签里面(如下图),再一起插入到页面文档中.

这将相当于svg-sprite-loader帮助我们将所有的svg图标在页面上注册了,而我们剩下的事情就是在页面上去引用图标就行了.

8.png

图标引用

我们在Home主页下填写如下代码(效果图如下).将#icon-前缀加上fonts/svg下对应的文件名拼接而成的字符串赋予xlink:href属性,那么就会渲染出该文件对应的图标.

<template>
  <div class="home">
    <p class="title">Hello world</p>
    <svg>
        <use xlink:href="#icon-trash"/> <!-- 使用图标 -->
    </svg>
  </div>
</template>

9.png

组件引用

页面上使用svguse标签引用图标的方式不太优雅,我们可以将它改造成组件.

在全局组件文件夹components下新建文件Icon/index.vue.该组件接受两个参数namecolor(代码如下).

参数name对应要渲染的图标名称,color为需要渲染的颜色.这里需要格外注意,svg的颜色修改只能通过fill属性,color属性赋值时不奏效.

<template>
    <svg :style="{fill:color?color:''}">
        <use :xlink:href="'#icon-'+name"/>
    </svg>
</template>

<script>
export default {
 props:{
     name:String, //图标名称
     color:{ // 图标颜色
         type:String,
         deafult:null
     }
 }
}
</script>

现在在Home页面引用Icon组件(代码如下).

渲染的图标名称为trash,颜色为蓝色(效果图如下).

<template>
  <div class="home">
    <p class="title">Hello world</p>
    <Icon name="trash" color="blue"/><!-- 使用图标 -->
  </div>
</template>
<script>
import Icon from "@/components/Icon/index";
export default {
    components:{
        Icon
    }
}
</script>

10.png

多主题支持

通过上文讲解可知,当我们给<svg>标签赋予样式属性fill,最终图标的颜色也会发生改变,这就为我们完成多主题的开发需求提供了可能.

我们接下来搭建一个点击按钮在线切换主题的场景,让svg图标也能随着主题的变换而改变.

配置多主题样式

首选在项目文件夹src/assets下新建文件scss/variable.scss,代码内容如下.

代码定义了三个主题,分别为默认主题主题1主题2.每个主题都定义了自己主题下的图标颜色和背景颜色.

代码下半部分定义了3mixin,分别用来设置fillcolorbackground-color属性.在每一个mixin里面,不同主题设置的颜色采用自己主题下的颜色设置.

// 默认主题
$icon-color:red;
$background-color:#fff;

// 主题1
$icon-color1:gray;
$background-color1:#eee;

// 主题2
$icon-color2:blue;
$background-color2:#999;

// 用于给svg填充颜色
@mixin fill {
    fill:$icon-color; //默认颜色用默认主题
    [data-theme = "theme1"] & { //切换到主题1时的颜色
        fill:$icon-color1;
    }
    [data-theme = "theme2"] & { //切换到主题2时的颜色
        fill:$icon-color2;
    }
}
//设置color属性
@mixin color {
    color:$icon-color; //默认颜色用默认主题
    [data-theme = "theme1"] & { //切换到主题1时的颜色
        color:$icon-color1;
    }
    [data-theme = "theme2"] & { //切换到主题2时的颜色
        color:$icon-color2;
    }
}

//设置背景颜色
@mixin backgroudColor {
    background-color:$background-color; //默认颜色用默认主题
    [data-theme = "theme1"] & { //切换到主题1时的颜色
        background-color:$background-color1;
    }
    [data-theme = "theme2"] & { //切换到主题2时的颜色
        background-color:$background-color2;
    }
}

variable.scss是一份全局多主题配置文件,该文件内不光可以配置各个主题下应该渲染的颜色,还可以配置字体大小,常用宽高等.

配置文件编写完成后,现在要将这份文件引用到项目当中.编辑器打开根目录下vue.config.js项目配置文件,新增代码如下.

const resolve = require("path").resolve;

module.exports = {
   chainWebpack(config){
        //引入图标
        config.module.rule("svg").exclude.add(resolve("./src/assets/fonts/svg"));
        config.module.rule("icon").test(/\.svg$/)
        .include.add(resolve("./src/assets/fonts/svg")).end()
        .use("svg-sprite-loader")
        .loader("svg-sprite-loader")
        .options({
            symbolId:'icon-[name]'
        });
   },
   css: {
    loaderOptions: {
        scss: {
            prependData: `@import "@/assets/scss/variable.scss";`
        },
    }
   } 
}

module.exports新增配置属性css,随后将我们在上面编写多主题配置文件的路径填入到prependData对应的值.

这里为了避免因为sass版本的不同导致文件引入失败,统一一下sasssass-loader的版本.

"sass": "1.26.5",
"sass-loader": "8.0.2",

vue.config.js配置完成后重启应用,variable.scss已经全局注入了应用.接下来我们在页面组件内不需要使用@import导入主题配置文件,variable.scss里面定义的变量和mixin可以直接拿来使用.

Icon改造

为了让图标响应主题的变换,全局的Icon组件做如下代码修改.color属性如果有传值,那么图标就按照传入的颜色渲染.如果没有传color,那么决定图标颜色的因素变成了类名icon.

<template>
    <svg class="icon" :style="{fill:color?color:''}">
        <use :xlink:href="'#icon-'+name"/>
    </svg>
</template>

<script>
export default {
 props:{
     name:String,
     color:{
         type:String,
         deafult:null
     }
 }
}
</script>

<style lang="scss" scoped>
 .icon{
   @include fill;
 }
</style>

类名 icon 里面调用 fill对应的mixin,文件variable.scssfill的定义如下面代码.

它最后返回一个属性 fill:color.默认情况下fill的颜色值为$icon-color.

当页面文档html标签上的属性data-theme值变成theme1时,fill渲染的颜色变成了主题1定义的颜色.同理切换到theme2,fill渲染的颜色变成了主题2定义的颜色.

// 用于给svg填充颜色
@mixin fill {
    fill:$icon-color; //默认颜色用默认主题
    [data-theme = "theme1"] & { //切换到主题1时的颜色
        fill:$icon-color1;
    }
    [data-theme = "theme2"] & { //切换到主题2时的颜色
        fill:$icon-color2;
    }
}

我们观察一下页面最终生成的dom结构就可以理解上面配置的目的,@mixin最终会将每个主题下的样式都生成了一份(如下图),这样一来只要<html>标签的data-theme等于哪个主题,对应主题的样式表就会生效.

11.png

页面校验

Home页面组件填写如下代码.在原来页面基础上新增了三个按钮默认主题主题1主题2.

点击按钮触发updateTheme函数,函数会修改<html>标签上data-theme的属性值,从而实现了主题切换的功能(效果图如下).

<template>
  <div class="home">
    <p class="title">Hello world</p>
    <Icon name="trash"/><!-- 使用图标 -->
    <button @click="updateTheme()">默认主题</button>
    <button @click="updateTheme('theme1')">主题1</button>
    <button @click="updateTheme('theme2')">主题2</button>
  </div>
</template>
<script>
import Icon from "@/components/Icon/index";
export default {
    components:{
        Icon
    },
    methods: {
        updateTheme(name){
            if(name == null){ // 采用默认主题
                document.documentElement.removeAttribute("data-theme");
            }else{
                document.documentElement.setAttribute("data-theme",name);
            }
        } 
    },
}
</script>
<style scoped lang="scss">
.home{
    height: 100%;
    @include backgroudColor;
}
.title{
  @include color;
}
</style>

最终效果图:

12.gif

尾言

上文介绍的多主题实现方案操作起来非常简单,但不适合用于大型复杂的项目.

试想一下,如果一个大型项目包含十几种主题,而每一类主题下的css代码十分庞大,一次性将所有主题下的样式代码全部注入到应用里是不合适的.

最佳实践应该是用户点击切换某一类主题时,就按需加载那一类主题的样式,再注入到应用中渲染,这样可以极大的提升整体性能.最佳实践方式可以参考社区内关于多主题切换的文章.

源代码