某些场景需要动态加载网络字体,整体思路为: 当需要加载字体时,向DOM
添加@font-face
,动态加载字体
问题在于字体包的大小,中文包小则1MB,大则10MB,而实际使用到的字符可能仅有几个,全量加载会造成带宽浪费
此时可用字体子集化来解决,用户需要加载字体时,向服务器发送字体信息和实际使用字符,服务器生成字体子集返回,以下表为参考,子集中的1个字符仅占原字体的 0.0014% ~ 0.0134%
字符集 | 字符数 | 单字符占比 |
---|---|---|
GB2312 | 汉字6763个 图形682个 | 0.0134% |
GBK | 汉字21003个 | 0.0047% |
GB18030 | 汉字70244个 | 0.0014% |
子集化字体工具
Fontmin
Fontmin
是百度出品的一款字体子集化工具
官方网站:http://ecomfe.github.io/fontmin
命令行调用
npm i -g fontmin // 安装
fontmin -h // 帮助
nodejs调用
const Fontmin = require('fontmin')
new Fontmin()
.src('ysbth.ttf') // 源
.dest('./build') // 目标路径
.use(Fontmin.glyph({
text: 'text' // 子集化字符
}))
.run((err, files) => {
if (err) throw err
})
sfnttool
这是一个Google开源项目sfntly中的一个工具
官方仓库:https://github.com/googlefonts/sfntly
使用方法:
-
安装ant: 搜索
apache ant
,左侧 Download -> Binary Distributions,选一个包下载解压,bin 配置到环境变量 -
下载编译: 编译后会在
dist/tools/sfnttool
生成sfnttool.jar
文件
git clone https:xxx
cd sfntly/java
ant
- 调用: 进入jar包同目录,执行
java -jar sfnttool.jar -h // 帮助
在java中使用命令行调用sfnttool
需要注意的是传递的字符串参数存在转义字符,需稍加处理
String minStr = "子集化字符串";
/**
* 对部分字符进行处理
* \ —> \\
* " —> \"
*/
minStr = minStr.replace("\\", "\\\\")
.replace("\"", "\\\"");
Process proc = Runtime.getRuntime().exec(
String.format("java -jar sfnttool.jar -s \"%s\" %s %s", minStr, "源字体目录", "子集化后保存目录"),
null,
new File("jar包所在目录"));
// 等待操作完成,不加则为异步
proc.waitFor();
前端动态加载字体
服务器收到请求后生成子集字体包,将字体包网络路径返回给前端,然后通过此方法加载字体:
let fontname = "xxx.xxx"
let fontpath = "http://xxx/xxx.xxx"
let style = document.createElement('style')
style.type = "text/css"
style.innerText = '@font-face{font-family:' + fontname + ';src:url("' + fontpath + '")}'
document.getElementsByTagName('head')[0].appendChild(style)
逻辑优化
每次发送请求给服务器时,如果没有对内容进行唯一性判断,可能会遇到生成子集完全相同的情况,浪费带宽和算力,所以前端请求前应该进行如下操作:
- 对子集化字符串进行排序去重
let minStr = "xxx"
// 切割成单个字符数组
let minStrCharArr = minStr.match(/.{1}/g)
let simpStrArr = []
// 去重
for(let i = 0; i < minStrCharArr.length; i++) {
let char = minStrCharArr[i]
let haveFlag = false
for(let j = 0; j < simpStrArr.length; j++) {
if(simpStrArr[j] == minStrCharArr[i]) {
haveFlag = true
break
}
}
if(!haveFlag) {
simpStrArr.push(char)
}
}
// 排序
for(let i = 0; i < simpStrArr.length; i++) {
for(let j = i + 1; j < simpStrArr.length; j++) {
if(simpStrArr[j] > simpStrArr[i]) {
let c = simpStrArr[i]
simpStrArr[i] = simpStrArr[j]
simpStrArr[j] = c
}
}
}
String.prototype.replaceAll = function(s1,s2){
return this.replace(new RegExp(s1,"gm"),s2)
}
minStr = simpStrArr.toString().replaceAll(",", "")
}
let fontname = "xxx.xxx"
let minStr= "xxx"
// 前端和后端约定好,以md5(字体包名+子集化字符串)的形式命名,前端凭约定计算出要请求的路径和字体名
let webFamilyName = md5(fontName + minStr)
let styleinner = '@font-face{font-family:' + webFamilyName + ';src:url("http://xxx/' + webFamilyName + '.xxx")}'
// 如果之前生成过同样的子集字体包,就不请求服务器直接使用
let styles = document.getElementsByTagName('head')[0].getElementsByTagName('style')
for(let i = 0; i < styles.length; i++) {
if(styles[i].innerText == styleinner) {
// 直接使用
}
}
借鉴
学习一下 "字由" 的解决方案,发现它会生成各种格式的字体以提高兼容性,可以借鉴
