想要字体图标设计师却给了SVG?没关系,自己转

简介: 本文为Varlet组件库源码主题阅读系列第三篇,读完本篇,你可以了解到如何将`svg`图标转换成字体图标文件,以及如何设计一个简洁的Vue图标组件。
本文为Varlet组件库源码主题阅读系列第三篇,读完本篇,你可以了解到如何将 svg图标转换成字体图标文件,以及如何设计一个简洁的Vue图标组件。

Varlet提供了一些常用的图标,图标都来自 Material Design Icon

转换SVG为字体图标

图标原文件是svg格式的,但最后是以字体图标的方式使用,所以需要进行一个转换操作。

处理图标的是一个单独的包,目录为/packages/varlet-icons/,提供了可执行文件:

打包命令为:

接下来详细看一下lib/index.js文件都做了哪些事情。

// lib/index.js
const commander = require('commander')

commander.command('build').description('Build varlet icons from svg').action(build)
commander.parse()

使用命令行交互工具commander提供了一个build命令,处理函数是build

// 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的配置如下:

// 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字体呢,就是类似下面这样的:

<?xml version="1.0" standalone="no"?> 
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
  <font id="geniconsfont" horiz-adv-x="24">
    <font-face font-family="geniconsfont"
      units-per-em="150" ascent="150"
      descent="0" />
    <missing-glyph horiz-adv-x="0" />
    <glyph glyph-name="播放"
      unicode="&#xE001;"
      horiz-adv-x="150" d=" M110.2968703125 71.0175129375C113.2343765625 72.853453875 113.2343765625 77.131555125 110.2968703125 78.967497L63.4218703125 111.779997C60.2997703125 113.7313095 56.25 111.486733875 56.25 107.8050045L56.25 42.1800046875C56.25 38.49827625 60.2997703125 36.253700625 63.4218703125 38.205013125L110.2968703125 71.0175129375z" />
    <glyph glyph-name="地址"
      unicode="&#xE002;"
      horiz-adv-x="150" d=" M0 150H150V0H0V150z M75 112.5C92.3295454375 112.5 106.25 98.9382045 106.25 83.119658125C106.25 58.529589375 83.238636375 42.758655625 75 37.5C66.7613636875 42.758655625 43.75 58.529589375 43.75 83.119658125C43.75 98.9382045 57.6704545625 112.5 75 112.5zM75 93.75C68.1250000625 93.75 62.5 88.1249999375 62.5 81.25C62.5 74.3750000625 68.1249999375 68.75 75 68.75C81.8749999375 68.75 87.5 74.3750000625 87.5 81.25C87.5 88.1249999375 81.8749999375 93.75 75 93.75z" />
  </font>
</defs>
</svg>

每个单独的svg文件都会转换成上面的一个glyph元素,所以上面这段svg定义了一个名为geniconsfont的字体,包含两个字符图形,我们可以通过glyph上定义的Unicode码来使用该字形,详细了解svg字体请阅读SVG_fonts

同一个Unicode在前端的htmlcssjs中使用的格式是有所不同的,在html/svg中,格式为&#dddd;&#xhhhh;&#代表后面是四位10进制数值,&#x代表后面是四位16进制数值;在css中,格式为\hhhh,以反斜杠开头;在js中,格式为\uhhhh,以\u开头。

转换成svg字体后再使用几个字体转换库分别转换成各种类型的字体文件即可。

到这里字体文件就生成好了,不过事情并没有结束。

// 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)])

清空上次的成果物,创建指定目录,继续:

// 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类名:

.var-icon-checkbox-marked-circle::before {
  content: "\F000";
}

var-icon是命名空间,防止冲突,通过伪元素显示UnicodeF000的字符。

这个约定是svgicons2svgfont规定的:

如果我们不自定义图标的Unicode,那么会默认从E001开始,在Unicode中,E000-F8FF的区间没有定义字符,用于给我们自行使用private-use-area

接下来就是生成css文件的内容了:

// 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文件的内容,最后写入文件即可:

// 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.cssless文件即可使用图标。

图标组件

字体图标可以在任何元素上面直接通过对应的类名使用,不过Varlet也提供了一个图标组件Icon,支持字体图标也支持传入图片:

<var-icon name="checkbox-marked-circle" />
<var-icon name="https://varlet-varletjs.vercel.app/cat.jpg" />

实现也很简单:

<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}`]

其实是个三元表达式,为什么不直接使用三元表达式呢,我也不知道,可能是更方便一点吧。

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,
  }
}

支持设置图标大小:

<var-icon name="checkbox-marked-circle" :size="26"/>

如果是图片则设置宽高,否则设置字号:

<template>
  <component
    :style="{
      width: isURL(name) ? toSizeUnit(size) : null,
      height: isURL(name) ? toSizeUnit(size) : null,
      fontSize: toSizeUnit(size),
    }"
  />
</template>

支持设置颜色,当然只支持字体图标:

<var-icon name="checkbox-marked-circle" color="#2979ff" />
<template>
  <component
    :style="{
      color,
    }"
  />
</template>

支持图标切换动画,当设置了 transition(ms) 并通过图标的 name 切换图标时,可以触发切换动画:

<script setup>
import { ref } from 'vue'

const name = ref('information')

const toggle = () => {
  name.value = name.value === 'information' 
    ? 'checkbox-marked-circle' 
    : 'information'
}
</script>

<template>
  <var-icon 
    :name="name" 
    :transition="300" 
    @click="toggle"
  />
</template>

具体的实现是监听name属性变化,然后添加一个改变元素属性的css类名,具体到这里是添加了一个设置缩小为0的类名--shrinking

.var-icon {
    &--shrinking {
        transform: scale(0);
    }
}

然后通过csstransition设置过渡属性,这样就会以动画的方式缩小为0,动画结束后再更新nextNamename属性的值,也就是变成新的图标,再把这个css类名去掉,则又会以动画的方式恢复为原来大小。

<template>
  <component
    :class="
      classes(
        [shrinking, n('--shrinking')]
      )
    "
    :style="{
      transition: `transform ${toNumber(transition)}ms`,
    }"
  />
</template>
const nextName: Ref<string | undefined> = ref('')
const shrinking: Ref<boolean> = ref(false)

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 })

图标组件的实现部分还是比较简单的,到这里图标部分的详解就结束了,我们下一篇再见~

相关文章
|
传感器 物联网 芯片
毕业设计 基于STM32单片机无线ZIGBEE智能大棚土壤湿度光照检测
毕业设计 基于STM32单片机无线ZIGBEE智能大棚土壤湿度光照检测
311 0
|
定位技术
阿里架构总监一次讲透中台架构,13页PPT精华详解,建议收藏!
本文整理了阿里几位技术专家,如架构总监 谢纯良,中间件技术专家 玄难等几位大牛,关于中台架构的几次分享内容,将业务中台形态、中台全局架构、业务中台化、中台架构图、中台建设方法论、中台组织架构、企业中台建设实施步骤等总共13页PPT精华的浓缩,供大家学习借鉴。
37078 115
|
SQL druid Java
线程池相关故障问题之Druid数据库连接池中,为何需要设置TransactionTimeout
线程池相关故障问题之Druid数据库连接池中,为何需要设置TransactionTimeout
479 0
|
8月前
|
传感器 人工智能 编解码
一文彻底拿捏PCB的发展历程
本文回顾了印制电路板(PCB)从20世纪初至今的发展历程,详述了各阶段的关键技术突破及其对电子产业的影响。从早期的手工焊接到现代的高密度互连(HDI)、柔性PCB和智能化集成,PCB技术不断演进,满足了电子设备小型化、高性能的需求,并推动了全球电子产业的快速发展。文章还探讨了环保制造工艺和可持续发展的趋势。
280 1
|
9月前
|
存储 安全 网络安全
勒索病毒最新变种.wxr勒索病毒来袭,如何恢复受感染的数据?
在数字化时代,网络安全特别是勒索病毒的威胁日益严峻。.wxr勒索病毒以其快速传播、广泛感染和多途径扩散的特点,对全球用户构成严重威胁。本简介深入解析.wxr勒索病毒的特性与影响,涵盖其传染机制、防范措施及应对策略,并强调了数据备份与安全意识的重要性。针对被加密的数据文件,提供技术服务支持(sjhf91),帮助用户恢复数据,抵御网络威胁。同时提醒用户关注“91数据恢复”以获取更多最新信息和支持。 由于字符限制,以上内容已被精简以适应要求。
589 23
|
消息中间件 人工智能 Java
活动回顾丨云原生开源开发者沙龙上海站回放 & PPT 下载
8 月 2 日,云原生开源开发者沙龙 AI 原生应用架构专场在上海举办,现场围绕 AI 应用开发和 Agent 编排、API 网关、可观测、智能编程、消息队列等视角分享了我们的开源成果和进展,以及落地实践。以下为分享回顾。
388 116
|
机器学习/深度学习 算法 数据处理
一文讲懂“预测滞后性”:详细解析
本文介绍了预测分析中常见的“预测滞后性”现象及其原因,包括数据收集延迟、模型训练耗时、预测算法延迟及模型特性等。文章还提供了应对策略,如实时数据处理、选择合适模型、在线学习及多方法结合,并附有使用简单移动平均法进行时间序列预测的Python代码示例,帮助读者理解和优化预测过程。
|
JSON fastjson Java
niubility!即使JavaBean没有默认无参构造器,fastjson也可以反序列化。- - - - 阿里Fastjson反序列化源码分析
本文详细分析了 Fastjson 反序列化对象的源码(版本 fastjson-1.2.60),揭示了即使 JavaBean 沲有默认无参构造器,Fastjson 仍能正常反序列化的技术内幕。文章通过案例展示了 Fastjson 在不同构造器情况下的行为,并深入探讨了 `ParserConfig#getDeserializer` 方法的核心逻辑。此外,还介绍了 ASM 字节码技术的应用及其在反序列化过程中的角色。
286 10
|
12月前
|
SQL 存储 关系型数据库
mysql SQL必知语法
本文详细介绍了MySQLSQL的基本语法,包括SELECT、FROM、WHERE、GROUPBY、HAVING、ORDERBY等关键字的使用,以及数据库操作如创建、删除表,数据类型,插入、查询、过滤、排序、连接和汇总数据的方法。通过学习这些内容,读者将能更好地管理和操
247 0
|
Linux API
在Linux中,程序产生了库日志虽然删除了,但磁盘空间未更新是什么原因?
在Linux中,程序产生了库日志虽然删除了,但磁盘空间未更新是什么原因?