Web 中的字体和 SVG 图标,你了解多少?

4,070 阅读13分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

专栏上篇文章传送门:实战案例:初探工程配置 & 图标组件热身

专栏下篇文章传送门:衍生需求:按钮集成图标组件 & 图标选择器

本节涉及的内容源码可在vue-pro-components c3 分支找到,欢迎 star 支持!

前言

本文是 基于Vite+AntDesignVue打造业务组件库 专栏第 4 篇文章【Web 中的字体和 SVG 图标,你了解多少?】,我们接着上篇的字体图标组件实战,继续探索图标的另一个展现形式 —— SVG 。在开发 SVG 图标组件之前,还有一些问题我们必须搞清楚!

值得了解的字体知识

不能盲目地开发组件,我们可以先问问自己:既然有了字体图标,为什么还需要 SVG 图标呢?

在回答这个问题之前,我们还需要了解字体的一些基础知识,免得在具体应用时一知半解。

我们首先来看一下计算机字体有哪几种大的分类:

引自维基百科 Computer_font

There are three basic kinds of computer font file data formats:

  • Bitmap fonts consist of a matrix of dots or pixels representing the image of each glyph in each face and size.
  • Vector fonts (including, and sometimes used as a synonym for, outline fonts) use Bézier curves, drawing instructions and mathematical formulae to describe each glyph, which make the character outlines scalable to any size.
  • Stroke fonts use a series of specified lines and additional information to define the size and shape of the line in a specific typeface, which together determine the appearance of the glyph.
  • 位图字体(同义词:点阵字体)本质上是点或像素组成的矩阵(也就是点阵)。像素化的内容在较高分辨率的设备上会有比较糟糕的表现(失真、模糊等),因为把低像素的内容放在高分辨率的设备上显示时,计算机会不知道如何展示(具体表现为不知道内容像素和设备像素的对应关系),这就需要根据 Nearest-neighbor interpolation 算法进行最近邻插值,最终计算出来的位置不一定理想,同时也可能出现锯齿,这种展示是有损的,需要结合抗锯齿算法来做优化。

image.png

  • 矢量字体(同义词:轮廓字体)是像素无关的,它使用贝塞尔曲线、绘图指令、数学公式等描述字形,它不像位图字体预先处理好像素,而是实时计算渲染,相对来说速度更慢。但是这样的字体理论上就可以适应各种大小的分辨率,但是最终效果也取决于渲染引擎的具体实现,可能也会出现锯齿,因为矢量在理论上是完美的,但是对应到显示设备上还是要落到具体像素上的,最终还是要结合抗锯齿、字体微调亚像素渲染DirectWrite 等技术手段优化。矢量字体主要包括 PostScript Type 1 and Type 3 fontsTrueTypeOpenType 等几类。

Type 1, Type 3 都是 Adobe 搞的;TrueType 是苹果搞出来对抗 Adobe 的,后面又许可微软加入一起用;最后微软又联合 Adobe 搞了 OpenType(可以理解为 Type 1 和 TrueType 的超集),纯纯的都是商业竞争啊!

.ttf 扩展名表示常规 TrueType 字体或具有 TrueType 轮廓的 OpenType 字体。

.woff 是 OpenType 字体或者 TrueType 字体,由于是 web 专用字体,采用了 zlib 压缩。woff 由 Mozilla 基金会、Opera Software和微软于 2010年4月向万维网联盟(W3C)提交, 在2012年12月13日成为了 W3C 推荐标准,woff2 由谷歌负责推进,改进了压缩方案,于2018年3月成为了 W3C 推荐标准,是未来 web 字体的发展方向。

  • 笔画字体是矢量字体的一种细分形式,使用一系列指定的线条和附加信息来定义特定字体中线条的大小和形状,它们共同决定了字形的外观。从直觉上不容易看出它和轮廓字体的区别,不过从字面意思看,轮廓字体有点类似于艺术字那种比较饱满的效果,会有描边和填充的操作,需要更多的顶点数量来支撑起整个字形;而笔画注重骨架和描边,需要较少的顶点,更省空间,在表意文字上用得比较多。

下图引用了发明专利《一种笔划矢量字库的存取方法》的附图,侵删!

左边是笔画字体,右边是轮廓字体。

image.png

位图字体的优点在于它相对于其他字体在制作、渲染等方面更简单,速度也更快;其最大的缺陷是不能自适应各种分辨率的显示设备,同时在展示是否粗体、是否斜体、不同字号时都需要一套单独的字形光栅图像,这是乘法级的存储冗余,比较浪费存储空间(虽然也可以通过算法调整变换出字体变体,但是比较耗费性能)。虽然有这些缺陷在,但是在早期计算机性能较弱的情况下,位图字体显得非常合时宜,有着不可替代的地位。

image.png

随着计算机性能(算力、渲染等)的提升,位图字体的地位开始受到矢量字体的挑战,在需要任意弹性伸缩展示字形的场景下,矢量字体成了绝佳的选择;而位图字体则活跃在一些需要考虑速度、简单程度、硬件性能等因素的场景下,例如嵌入式设备、操作系统终端控制台、点阵打印机等。

Web 字体是矢量字体吗?

了解了这些字体知识后,问题来了,Web 网页中我们常用的的字体都是什么类型的字体?

直觉告诉我们,好像是矢量字体,因为伸缩网页时,字还是很清晰,没有出现锯齿。

web字体伸缩都很清晰.gif

事实确实如此,点阵字体是计算机早期采用的字体,80年代开始,矢量字体就慢慢流行了起来,我们现在在网页中看到的基本上都是矢量字体。

字体有很多种,但是能直接用在 Web 中的不多,因为不能保证用户的电脑中安装了指定的字体。所以指定font-family时通常考虑使用 Web 安全字体,保证各个操作系统、新旧版本、英文中文、emoji 等都能兼顾。

font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

字体图标 VS SVG 图标

既然 Web 字体基本上都是矢量字体,字体图标基本上用的也是.ttf, .woff, .woff2等矢量字体,看起来没什么缺点,那么为什么 SVG 图标慢慢成为了各大网站的首选呢?

首先,字体是一种资源,如果字体加载失败,字体图标也就会渲染失败,可能会出现“小方格”。并且,如果由于网络等原因导致字体加载较慢,可能会出现占位抖动的情况(采用Base64内联字体会解决这个问题)。

image.png

而 SVG 是 HTML 文档内联的内容,渲染上会更可靠一点。

其次,字体图标并不支持彩色图标,即便现在彩色字体在 Web 中慢慢有了一些声音(iconfont 平台也支持了彩色字体图标),但是兼容性还有一些欠缺,在生产使用时还需要慎重考虑。而 SVG 本身是通过 XML 描述绘图指令,在绘图细节上控制力更强,天然支持彩色!不过随着未来 Web 彩色字体的普及,这方面也将不再是字体图标的劣势。

SVG 图标可以支持更丰富的表现能力,比如滤镜、动画等,同时也支持 DOM 操作,这些是字体图标比不了的。

字体图标的可访问性并不友好。字体图标是通过伪元素实现,比 SVG 的可访问性要差一些,SVG 内部可以使用titledesc标签描述信息,这无论对 SEO 还是无障碍阅读来说都显得更合适。

在图标数量很多时,SVG 图标的优势更为明显,SVG 支持按需使用。而字体图标的图标和图标之间耦合在一个字体文件中,这会导致最终的字体文件很大。

但也不是说字体图标就一无是处,字体图标在浏览器兼容性方面要更胜一筹,不过在“IE 已死”的局面下似乎也无关痛痒(老系统例外)。

总的来说,字体图标和 SVG 图标都有各自的优势,并不是说哪个一定比另一个优秀,我们可以结合实际场景来考虑选用。

SVG 图标的使用方式

  • 直接使用 SVG:内联 SVG,图片,背景图等形式,缺点是使用起来不是很方便,通用性不够。
  • SVG Sprite:基于 symbol + use 实现,是 SVG 版本的雪碧图,可以封装成组件使用。
  • SVG 独立组件:把每个 SVG 图标做成按需加载的组件,这样就可以按需使用,同时兼具组件化的优点,类似于@ant-design/icons-vue提供的图标单组件。

SVG 图标组件实现思路

我们先尝试一下 SVG Sprite 的方式。

SVG Sprite 是先有 symbol,再通过 use 去引用。

The  <symbol>  element is used to define graphical template objects which can be instantiated by a <use> element.

symbol 标签是用来定义图形模板对象的,但是不会直接渲染,需要通过 use 标签去实例化。

image.png

上图就是 SVG symbol 的大致结构,可以发现最外层的 svg 下面是一个 defs 标签。

The  <defs>  element is used to store graphical objects that will be used at a later time. Objects created inside a <defs> element are not rendered directly。

在SVG 中,可复用的内容定义在 defs 下面,并不局限于 symbol。

image.png

symbol 也不强制放在 defs 下面,直接放在 svg 标签下也可以,因为它本身就是模板的含义,不会直接渲染。

我们还可以发现,每个 symbol 上都有一个属性 id,而这个 id 将成为 use 标签引用的依据,xlink:href通过#加 id 的方式就可以引用对应的 symbol。

image.png

了解这些知识后,我们应该清楚,要实现 SVG Sprite 组件,第一步是要生产出 symbol 内容。

UI 交付给前端的一般是一个个独立的 SVG 文件,那么怎么把这些独立的 SVG 文件变成我们想要的 symbol 呢?

这涉及到文件操作,如果用 nodejs 实现,必然离不开 fs 模块相关的 api,基本的原理就是读文件的字符串内容,然后做拼接处理,输出一个字符串,这个字符串最终可以通过 innerHTML 方式插入到 HTML 文档流中。

如果你的图标类的 SVG 文件都是放在项目工程中的,那么可以选用 svg-sprite-loader(webpack 体系)直接把这部分工作做好,剩下的事情就是封装组件。

Vite 中也可以实现类似的插件,大家按 vite svg sprite 等关键词去搜一搜就能找到很多。

如果我们使用的是 iconfont,也可以直接用 iconfont 提供的脚本。通过script标签引入这个 js 文件后,会自动创建相关的 SVG symbol。

image.png

剩下的工作就是把 use 的使用封装到组件中,让业务调用变得简单。

编码实现 SVG 图标组件

基本逻辑捋清楚了,我们来编码实现一下。

为了和字体图标组件区分开,我们把上节中实现的字体图标组件Icon重新命名为IconFont,而本次要实现的 SVG 图标组件就叫做IconSvg

首先在业务项目中引入 iconfont 图标项目中的这个在线 js 文件。

image.png

按照前一篇文章所述,我们可以暂时把playground包看作是业务项目,所以直接在playground包中的index.html中引入这个 js 就可以了。

image.png

引入 js 后,我们可以看到 SVG symbol 已经生成好了。

image.png

注意:如果您担忧 cdn 的稳定性,可以考虑把 iconfont 项目中的相关资源下载到项目中直接引用,这样就不用担心哪天线上业务由于 cdn 问题受到影响。

image.png

接着就是在组件中把 use 的逻辑处理好。

<template>
    <svg :style="{ width: `${size}px`, height: `${size}px`, fill: color }">
        <use :xlink:href="`#${iconPrefix}${icon}`"></use>
    </svg>
</template>

<script lang="ts" setup>
import { props } from './props'

defineProps(props)
</script>

属性定义基本上与IconFont组件一致,但是具体用法有一点区别。

image.png

组件的尺寸还是交给属性size控制,但是对应到 style 上是由widthheight控制(因为font-size对 svg 无效)。

组件的颜色是通过 style 的fill控制的(因为color也对 svg 无效)。

我们看看目前的使用效果。

image.png

image.png

基本效果出来了,但是我们发现一个问题,如果不绑定sizecolor属性,SVG 图标的表现不符合我们的预期。

image.png

默认的尺寸太大了,预期应该是和同一个块中的文字差不多大。

默认的颜色也不符合预期,我们希望它能跟随父级元素的字体颜色,这样才显得协调。

所以,还需要再做一些优化。我们把组件的外层包裹一个span标签,让svg的尺寸继承span的字体大小,让svg的颜色继承span的颜色。

  • 尺寸上的继承:可以设置svg的宽高都为1em,这样就可以与文本的字体大小保持一致。
  • 颜色上的继承:可以设置svgfill属性值为currentColor,currentColor 是一个 css 变量,它能取到当前元素的color属性值,这样就可以与文本颜色保持一致了。

代码改造如下:

image.png

其中样式部分如下:

image.png

这里用到了一个text-rendering: optimizelegibility;,对抗锯齿、字体微调等会更友好,具体见这篇MDN介绍

后面两个属性-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;也是对抗锯齿的优化。

line-height设置为0,可以消除行高对整个 span 尺寸的影响,使得 svg 的尺寸能准确表现出来。

再次查看效果,发现不传任何属性时,SVG 图标也能与文本效果协调。

image.png

image.png

至此,SVG 图标组件已经能应付大部分场景了。

参考文献

结语

在本节中,我们首先了解了一些字体相关的基础知识,以及使用 SVG 图标的一些优点,接着学习了实现 SVG 图标组件的基本思路和编码过程。本文写作过程中,我有感觉到字体是个很复杂的知识领域,若文中相关知识点叙述有误,还请指出!在实际项目中,对图标组件还会有一些拓展的需求,我们在下篇文章中会具体展开聊聊。如果您对我的专栏感兴趣,欢迎您订阅关注本专栏,接下来可以一同探讨和交流组件库开发过程中遇到的问题。