关于前端:想要字体图标设计师却给了SVG没关系自己转

38次阅读

共计 7998 个字符,预计需要花费 20 分钟才能阅读完成。

本文为 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})

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

正文完
 0