前言
本文撰写的初衷是为了向组内成员推行使用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
主要基于两个标签元素:和
.
对元素进行分组,它不会显示在界面上,相当于定义一个模板.
元素用于引用并渲染图标.
例如存在以下某个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官网 ,选择好自己的项目中要用到的图标,鼠标移动到图标上点击添加入库
.
图标收集完后点击头部右侧导航栏的购物车,出现弹出框,点击添加至项目
.所有图标确定添加到项目后,页面会自动跳转到导航栏资源管理--我的项目
下的页面(如下图).
我们的目标是为了生成图标对应的svg
文件,这里要做一下设置.打开上图中的项目设置
选项,弹框打开后如下图.
弹框字体格式
的那一栏,只保留SVG
勾选项,其他都取消,设置好后点击保存按钮.
页面此时会刷新一遍,然后点击页面上的下载至本地
的按钮,将所有图标的svg
文件下载下来并解压,解压后的文件结构如下图.
观察上图中的文件结构,我们发现所有svg
图标的代码全部都写在iconfont.svg
这一个文件,这并不符合预期.我们希望的结果是一个图标对应一个svg
文件,而不是像现在一样全部揉进了一个文件内.
虽然iconfont
目前没有提供文件分离的机制,但是我们可以借助其他平台帮我们将融合的svg
文件分离成单个文件. iconmoon
网站便具备这个功能,它也是一家和iconfont
类似的图标库网站.
浏览器点击打开 iconmoon官网 ,选择顶部导航栏右侧的IcoMoon App
进入图标选择页面,点击页面头部导航栏左侧的Imports Icons
,将从iconfont
下载的iconfont.svg
文件导入,结果如下图.
在iconfont
那一栏可以看到我们导进去的图标展现到了页面上,接下来使用鼠标单击导进去的图标将其标记为选中,再点击页面左下角的Generate SVG & More
按钮(如下图).
按钮点击后页面跳转,此时依旧点击左下角的Download
按钮(如下图)下载图标.
下载完成后解压目录,解压后的目录下出现了SVG
文件夹,打开该文件夹会发现所有图标都被分离成了单个文件(如下图).
项目设置
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
和.loader
将svg-sprite-loader
配置到项目环境里,并设置symbolId
为icon-[name]
.
这里的symbolId
关乎到标签生成的`id`名称.如果设置`symbolId`为`icon-[name]`,那么最后页面上
标签引用图标时就会使用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
文件夹下就可以完成自动引入了.
- 最后一步在项目的入口文件
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
的代码内容全部都封装到一个个``标签里面(如下图),再一起插入到页面文档中.
这将相当于svg-sprite-loader
帮助我们将所有的svg
图标在页面上注册了,而我们剩下的事情就是在页面上去引用图标就行了.
图标引用
我们在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>
组件引用
页面上使用svg
、use
标签引用图标的方式不太优雅,我们可以将它改造成组件.
在全局组件文件夹components
下新建文件Icon/index.vue
.该组件接受两个参数name
和color
(代码如下).
参数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>
多主题支持
通过上文讲解可知,当我们给``标签赋予样式属性fill
,最终图标的颜色也会发生改变,这就为我们完成多主题的开发需求提供了可能.
我们接下来搭建一个点击按钮在线切换主题的场景,让svg
图标也能随着主题的变换而改变.
配置多主题样式
首选在项目文件夹src/assets
下新建文件scss/variable.scss
,代码内容如下.
代码定义了三个主题,分别为默认主题
、主题1
和主题2
.每个主题都定义了自己主题下的图标颜色和背景颜色.
代码下半部分定义了3
个mixin
,分别用来设置fill
、color
和background-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
版本的不同导致文件引入失败,统一一下sass
和sass-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.scss
对fill
的定义如下面代码.
它最后返回一个属性 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
最终会将每个主题下的样式都生成了一份(如下图),这样一来只要``标签的data-theme
等于哪个主题,对应主题的样式表就会生效.
页面校验
Home
页面组件填写如下代码.在原来页面基础上新增了三个按钮默认主题
、主题1
和主题2
.
点击按钮触发updateTheme
函数,函数会修改``标签上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>
最终效果图:
尾言
上文介绍的多主题实现方案操作起来非常简单,但不适合用于大型复杂的项目.
试想一下,如果一个大型项目包含十几种主题,而每一类主题下的css
代码十分庞大,一次性将所有主题下的样式代码全部注入到应用里是不合适的.
最佳实践应该是用户点击切换某一类主题时,就按需加载那一类主题的样式,再注入到应用中渲染,这样可以极大的提升整体性能.最佳实践方式可以参考社区内关于多主题切换的文章.