前端字体截取技术,做炫酷网站再也不用怕中文字体数据量大啦!【实战】

众所周知,相对于英文字体,中文字体天生是“庞然大物”。英文字体两三百KB已经很大了,而中文字体几MB十几MB都算小的。一方面,中文字体包含的字形数量极多,动辄数以千计甚至万计,而英文字体则只需包含几十个基本字符和符号,哪怕支持多种语言及字符变体,容量达到三千多个字形已经算非常庞大的了。另一方面,中文字形的曲折变化复杂度高,在基于轮廓的矢量字体设计中,用于控制中文字形曲线的控制点普遍比英文更多,因而需要的数据量更大,也会导致字体文件膨胀。

image.png

前端开发实践中,为了实现一些特殊视觉效果,经常需要使用某些特殊字体,而用户电脑上几乎不太可能安装这些字体,这时候通常需要使用Web字体技术,让浏览器动态下载我们的自定义字体。可是中文字体非常庞大,很多时候“全量”加载某个字体文件是不现实的。特别是对于一些动态页面且每个页面只有少量字符用到该字体的情况下。当然,也不是每个页面都会用到一个字体文件中的所有字符,全量加载本身也极其浪费。

研究表明,3500常用中文汉字(中国义务教育9年级需要掌握的汉字数量)即可覆盖日常使用汉字的99.8%:

500 字(78.53202%)

1000字(91.91527%)

1500字(96.47563%)

2000字(98.38765%)

2500字(99.24388%)

3000字(99.63322%)

3500字(99.82015%)

可见,最常用的前500个汉字的覆盖率已经达到78%。因此,“全量”加载某个字体,特别是中文字体,在当前网络环境下不仅浪费流量和时间,而且也是完全没有必要的。这时候,我们可以根据网页用到的字符来截取字体的片段,这个技术英文叫subset,也就是“取子集”。

本文首先简单回顾Web自定义字体的技术规范,然后通过实例介绍两种前端常用的截取字体的技术。首先是CSS中的unicode-range`属性,我们称之为“软截取技术”,因为它只是在本地既有字体或者浏览器已经下载的字体基础上做一个指向子集的“软链接”,并不能真正减小浏览器下载文件的大小。其次是Node命令行工具glyphhanger,我们称之为“硬截取技术”,即在服务端从“全量”字体中分离出一个体积相对极小的字体子集,做成Web字体通过Web服务器或CDN下发给浏览器。

无论是“软截取”,还是“硬截取”,都会用到Web字体和 @font-face 规则。因此,我们需要先来了解一下这个基础的Web标准语法。

#Web字体与@font-face

为了超越“Web安全字体”的局限,在网页上使用一些用户电脑上不太可能会安装的字体,微软曾率先提出了 @font-face 规则。这个规则后来进入W3C的 CSS Fonts Module Level 3 模块,于是就有了前端常用的Web自定义字体技术:

@font-face {
  font-family: 'MyWebFont';
  src: url('webfont.eot'); /* 兼容IE9 */
  src: url('webfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
       url('webfont.woff2') format('woff2'), /* 最新浏览器 */
       url('webfont.woff') format('woff'), /* 较新浏览器 */
       url('webfont.ttf')  format('truetype'), /* Safari、Android、iOS */
       url('webfont.svg#svgFontName') format('svg'); /* 早期iOS */

当然,上面的代码是几乎可以兼容所有浏览器的方案。大约在两年前,也就是2016年,由于浏览器版本的快速更迭,写成下面这样已经是比较现实的了:

@font-face {
  font-family: 'MyWebFont';
  src:  url('myfont.woff2') format('woff2'),
        url('myfont.woff') format('woff');
}

如果要兼容更多浏览器,那再加上一种几乎所有浏览器都支持的 ttf 格式则似乎更稳妥:

@font-face {
  font-family: 'MyWebFont';
  src: url('myfont.woff2') format('woff2'),
       url('myfont.woff') format('woff'),
       url('myfont.ttf') format('truetype');
}

不过,我们的最终目标还是写成这样,即只使用 woff2 这种自带压缩的格式:

@font-face {
  font-family: 'MyWebFont';
  src: url('myfont.woff2') format('woff2');
}

从技术角度讲,除了直接使用 @font-face ,还可以使用 @import 规则或 link 元素导入或加载包含 @font-face 声明的外部文件:

// 导入
@import url(//fonts.googleapis.com/css?family=Open+Sans);
// 或者引用
<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
// 实际使用
body {
  font-family: 'Open Sans', sans-serif;

打开Google Fonts看一看:https://fonts.googleapis.com/css?family=Open+Sans

以上都是技术规范,至于什么时候可以过渡到只使用专门针对Web字体优化的压缩格式 woff2,应该只是一个时间问题。

回顾完了基础的技术规范和语法,明确了未来的方向,接下来我们进入实战。先看一下 CSS Fonts Module Level 3 定义的与 @font-face 规则配合使用的 unicode-range 属性,然后再给大家介绍一家有名的国外Web开发公司 Filament Group, Inc. 推出的字体截取工具 glyphhanger

unicode-range

unicode-range 属性虽然可以算作“字体截取”技术,但它是“软截取”,不是“硬截取”。它类似于一种快捷方式,而不能真正减少浏览器需要下载的字体文件大小。

顾名思义, unicode-range 用于指定自定义字体中包含的字符的Unicode码点范围,语法如下:

// CSS
@font-face {
  font-family: 'Ampersand';
  src: local('Times New Roman');
  unicode-range: U+26;
}
div {
  font-size: 4em;
  font-family: Ampersand, Helvetica, sans-serif;    
}
// HTML
<div>Me & You = Us</div>

示例代码出处:https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range

以上 @font-face 规则自定义了一个名为“Ampersand”(英文&符号)的字体,这个字体“截取”自本地字体Times New Roman,而这个字体只包含一个字符: U+26 ( 26 是英文&符号的十六进制Unicode码点,对应的十进制值是38)。

HTML中 div 元素根据 font-family 的指令,依次会应用自定义字体Ampersand(Times New Roman,衬线字体)、Helvetica(无衬线字体)和 sans-serif (无衬线)字体族。实际应用效果如下:

image.png

Unicode编码扩容到了17个编码平面,每个平面的容量为65,536,总容量为1,114,112个码点,其中实际分配使用的只有128,237个,约占12%。因此在可以预见的未来,Unicode有足够的空间包含地球上所有文明的字符。

看一个中文字体的例子。假设我们要用特殊字体突出显示“初唐四杰”之一王勃的千古名篇《滕王阁序》中最有名的那句:“落霞与孤鹜齐飞,秋水共长天一色。”

image.png

可以先把这句名句(包括标点)转换成Unicode码点:

字符串转码点可以使用以下JavaScript函数:

function text2point(t) {
   return t.split('').map(c => 'u+'+c.charCodeAt().toString(16)).join(',')
}

然后以“隶变”(Libian SC)作为源字体,自定义一个名叫 custom 的字体,把它应用到 .emphasis 元素:

// CSS
@font-face {
  font-family: custom;
  src: local(Libian SC);
  unicode-range: u+843d,u+971e,u+4e0e,u+5b64,u+9e5c,u+9f50,u+98de,u+ff0c,
                  u+79cb,u+6c34,u+5171,u+957f,u+5929,u+4e00,u+8272,u+3002;
  font-weight: 500;
}
.emphasis {
  font-family: custom;
}
// HTML
<!--其他句子-->
<span class="emphasis">落霞与孤鹜齐飞,秋水共长天一色。</span>
<!--其他句子-->

注意,上面代码中的码点列表为排版阅读方便而人为换了行,实际使用中不要人为换行,以免造成语法错误。下面的代码示例也一样。

结果如下:

https://img2.tuicool.com/ANBr6vN.jpg!web

此时,我们发现标点(逗号和句号)的样式与其他文字不统一,而其他文字使用的是“苹方”(PingFang SC)字体(在Mac上)。是不是可以简单地从前面的码点列表中删除逗号和铝的码点 u+ff0c 和 u+3002 ?这个方案在Safari 12、Firefox 62中可行,删除码点之后的逗号和句号会继承使用“苹方”字体,但是在Chrome 69中并不奏效。

此外,Chrome似乎还有一个bug。假设不删除上述码点,而直接在标点左侧输入一个自定义字体中并不包含的字符,Chrome会强制把这个字符显示成自定义字体。看来浏览器的实现还是有不一致的地方。时间关系,Windows平台下的IE和Edge没有测试,读者可以自行测试一下。

无论如何,我们可以再定义一个只包含逗号和句号两个字符的自定义字体来解决这个问题:

@font-face {
  font-family: punc;
  src: local(PingFang SC);
  unicode-range: u+ff0c,u+3002;
}
.emphasis {
  font-family:punc, custom;
}

这样,即使不删除 custom 声明中的码点,Chrome、Safari和Firefox也都可以将逗号和句号显示为“苹方”字体了:

image.png

注意,不要试图基于英文字体自定义 punc 字体,因为英文字体中不包含对中文标点符号对应码点的映射。

虽然这个例子明显是自造的,但“对中文内容中的某部分中文字符做特殊字体处理,或者是英文字体中部分字符做特殊字体处理”才是 unicode-range 这种“软截取技术”最适合的应用场景。更多 unicode-range 的内容,推荐大家看一看张鑫旭老师的文章 “CSS unicode-range特定字符使用font-face自定义字体”:(https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/)

使用unicode-range的注意事项:

unicode-range可以接收

单个码点: U+26 (或 u+26 )

码点范围: U+0-7F , U+0025-00FF

通配符范围: U+4?? ,相当于 U+400-U+4FF

逗号分隔的多个值: U+0025-00FF, U+4??

unicode-range 默认值为: U+0-10FFFF ,即全部Unicode字符编码

unicode-range 的值是码点的字面值或字面值列表, 不是字符串

unicode-range: u+ff0c,u+3002;

unicode-range 的值不能有语法错误,比如上面说的 不是字符串 ,以及不能出现多余的逗号: u+ff0c,u+3002,; (末尾多了一个逗号)等,出现语法错误的后果是自定义字体会变成源字体的别名,而非基于源字体截取的子集。(当然,通过 @font-face 定义已有字体全集的别名,也是一种实用的CSS技术,可以参考前面张老师的文章。)

软件为码点时确保使用正确的字符,比如前面例子中的“鹜”(u+9e5c)不要错误地使用“骛”(u+9a9b)。

关于 unicode-range 这种“软截取技术”的使用就介绍这些。接下来我们介绍“硬截取工具”:glyphhanger。

#glyphhanger

glyphhanger是 Zach Leatherman(https://www.zachleat.com/web/) 为 Filament Group(https://www.filamentgroup.com) 写的一个 .ttf 转WOFF/WOFF2的命令行工具,可以:

@font-face

这个工具非常实用方便,下面我们就来演示它的几个典型用法。

首先,全局安装:

npm install -g glyphhanger

用法一:把王勃的千古名句改成“思源宋体”

进入包含例子页面的目录fontSubsetInAction,运行如下命令:

➜ fontSubsetInAction glyphhanger http://127.0.0.1:8080/index.html --family='custom' --subset=SourceHanSerifCN-Light.ttf --formats=woff2

U+3002,U+4E00,U+4E0E,U+5171,U+5929,U+5B64,U+6C34,U+79CB,U+8272,U+843D,U+957F,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C
Writing CSS file: SourceHanSerifCN-Light.css
Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.woff2 (was 12.44 MB, now 3.57 KB)

#这里给glyphhanger传入了4个参数。

要分析的远程文件(这里是一个本地Web服务): http://127.0.0.1:8080/index.html

--family='custom' 指定只分析以上页面中应用了 font-family: custom; 规则的元素

--subset=SourceHanSerifCN-Light.ttf 指定使用的源字体,这里为“思源宋体”(Source Han Serif)

--formats=woff2 指定想要生成的字体子集的目标格式,这里是WOFF2

glyphhanger首先输出了“落霞与孤鹜齐飞,秋水共长天一色。”对应的Unicode码点(包含逗号和句号)。紧接着在当前目录创建了一个名为“SourceHanSerifCN-Light.css”的文件。之后的输出显示,截取的字体叫“SourceHanSerifCN-Light-subset.woff2”,且源字体文件有12.44 MB,子集文件3.57 KB。16个汉字字符就用了3.57 KB,平均每个字符占228字节,吓人吧?!

不过,比起12.44 MB,3.57 KB已经算极小了。下面,看看glyphhanger帮我们生成的CSS文件:

/* This file was automatically generated by GlyphHanger 3.0.3 */
@font-face {
  font-family: custom;
  src: url(SourceHanSerifCN-Light-subset.woff2) format("woff2");
  unicode-range: U+3002,U+4E00,U+4E0E,U+5171,U+5929,U+5B64,U+6C34,U+79CB,
                  U+8272,U+843D,U+957F,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C;
}

直接使用即可,相比之前手工生成码点,这样省事多了。结果如下:

用法二:分析网页用到汉字子集

可能有读者没有注意到,上面例子中glyphhanger输出的码点是按照每个字符在Unicode编码中的顺序从小到大排序过的。而且,这些码点是在自动去重之后排的序。

“落霞与孤鹜齐飞,秋水共长天一色。”没有重复的字,我们再看下面这个例子:

➜ fontSubsetInAction glyphhanger https://lisongfeng.cn/post/dive-into-async-function.html --string

 "#$&'()*+,-./0123456789:;<=>?ACDEFGHIJKLMNOPQRSTUVWXY[]`abcdefghijklmnopqrstuvwxy{|} ©«»—“”…、。一丁三上下不与且两个中串为主么义之乎乘也书了事二于互些交人什仅今介从代以们件任会伟传似但位低住体何作你使例供依便信修倍候值假做停催像儿充先兑入全兮关其典内册再写决况准出函分切列则刚创初利别到制刻前力办功加务动助努包化区升半协单占即却原去参又及反发取受变口句另只叫可各合同名后向吗吧含启呀员呢周味命和品哈哉响哎哥哦啊啥啦喽嘛器回因困围图在地坑块型基塞境增处备复外多大夫失头奇套好如始姐媒媲子字存它完定实家容对导封将小少尔尝就尾层屈展属山崩嵌工己已布带帮常干年并序库应底度建开异式引张强当彻往征待很得循微心必快念态怎思急性总恢息悉悖悲情想意感成我或户所手才打执扩扰找承把抛抢护抽拒括拼拿持按挠捕换据探接控推描提摸操擎支收改效救数文料断新方无既早时明易是显普景智暂更替最月有未末本机杂束条来构析果某查标样核根格案梦检概模次止正此步段每比毕毫永求没法注洞活流浏消涩深添溃满漏演漫点烦烫然照熟版特状独狱环现理甚甜生用由界疼疾白的目直相省看真眼着知码础确示神种秒积称程稍穷立竟端笔符第笼等答简算管箭类糖系索级纯线组细终绍经结给绝绞统继续维综编网置美翻考者而聪肉背能脚自至致节芋苦荒获虽蛮行补表被装要见规览解触计认让议记讲论设访证评译试话该详语误说请诺读调谋象负责败质费资赋越足踩身转载较辑达迅过迈运返还这进远迭述退送适逆逐递通速造逻遍道那部都释里重量鉴针链错键长问闲间队阻际限除随隐集需非靠面页顺须题风饰饱首香驾验高麻默!(),:;?~

字符串去重其实很简单,下面这个简单的JavaScript函数就可以搞定:

function textEliminateDuplicationAndSorting(text) {
    return text.split('').filter((value, index, self) => { 
      return self.indexOf(value) === index;
    }).sort().join('')
}

用法三:指定文本或码点生成字体子集

当然,如果你有现成的文本或码点,也可以只让glyphhanger帮你生成相应源字体的子集和CSS文件。比如我想把“渔舟唱晚,响穷彭蠡之滨,雁阵惊寒,声断衡阳之浦。”也显示为“思源宋体”:

➜  fontSubsetInAction glyphhanger --whitelist="落霞与孤鹜齐飞,秋水共长天一色。渔舟唱晚,响穷彭蠡之滨,雁阵惊寒,声断衡阳之浦。" --subset=SourceHanSerifCN-Light.ttf --css

U+3002,U+4E00,U+4E0E,U+4E4B,U+5171,U+54CD,U+5531,U+58F0,U+5929,U+5B64,U+5BD2,U+5F6D,U+60CA,U+65AD,U+665A,U+6C34,U+6D66,U+6E14,U+6EE8,U+79CB,U+7A77,U+821F,U+8272,U+843D,U+8821,U+8861,U+957F,U+9633,U+9635,U+96C1,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C
Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.ttf (was 12.44 MB, now 13.02 KB)
Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.zopfli.woff (was 12.44 MB, now 9.13 KB)
Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.woff2 (was 12.44 MB, now 7.45 KB)
Writing CSS file: SourceHanSerifCN-Light.css

@font-face {
  src: url(SourceHanSerifCN-Light-subset.woff2) format("woff2"), 
          url(SourceHanSerifCN-Light-subset.zopfli.woff) format("woff"), 
          url(SourceHanSerifCN-Light-subset.ttf) format("truetype");
  unicode-range: U+3002,U+4E00,U+4E0E,U+4E4B,U+5171,U+54CD,U+5531,U+58F0,U+5929,
              U+5B64,U+5BD2,U+5F6D,U+60CA,U+65AD,U+665A,U+6C34,U+6D66,U+6E14,
            U+6EE8,U+79CB,U+7A77,U+821F,U+8272,U+843D,U+8821,U+8861,U+957F,
            U+9633,U+9635,U+96C1,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C;
}

这一次使用 --whitelist 参数传入了要截取的汉字,省略了 --formmats ,增加了 --css 参数。

从结果可以看到,glyphhanger还是对文字进行了去重、转码点和排序。而且,在没有指定 --formats 的情况下,生成了 .ttf 、 woff 和 woff2 三种格式的字体子集,这是为了提高对浏览器的兼容性。最后,除了例行生成CSS文件, --css 选项还让glyphhanger把CSS文件的内容输出到了控制台,便于复制。

但是要注意,CSS文件和输出都没有包含 font-family 属性,也就是没有自定义字体的名字( custom ),使用时必须自己手工加上。好,结果如下:

image.png

安装pyftsubset

glyphhanger本身只做了网页抓取和分析,实际的字体截取使用的是一个著名的Python包 fonttools : https://github.com/fonttools/fonttools。安装方法如下:

pip install fonttools

# Additional installation for --flavor=woff2
git clone https://github.com/google/brotli
cd brotli
python setup.py install

# Additional installation for --flavor=woff --with-zopfli
git clone https://github.com/anthrotype/py-zopfli
cd py-zopfli
git submodule update --init --recursive
python setup.py install

文章最后,为了便于大家参考,我们给出glyphhanger的帮助信息,大家可以自己去探索更多好玩的用法:

➜  fontSubsetInAction glyphhanger -h
glyphhanger error: requires at least one URL or whitelist.

usage: glyphhanger ./test.html
       glyphhanger http://example.com
       glyphhanger https://google.com https://www.filamentgroup.com
       glyphhanger http://example.com --subset=*.ttf
       glyphhanger --whitelist=abcdef --subset=*.ttf

arguments:
  --version
  --whitelist=abcdef
       A list of whitelist characters (optionally also --US_ASCII).
  --string
       Output the actual characters instead of Unicode code point values.
  --family='Lato,monospace'
       Show only results matching one or more font-family names (comma separated, case insensitive).
  --json
       Show detailed JSON results (including per font-family glyphs for results).
  --css
       Output a @font-face block for the current data.
  --subset=*.ttf
       Automatically subsets one or more font files using fonttools `pyftsubset`.
  --formats=ttf,woff,woff2,woff-zopfli
       woff2 requires brotli, woff-zopfli requires zopfli, installation instructions: https://github.com/filamentgroup/glyphhanger#installing-pyftsubset

  --spider
       Gather local URLs from the main page and navigate those URLs.
  --spider-limit=10
       Maximum number of URLs gathered from the spider (default: 10, use 0 to ignore).
  --timeout
       Maximum navigation time for a single URL.

最后

这里推荐一下我的前端学习交流群:784783012,里面都是学习前端的,如果你想制作酷炫的网页,想学习编程。自己整理了一份2018最全面前端学习资料,从最基础的HTML+CSS+JS【炫酷特效,游戏,插件封装,设计模式】到移动端HTML5的项目实战的学习资料都有整理,送给每一位前端小伙伴,有想学习web前端的,或是转行,或是大学生,还有工作中想提升自己能力的,正在学习的小伙伴欢迎加入学习。

点击:加入

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏iKcamp

追溯 React Hot Loader 的实现

文:萝卜(沪江金融前端开发工程师) 本文原创,转载请注明作者及出处 如果你使用 React ,你可以在各个工程里面看到 Dan Abramov 的身影。他于...

496140
来自专栏张善友的专栏

初探ReactJS.NET 开发

ReactJS通常也被称为"React",是一个刚刚在这场游戏中登场的新手。它由Facebook创建,并在2013年首次发布。Facebook认为React在处...

25250
来自专栏学海无涯

Python爬虫之BeautifulSoup

上一篇博文中提到用正则表达式来匹配数据项,但是写起来容易出错,如果有过DOM开发经验或者使用过jQuery的朋友看到BeautifulSoup就像是见到了老朋友...

432100
来自专栏跟着阿笨一起玩NET

关于DevExpress的XtraTreeList使用方法总结

18910
来自专栏企鹅号快讯

使用多个Python库开发网页爬虫(一)

21CTO社区导读:在本篇文章里,我们将讨论使用Python进行网页抓取以及如何引用多个库,如Beautifusoup,Selenium库,以及JavaScri...

41360
来自专栏施炯的IoT开发专栏

《101 Windows Phone 7 Apps》读书笔记-BOOK READER

课程内容 Ø编页 ØList Picker Ø拉伸List Box 控件中的条目     本章的Book Reader应用程序为Jane Austen的经典小...

19860
来自专栏前端知识分享

Vue入门---常用指令详解

Vue是一个MVVM(Model / View / ViewModel)的前端框架,相对于Angular来说简单、易学上手快,近两年也也别流行,发展速度较快,已...

13210
来自专栏SeanCheney的专栏

《Learning Scrapy》(中文版)第2章 理解HTML和XPath使用Chrome浏览器获得XPath表达式常见工作提前应对网页发生改变

为了从网页提取信息,了解网页的结构是非常必要的。我们会快速学习HTML、HTML的树结构和用来筛选网页信息的XPath。 HTML、DOM树结构和XPath 从...

327120
来自专栏互联网杂技

jQuery 升级踩坑大全

背景 jQuery想必各个web工程师都再熟悉不过了,不过现如今很多网站还采用了很古老的jQuery版本。其实如果早期版本使用不当,可能会有DOMXSS漏洞,非...

67080
来自专栏程序员的诗和远方

在React Native中优雅的使用iconfont

React Native大火大热,其中为了解决图标,易于修改,换颜色,高清等需求,iconfont的应用更是必不可少。 React Native中的ic...

62840

扫码关注云+社区

领取腾讯云代金券