某些場景需要動態加載網絡字體,整體思路為: 當需要加載字體時,向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) {
// 直接使用
}
}
借鑒
學習一下 "字由" 的解決方案,發現它會生成各種格式的字體以提高兼容性,可以借鑒
