使用 Fontmin 根据网页内容生成子集字体文件
在使用 Hexo 生成博客页面时,有时会需要自定义页面上的字体,然而有些字体并非所有用户设备上都已经安装,系统默认的字体可能达不到需求的要求,直接修改 font-family 不能保证一致的浏览体验。
想要在所有设备都能以指定字体显示,可以通过引入字体文件结合 CSS 的 fontface 为用户提供自定义的字体文件。在 fontface 中指定字体文件资源的路径,浏览器就会下载对应的字体文件。
字体文件压缩
通常一个字体文件的大小较大,例如 MiSans:
1 2 3
| Mode LastWriteTime Length Name ---- ------------- ------ ---- ------ 2023/8/22 17:18 5409536 MiSans-Regular.woff
|
单是一个字体文件就超过 5MB,对于带宽较小的服务器来说,加载耗时会很长。页面加载出来过一段时间才能使用到自定义的字体,这显然体验不好。
使用 CDN 加速是一种方案,然而并非所有的字体都有免费 CDN 的支持,因此这里引入一个第三方库 Fontmin 对完整的字体文件进行压缩,压缩后的文件通常只有几十KB,较大也只有一两百KB,不会造成过大网络压力。
Fontmin
Fontmin 是一个缩小字体文件的第三方库,相关链接:
官方文档 | Github
利用这一第三方库提供的字体文件处理功能,可以实现按需生成字体文件。只有 Hexo 生成的页面上存在的文字才提供对应的字体文件,能大幅度缩小字体文件的大小。
脚本处理
本文脚本基于 Fontmin 进行了二次封装,分析生成的 HTML 所包含的字体,生成压缩后的字体文件,注入引用到 HTML 中,实现自定义字体。
使用方法:
- 安装依赖
npm install -D cheerio fontmin@1
- 在项目根目录新建文件
/scripts/font.js,并写入以下内容。(没有scripts文件夹可以新建)
- 替换配置项的内容
fontPath 需要使用的字体源文件路径
outputDir 字体压缩后文件输出的路径
FONT_NAME 字体名称
DIST_PATH 字体在页面上引用的路径(通常是相对于生成的文件目录,默认为public下)
CSS_PATH 在页面上引用自定义字体的 CSS 文件路径
- Hexo 在生成文件时会自动运行
scripts 下的脚本文件,生成成功将在 输出文件夹/fonts/xxx.woff2 路径下生成压缩后的字体文件。
注意:脚本执行环境要求 Node.js 18 以上,没有相应环境的可以参考上篇的解决方案。
Fontmin 2 只支持 ES Module 的导入方法,只有 v1 才支持 CommonJS 的导入,建议安装 v1 使用该脚本。
脚本内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| const Fontmin = require("fontmin"); const cheerio = require("cheerio"); const path = require("path"); const { promisify } = require("util"); const { finished } = require("stream"); const log = hexo.log || log.log;
const fontPath = path.join( __dirname, "../source", "/path/to/font/xxx.ttf" ); const outputDir = path.join(__dirname, "../public", "path/to/font/"); const FONT_NAME = "xxx"; const DIST_PATH = `/path/to/font/${FONT_NAME}`; const CSS_PATH = `/assets/styles/font.css`;
const styleData = `@font-face { font-family: '${FONT_NAME}'; src: url('${DIST_PATH}.woff2') format('woff2'), url('${DIST_PATH}.ttf') format('truetype'); font-weight: normal; font-style: normal; } body { font-family: '${FONT_NAME}', system-ui, "Microsoft Yahei", "Segoe UI", -apple-system, Roboto, Ubuntu, "Helvetica Neue", Arial, "WenQuanYi Micro Hei", sans-serif; } .code *, code *{ font-family: Consolas, '${FONT_NAME}', Menlo, Monaco, Consolas, system-ui, "Courier New", monospace, sans-serif; }`;
hexo.extend.filter.register("after_generate", async () => { const htmlFiles = hexo.route.list().filter((path) => path.endsWith(".html"));
let text = "";
const processFile = async (filePath) => { let html = ""; const stream = hexo.route.get(filePath); stream.on("data", (chunk) => { html += chunk; }); stream.on("end", () => { const $ = cheerio.load(html); $("head").append( `<link rel="stylesheet" href="${CSS_PATH}" type="text/css">` ); hexo.route.set(filePath, $.html()); text += html; }); return promisify(finished)(stream); };
log.log("Generating font..."); let start = Date.now();
try { await Promise.all(htmlFiles.map(processFile)); const textLib = [...new Set(text)].join(""); const fontmin = new Fontmin() .src(fontPath) .use( Fontmin.glyph({ text: textLib, hinting: false, }) ) .use(Fontmin.ttf2woff2()) .dest(outputDir); await promisify(fontmin.run.bind(fontmin))(); hexo.route.set(CSS_PATH, styleData); let end = Date.now(); log.log( `Font [${FONT_NAME}] generated successfully in ${( (end - start) / 1000 ).toFixed(1)} s` ); } catch (error) { log.error(`Font ${FONT_NAME} generation failed: ${error}`); console.error(error); } });
|