你需要知道的字体编码知识

1,582 阅读7分钟

最近在做一些页面优化的事情。在使用到了自定义字体和iconfont之后,还是决定把字体、iconfont的原理给搞清楚。

在搞清楚iconfont之前,先了解一下计算机字符编码相关的知识:Unicode和Utf-8。

ASCII

在了解Unicode之前,先简单介绍一下ASCII码。ASCII是世界上最通用的 单字节 编码系统,这里单字节划重点。ASCII码一共定义了 128 个字符,所以只需要7 bits 我们就可以完全表示出全部的ASCII码,1个字节包含了8个bit,所以最前面的1位是0。举个例子就是,字符 'A' 用ASCII码表示为01000001,即对应数字65。

但是ASCII码只有 单字节,最多最多只能容纳255个字符,无法表达出世界上那么多种语言的字符,所以Unicode来了。

Unicode

什么是unicode?Unicode是一种字符标准,它对世界上的大部分字符进行了整理和编码,现在已经收录了超过 13万个字符。从ASCII码可以看出,字符编码实际上就是一个数字(一个表示)会对应到一个字符,计算机会根据这种一一对应关系去识别当前这个表示的是哪一个字符。Unicode官网上有很多例子:

但是Unicode本身是一个标准,不是一个具体的实现,它只是给出了一个字符具体的编码值,而不会去要求计算机本身如何存储这个值。这就引出了我们下面要介绍的具体实现了。

UTF-32

UTF-32是Unicode的一种实现方式。UTF-32使用四个字节表示一个字符,也就是说,无论这个字符对应的编码值是多大,都会用32个bit来表示。这样的优点是简单,易于解析和查找,每4个字节一组,转换成对应的字符即可。但是缺点也很明显,如何文本内容全部都是ASCII码中的字符,本来1个字节就可以表示的字符现在要4个字节,文件的大小会大4倍,所以在HTML规范中,不会使用UTF-32来进行编码。

UTF-8

UTF-8是最常见的格式了,在HTTP的请求头中就可以明确定义charset为utf-8,例:

Content-Type: text/html; charset=utf-8

UTF-8区别于UTF-32最大的特点就是UTF-8是变长的编码实现,用1~4个字节来表示一个字符,这样就兼容了ASCII码中的字符(都只需要1个字节即可)

UTF-8的编码规则如下:

单字节的字符,首位为0,其他7位是字符的编码值,和ASCII码对应的值是一样的

对于多字节的字符,假设有n个字节,第一个字节的前n位都是1,第n+1位是0,后面的每一个字节的前两位都是10。

举个例子更好的理解:

a的ASCII码是97,对应的二进制是 1100001,一个字节就可以表示,utf-8的表示和ASCII码的表示结果是一样的都是 01100001。

汉字”哈“对应的编码值是21704的二进制是 101010011001000,一共有15位,看上去是两个字节就可以表示,但是不要忘记utf-8表示法里面是有特殊的标志位的,2个字节的utf-8表示真正代表字符值的只有11位,所以想要表示”哈“,我们需要三个字节。最后表示出来就是(蓝色的部分是占位符,绿色是真正代表字符的位):

用Unicode做一些有趣的事

我们各种字符包括Emoji都是在Unicode的表示范围中的,有的Unicode之间可以产生”联动”效果。

比如:

汉字“哈” 对应unicode的十六进制是54c8,在JS里用\u前缀拼接上十六进制就代表一个字符的unicode,所以JS里 “哈” 表示为:

'\u54c8'

(Tips: 如何获取一个字符对应的unicode值?JS的string类型提供了codePointAt() 方法,可以获取到十进制的值)

上面也说了Unicode支持拼接,我们在'\u54c8' 后面拼一个 二声符号 的unicode值:

'\u54c8' + '\u0301'

得到的结果是:“哈́” (蛤), 注意上面有个小符号(看不到的小伙伴 可以看下面这张图)

Emoji能玩的就更多了,在这里举一个栗子:

我们找一个男人的头像👨,对应的unicode 的值是

'\u{1f468}' // 超过4位的要用大括号括起来

Unicode有几个值是代表了肤色,分别是:

'\u{1f3ff}' 
'\u{1f3fe}' 
'\u{1f3fd}' 
'\u{1f3fb}' 
'\u{1f3fc}'

我们继续用上面的拼接方式,例:

'\u{1f468}' + '\u{1f3fd}'

就得到了一个棕色皮肤的男人头像:👨🏽

iconfont

编码的环节就结束了,接下来我们了解一下iconfont相关的内容。我们首先可以去阿里巴巴的iconfont网站中去看一下,里面有超级多的icon,但是为什么叫iconfont呢,因为这些icon实际上都是一个个文字。(这可能有些难理解,icon为啥会是文字?)

我们随便挑一个icon看一下他的具体内容:

在一个div标签中包裹的是 哈 54c8就是 汉字“哈”对应的值,我们打开HTML看一下,看到页面上展示的值是一个“哈”

那我们把上面icon对应的unicode值写到html中会发生什么呢?

发现是一个框,为什么展示的不是上面的icon呢,因为将unicode转换成对应样式是和字体有关的,这个icon对应的编码值在你的系统中可能并不能找到对应的样子,所以才有 webfont 的存在。在iconfont上,可以看到除了icon对应的值以外,还有一段css代码:


@font-face {
  font-family: 'iconfont';  /* project id 1963094 */
  src: url('//at.alicdn.com/t/font_1963094_kfa3gxlqywt.eot');
  src: url('//at.alicdn.com/t/font_1963094_kfa3gxlqywt.eot?#iefix') format('embedded-opentype'),
  url('//at.alicdn.com/t/font_1963094_kfa3gxlqywt.woff2') format('woff2'),
  url('//at.alicdn.com/t/font_1963094_kfa3gxlqywt.woff') format('woff'),
  url('//at.alicdn.com/t/font_1963094_kfa3gxlqywt.ttf') format('truetype'),
  url('//at.alicdn.com/t/font_1963094_kfa3gxlqywt.svg#iconfont') format('svg');
}

这个css就是引入的字体,让我们把这个字体样式引入,然后赋值给unicode:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    @font-face {
      font-family: 'iconfont';  /* project id 1963094 */
      src: url('https://at.alicdn.com/t/font_1963094_kfa3gxlqywt.eot');
      src: url('https://at.alicdn.com/t/font_1963094_kfa3gxlqywt.eot?#iefix') format('embedded-opentype'),
      url('https://at.alicdn.com/t/font_1963094_kfa3gxlqywt.woff2') format('woff2'),
      url('https://at.alicdn.com/t/font_1963094_kfa3gxlqywt.woff') format('woff'),
      url('https://at.alicdn.com/t/font_1963094_kfa3gxlqywt.ttf') format('truetype'),
      url('https://at.alicdn.com/t/font_1963094_kfa3gxlqywt.svg#iconfont') format('svg');
    }
    .font {
      font-family: 'iconfont';
    }
</style>
</head>
<body>
  <div class="font">
    &#xe646;
  </div>
</body>
</html>

发现icon正常展示出来了:

所以iconfont的原理也显而易见了,其实改变的就是展示的字体,该字体定义了一个编码值对应的样式,文字就是icon所以叫做iconfont。

引入font常做的优化

通常,设计师给我的字体文件都会很大,基本上在几MB到十几MB不等。对于前端来说,10+MB的大小是肯定不能被接受的,等到字体文件加载完成,说不定用户都讲页面关掉了,我们想要的字体根本没能展示出来。

所以对于关键的字体,比如页面首屏核心区域的必要字体样式,最好能够单独抽离出来,然后内联到HTML中随着HTML一起加载进来,这样就不存在额外加载字体的时延,整体的表现会很好。如何单独抽离字体呢?这里可以使用一个npm包:fontmin。用法非常简单,举个例子:

var Fontmin = require('fontmin');
var fontmin = new Fontmin()
    .src('./font_example.ttf')   // 输入字体文件
    .use(Fontmin.glyph({        // 字型提取插件
        text: '这些是我要抽离的字体' // 所需文字
    }))
    .dest('./font');
fontmin.run(function (err, files, stream) {
  if (err) {                  // 异常捕捉
      console.error(err);
  }
  console.log('done');        // 成功
});

执行完成后就会生成一个只包含“这些是我要抽离的字体”这几个文字对应的字体样式了。包体积会小很多,然后可以内联来,一来HTML体积不会增加太多,二来展示就不会出现延迟问题了。

那其他的字体怎么办呢?可以使用预加载的方式,提前加载其他font字体,缩短字体展示的延迟。举个例子:

<link rel="preload" href="./src/assets/font/font.ttf" as="font" type="font/ttf">

今天的分享就到这里了~。

参考资料

欢迎关注我的个人技术公众号,不定期分享各种前端技术~