加载中

前端动态字体解决方案

image

某些场景需要动态加载网络字体,整体思路为: 当需要加载字体时,向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

使用方法:

  1. 安装ant: 搜索apache ant,左侧 Download -> Binary Distributions,选一个包下载解压,bin 配置到环境变量

  2. 下载编译: 编译后会在dist/tools/sfnttool生成sfnttool.jar文件

git clone https:xxx
cd sfntly/java
ant
  1. 调用: 进入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)

逻辑优化

每次发送请求给服务器时,如果没有对内容进行唯一性判断,可能会遇到生成子集完全相同的情况,浪费带宽和算力,所以前端请求前应该进行如下操作:

  1. 对子集化字符串进行排序去重
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) {
    // 直接使用
  }
}

借鉴

学习一下 "字由" 的解决方案,发现它会生成各种格式的字体以提高兼容性,可以借鉴

posted @ 2020-11-07 17:14  jialeYang  阅读(2328)  评论(0编辑  收藏  举报