管理前端项目中的图标
前端项目中,图标管理是一个前期容易被忽略,但后期维护比较困难的一个地方,最后得到的结果就是项目中散落着各种各样的图标,也无法轻易移除。
本文决定讨论一下这个问题,在研究了各种前端图标方案后,决定试试逐渐流行起来的 svg 图标组件方案。
历史方案
原始图片
可以通过img
或background-image
的方式使用图片图标
@mixin image-icon($w, $h, $img) {
width: $w;
height: $h;
background: #{$img} no-repeat center;
background-size: contain;
}
.icon-user {
@include (20px, 20px, url("~@/assets/icons/user.png"));
}
每个图片都会发送网络请求,比较浪费资源,
- 一种方式是通过
url-loader
等工具将较小的图片通过 base64 内联代码中,节省 http 请求,但会导致输出文件体积增大 - 更普遍的做法是使用精灵图
sprite image
,将所有图标放在一张 png 图片中,然后通过background-position
来控制图标的展示
优点
- 实现简单,基本没有兼容问题
缺点
- 图片无法自由缩放,可能需要设计导出多份 N 倍图避免图片模糊
- 无法定制颜色,每种状态的图标都需要对应的图片
- 精灵图需要特殊的工具生成,手动编写维护图片位置工作量比较大
字体图标
下面是两个比较著名的字体图标网站
- fontawesome,一套绝佳的图标字体库和 CSS 框架
- iconfont,阿里巴巴矢量图标库
选择图标添加到项目,最后会得到一个字体集
@font-face {
font-family: "iconfont"; /* Project id 3901921 */
src: url("//at.alicdn.com/t/c/font_3901921_n5nd4y2lpes.woff2?t=1676621012694")
format("woff2"), url("//at.alicdn.com/t/c/font_3901921_n5nd4y2lpes.woff?t=1676621012694")
format("woff"),
url("//at.alicdn.com/t/c/font_3901921_n5nd4y2lpes.ttf?t=1676621012694")
format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-a-yxy-gender-lined:before {
content: "\eb19";
}
然后使用对应的样式类就可以了
<i class="iconfont icon-a-yxy-gender-lined" />
字体图标的另外一种使用方式是unicode
,这种用法缺少语义性,目前已经很少使用了
<i class=iconfont></>
优点
- 方便控制大小和图标颜色,不用切两张图
- 使用方便,兼容性好,甚至在 iOS、Android 或 Flutter 项目中都可以使用
缺点
- 不支持多色彩图标,因为字体颜色是应用于整个图标的
- 需要开发者记住每个图标的名字,维护起来不是很方便
svg 图标
参考
- SVG 图标,这篇文章写得比较全
首先 svg 可以像一个普通的png
、jpg
一样作为图片文件去使用
比如在 css 中作为背景图
.icon-user {
background: url("./assets/user.svg");
}
或者在 html 中作为img
标签的 src
<img src="./assets/user.svg" />
手动引入 svg 文件比较繁琐,因此我们需要一些自动引入 svg 图标的方式
社区提供了诸如 webpack 的svg-sprite-loader、vite 的vite-plugin-svg-icons等插件,自动引入 svg 图标
以vite-plugin-svg-icons
为例
// vite.cofnig.ts
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
export default defineConfig({
plugins: [
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), "./src/assets/icons")],
symbolId: "icon-[dir]-[name]",
customDomId: "__svg__icons__dom__",
}),
],
});
其原理是加载指定目录下的所有 svg 图标,通过 symbol 来定义一个图形模板对象并插入到页面 body 中
<svg>
<symbol id="icon-home" viewBox="0 0 16 16">
<!-- 对应内容 -->
</symbol>
<symbol id="icon-user" viewBox="0 0 16 16">
<!-- 对应内容 -->
</symbol>
</svg>
然后就可以通过 use 拿到对应 id 的 svg 图标了
<svg aria-hidden="true" class="svg-icon">
<use :href="symbolId" :fill="color"/>
</svg>
重复编写<svg><use :href="#icon-home" /></svg>
的方式也略显重复,因此可以封装成组件
<template>
<svg aria-hidden="true" class="svg-icon">
<use :href="symbolId" :fill="color" />
</svg>
</template>
<script lang="ts" setup>
import { computed } from "vue";
type Props = {
prefix?: string;
name: string;
color?: string;
};
const props = withDefaults(defineProps<Props>(), {
prefix: "icon",
color: "currentcolor",
});
const symbolId = computed(() => {
return `#${props.prefix}-${props.name}`;
});
</script>
<style lang="scss" scoped>
.svg-icon {
width: 1em !important;
height: 1em !important;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
使用的时候就只需要传图标的名字就行了
<svg-icon name="user"></svg-icon>
优点
- 矢量图,无损缩放
- 控制非常方便,定制颜色、尺寸等,可以实现多色彩图标
- 可以拿来实现 CSS 动画
缺点
- 需要开发者记住每个图标的名字,维护起来不是很方便,后续也不太知道哪些图标已经废弃使用了
- svg 存在兼容性,和一些渲染性能问题
svg 组件
上面封装的 svg 组件,最大的问题在于需要维护每个图标的名字,开发者需要知道对应图标的名字才能使用,因此需要考虑更灵活和容易维护的 svg 图标组件封装方式。
参考
- element-plus-icons,下面的章节参考了
element-plus-icons
的源码 - 使用 Figma + GitHub Actions 完成 SVG 图标的完全自动化交付,让设计师参与图标的构建,通过 git hooks 自动发布,有点意思
- SVGIcon 组件的构建与使用
单个组件
封装单个组件并没有什么高科技
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path
:fill="color"
d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
/>
</svg>
</template>
<script lang="ts" setup>
type Props = {
color?: string;
};
const props = withDefaults(defineProps<Props>(), {
color: "currentcolor",
});
</script>
得到的图标组件,就可以像任意一个组件一样使用了
<template>
<el-button size="small" circle :icon="ArrowRight"></el-button>
</template>
<script setup lang="ts">
import { ArrowRight } from '@element-plus/icons-vue'
</script>
也考虑过借鉴vite-plugin-svg-icons
等插件的思路,可以聚合所有的 svg 文件,然后通过use
来引入对应的 symbol。
但这个方案存在的一些无法克服的问题:
- 无法实现 svg 代码的按需加载了,所有的 svg 都合并到了一个大的模版中,唯一得到的好处是组件文件里面没有大段的 svg path 代码(好像没有什么必要)
- 对于多色彩的 svg 图标,如果通过 use 来引入 svg,就无法定制组件的差异化 prop 了。
因此,我认为封装的 svg 组件中,包含原始的 svg 代码,是更合理的方法。
批量自动生成组件
一个项目中往往有很多个 svg 组件,手动编写单个组件显得有点重复,因此可以考虑写个脚本将某个文件夹(往往是icons
)下的所有 svg 文件都自动转成xx.vue
图标组件。
下面展示了相关的伪代码
async function transformSvg2Vue(file) {
const content = await readFile(file, "utf-8");
const { filename, componentName } = getName(file);
const vue = formatCode(
`
<template>
${content}
</template>
<script lang="ts">
type Props ={
color?: string
}
const props= withDefaults(defineProps<Props>(), {
color:'currentcolor',
})
`,
"vue"
);
writeFile(path.resolve(pathComponents, `${filename}.vue`), vue, "utf-8");
}
const files = readFiles("./icons");
for (const file of files) {
transformSvg2Vue(file);
}
思路非常简单
- 读取 svg 文件列表
- 对于单个文件,读取文件内容,拼接 vue 组件模版(react 的话就拼接 FC 组件格式代码)
- 如果某些组件需要定制 prop,这种利用模版生成组件的方式就需要额外处理一下,也许维护一个组件名和 prop 的映射是一个不错的做法
组件 playground
通过脚本生成了一批组件文件,如果能实时预览这些组件就更好了。因此可以搭建一个 playground,用于展示项目中生成的所有组件,通过 vite 可以快速实现这个目标。
首先获取所有的组件文件
// 脚本生成的所有文件都在这个目录下
import * as icons from "./components";
import type { App } from "vue";
export interface InstallOptions {
prefix?: string;
}
export default (app: App) => {
for (const [key, component] of Object.entries(icons)) {
app.component(prefix + key, component);
}
};
export { icons };
然后直接展示这些 icons 就行了
<template>
<component
:is="Icon"
v-for="(Icon, key) in icons"
:key="key"
class="icon"
/>
<hr />
<component
:is="`ElIcon${key}`"
v-for="key in Object.keys(icons)"
:key="key"
class="icon"
/>
</template>
<script lang="ts" setup>
import { icons } from "./icons";
</script>
<style>
.icon {
height: 48px;
color: #409eff;
}
</style>
小结
svg 图标组件已经成为前端项目中非常流行的方式了,优点包括
- 集成了 svg 图标的所有优点,
- 对开发者非常友好,无需再使用字符串保存每个组件的名字
- 每个组件都可以自定义 props
而对于封装组件的这一点小成本,也可以通过脚本等方式解决。在下一个项目中试试,就这么愉快的决定了
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。