想要字体图标设计师却给了SVG?没关系,自己转
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
本文为Varlet组件库源码主题阅读系列第三篇,读完本篇,你可以了解到如何将
svg
图标转换成字体图标文件,以及如何设计一个简洁的Vue图标组件。
Varlet
提供了一些常用的图标,图标都来自 Material Design Icon。
转换SVG为字体图标
图标原文件是svg
格式的,但最后是以字体图标的方式使用,所以需要进行一个转换操作。
处理图标的是一个单独的包,目录为/packages/varlet-icons/
,提供了可执行文件:
打包命令为:
接下来详细看一下lib/index.js
文件都做了哪些事情。
```js // lib/index.js const commander = require('commander')
commander.command('build').description('Build varlet icons from svg').action(build) commander.parse() ```
使用命令行交互工具commander提供了一个build
命令,处理函数是build
:
```js // lib/index.js const webfont = require('webfont').default const { resolve } = require('path') const CWD = process.cwd() const SVG_DIR = resolve(CWD, 'svg')// svg图标目录 const config = require(resolve(CWD, 'varlet-icons.config.js'))// 配置文件
async function build() { // 从配置文件里取出相关配置 const { base64, publicPath, namespace, fontName, fileName, fontWeight = 'normal', fontStyle = 'normal' } = config
const { ttf, woff, woff2 } = await webfont({
files: `${SVG_DIR}/*.svg`,// 要转换的svg图标
fontName,// 字体名称,也就是css的font-family
formats: ['ttf', 'woff', 'woff2'],// 要生成的字体图标类型
fontHeight: 512,// 输出的字体高度(默认为最高输入图标的高度)
descent: 64,// 修复字体的baseline
})
} ```
varlet-icons
的配置如下:
js
// varlet-icons.config.js
module.exports = {
namespace: 'var-icon',// css类名的命名空间
fileName: 'varlet-icons',// 生成的文件名
fontName: 'varlet-icons',// 字体名
base64: true,
}
核心就是使用webfont包将多个svg
文件转换成字体文件,webfont
的工作原理可以通过其文档上的依赖描述大致看出:
使用svgicons2svgfont包将多个svg
文件转换成一个svg
字体文件,何为svg
字体呢,就是类似下面这样的:
```svg
```
每个单独的svg
文件都会转换成上面的一个glyph
元素,所以上面这段svg
定义了一个名为geniconsfont
的字体,包含两个字符图形,我们可以通过glyph
上定义的Unicode
码来使用该字形,详细了解svg
字体请阅读SVG_fonts。
同一个Unicode
在前端的html
、css
、js
中使用的格式是有所不同的,在html/svg
中,格式为&#dddd;
或&#xhhhh;
,&#
代表后面是四位10
进制数值,&#x
代表后面是四位16
进制数值;在css
中,格式为\hhhh
,以反斜杠开头;在js
中,格式为\uhhhh
,以\u
开头。
转换成svg
字体后再使用几个字体转换库分别转换成各种类型的字体文件即可。
到这里字体文件就生成好了,不过事情并没有结束。
```js // lib/index.js const { writeFile, ensureDir, removeSync, readdirSync } = require('fs-extra')
const DIST_DIR = resolve(CWD, 'dist')// 打包的输出目录 const FONTS_DIR = resolve(DIST_DIR, 'fonts')// 输出的字体文件目录 const CSS_DIR = resolve(DIST_DIR, 'css')// 输出的css文件目录
// 先删除输出目录 removeSync(DIST_DIR) // 创建输出目录 await Promise.all([ensureDir(FONTS_DIR), ensureDir(CSS_DIR)]) ```
清空上次的成果物,创建指定目录,继续:
```js // lib/index.js const icons = readdirSync(SVG_DIR).map((svgName) => { const i = svgName.indexOf('-') const extIndex = svgName.lastIndexOf('.')
return {
name: svgName.slice(i + 1, extIndex),// 图标的名称
pointCode: svgName.slice(1, i),// 图标的代码
}
})
const iconNames = icons.map((iconName) => "${iconName.name}"
)
```
读取svg
文件目录,遍历所有svg
文件,从文件名中取出图标名称和图标代码。svg
文件的名称是有固定格式的:
uFxxx
是图标的Unicode
代码,后面的是图标名称,名称也就是我们最终使用时候的css
类名,而这个Unicode
实际上映射的就是字体中的某个图形,字体其实就是一个“编码-字形(glyph)”映射表,比如最终生成的css
里的这个css
类名:
css
.var-icon-checkbox-marked-circle::before {
content: "\F000";
}
var-icon
是命名空间,防止冲突,通过伪元素显示Unicode
为F000
的字符。
这个约定是svgicons2svgfont规定的:
如果我们不自定义图标的Unicode
,那么会默认从E001
开始,在Unicode
中,E000-F8FF
的区间没有定义字符,用于给我们自行使用private-use-area:
接下来就是生成css
文件的内容了:
```js // lib/index.js
// commonjs格式:导出所有图标的css类名
const indexTemplate = \
module.exports = [
${iconNames.join(',\n')}
]
// esm格式:导出所有图标的css类名
const indexESMTemplate = \
export default [
${iconNames.join(',\n')}
]
// css文件的内容
const cssTemplate = \
@font-face {
font-family: "${fontName}";
src: url("${
base64
?
data:application/font-woff2;charset=utf-8;base64,${Buffer.from(woff2).toString('base64')}:
${publicPath}${fileName}-webfont.woff2}") format("woff2"),
url("${
base64
?
data:application/font-woff;charset=utf-8;base64,${woff.toString('base64')}:
${publicPath}${fileName}-webfont.woff}") format("woff"),
url("${
base64
?
data:font/truetype;charset=utf-8;base64,${ttf.toString('base64')}:
${publicPath}${fileName}-webfont.ttf`
}") format("truetype");
font-weight: ${fontWeight};
font-style: ${fontStyle};
}
.${namespace}--set, .${namespace}--set::before { position: relative; display: inline-block; font: normal normal normal 14px/1 "${fontName}"; font-size: inherit; text-rendering: auto; -webkit-font-smoothing: antialiased; }
${icons
.map((icon) => {
return .${namespace}-${icon.name}::before {
content: "\\${icon.pointCode}";
}
})
.join('\n\n')}
`
```
很简单,拼接生成导出js
文件及css
文件的内容,最后写入文件即可:
js
// lib/index.js
await Promise.all([
writeFile(resolve(FONTS_DIR, `${fileName}-webfont.ttf`), ttf),
writeFile(resolve(FONTS_DIR, `${fileName}-webfont.woff`), woff),
writeFile(resolve(FONTS_DIR, `${fileName}-webfont.woff2`), woff2),
writeFile(resolve(CSS_DIR, `${fileName}.css`), cssTemplate),
writeFile(resolve(CSS_DIR, `${fileName}.less`), cssTemplate),
writeFile(resolve(DIST_DIR, 'index.js'), indexTemplate),
writeFile(resolve(DIST_DIR, 'index.esm.js'), indexESMTemplate),
])
我们只要引入varlet-icons.css
或less
文件即可使用图标。
图标组件
字体图标可以在任何元素上面直接通过对应的类名使用,不过Varlet
也提供了一个图标组件Icon,支持字体图标也支持传入图片:
html
<var-icon name="checkbox-marked-circle" />
<var-icon name="http://varlet-varletjs.vercel.app/cat.jpg" />
实现也很简单:
html
<template>
<component
:is="isURL(name) ? 'img' : 'i'"
:class="
classes(
n(),
[isURL(name), n('image'), `${namespace}-${nextName}`],
)
"
:src="isURL(name) ? nextName : null"
/>
</template>
通过component
动态组件,根据传入的name
属性判断是渲染img
标签还是i
标签,图片的话nextName
就是图片url
,否则nextName
就是图标类名。
n
方法用来拼接BEM风格的css
类名,classes
方法主要是用来支持三元表达式,所以上面的:
[isURL(name), n('image'), `${namespace}-${nextName}`]
其实是个三元表达式,为什么不直接使用三元表达式呢,我也不知道,可能是更方便一点吧。
```ts const { n, classes } = createNamespace('icon')
export function createNamespace(name: string) {
const namespace = var-${name}
// 返回BEM风格的类名 const createBEM = (suffix?: string): string => { if (!suffix) return namespace
return suffix.startsWith('--') ? `${namespace}${suffix}` : `${namespace}__${suffix}`
}
// 处理css类数组 const classes = (...classes: Classes): any[] => { return classes.map((className) => { if (isArray(className)) { const [condition, truthy, falsy = null] = className return condition ? truthy : falsy }
return className
})
}
return { n: createBEM, classes, } } ```
支持设置图标大小:
html
<var-icon name="checkbox-marked-circle" :size="26"/>
如果是图片则设置宽高,否则设置字号:
html
<template>
<component
:style="{
width: isURL(name) ? toSizeUnit(size) : null,
height: isURL(name) ? toSizeUnit(size) : null,
fontSize: toSizeUnit(size),
}"
/>
</template>
支持设置颜色,当然只支持字体图标:
html
<var-icon name="checkbox-marked-circle" color="#2979ff" />
html
<template>
<component
:style="{
color,
}"
/>
</template>
支持图标切换动画,当设置了 transition(ms)
并通过图标的 name
切换图标时,可以触发切换动画:
```html
```
具体的实现是监听name
属性变化,然后添加一个改变元素属性的css
类名,具体到这里是添加了一个设置缩小为0
的类名--shrinking
:
less
.var-icon {
&--shrinking {
transform: scale(0);
}
}
然后通过css
的transition
设置过渡属性,这样就会以动画的方式缩小为0
,动画结束后再更新nextName
为name
属性的值,也就是变成新的图标,再把这个css
类名去掉,则又会以动画的方式恢复为原来大小。
html
<template>
<component
:class="
classes(
[shrinking, n('--shrinking')]
)
"
:style="{
transition: `transform ${toNumber(transition)}ms`,
}"
/>
</template>
```ts
const nextName: Ref
const handleNameChange = async (newName: string | undefined, oldName: string | undefined) => { const { transition } = props
// 初始情况或没有传过渡时间则不没有动画
if (oldName == null || toNumber(transition) === 0) {
nextName.value = newName
return
}
// 添加缩小为0的css类名
shrinking.value = true
await nextTick()
// 缩小动画结束后去掉类名及更新icon
setTimeout(() => {
oldName != null && (nextName.value = newName)
// 恢复为原本大小
shrinking.value = false
}, toNumber(transition))
}
watch(() => props.name, handleNameChange, { immediate: true }) ```
图标组件的实现部分还是比较简单的,到这里图标部分的详解就结束了,我们下一篇再见~
- 面试官问我按钮级别权限怎么控制,我说v-if,面试官说再见
- Vue组件库文档站点的搭建思路
- 想要字体图标设计师却给了SVG?没关系,自己转
- 这些年我开源的几个小项目
- 开源的网易云音乐API项目都是怎么实现的?
- 关联线探究,如何连接流程图的两个节点
- 我做了一个在线白板!
- 基于Vue2.x的前端架构,我们是这么做的
- 揭开Vue异步组件的神秘面纱
- 手把手教你实现在Monaco Editor中使用VSCode主题
- Vue0.11版本源码阅读系列五:批量更新是怎么做的
- Vue0.11版本源码阅读系列七:补充
- Vue0.11版本源码阅读系列五:批量更新是怎么做的
- 冬天到了,给你的网站下个雪吧
- 带你实现一个简单的多边形编辑器
- 登录重构小记
- 冬天到了,给你的网站下个雪吧
- 高仿一个echarts饼图