背景
在实际开发需求中,字体包(如 .ttf
、.woff
、.woff2
等格式)通常被用来确保应用的字体显示一致性,尤其是在跨平台和跨设备的环境下。在app中为了提升性能并确保用户能够离线使用这些资源,通常会将字体包随应用一起打包并通过 CDN 分发,这样可以减少用户等待时间,提升加载速度。而在h5,或者web内同时使用CDN来进行内容分发,再或者就是在使用img图片来展示类似花边字体的实现。
在本篇文章我们只讨论在线网络情况,因为若是离线包存在问题,通常需要重新发布整个离线包。
回顾
在过去我们尝试使用@font-face来因此在线CDN字体样式,让浏览器下载并使用指定的字体来显示文本。这里我准备了一个5.2mb的字体包,这个包并不算大相对于项目中的大字体包而言。
后面我启动本地服务器,前端使用@font-face向本地服务器请求这个ttf字体包,并使用到页面中
/* 使用 @font-face 加载自定义字体 */
@font-face {
font-family: 'Roboto'; /* 自定义字体名称 */
src: url('http://localhost:3001/fonts/LongCang-Regular.ttf') format('truetype');
/* 这里是字体文件的路径 */
}
/* 应用该字体到页面 */
body {
font-family: 'Roboto';
}
可以看到请示的字体大小为5.2mb耗时24ms,这是是在我本地请求的情况下,换作用户场景这个请求时间就要翻倍了,因此这个时间其实是不能被我们接受的。
改进方案
我们知道了@font-face直接请求的弊端:下载字体文件,会增加页面加载时间,尤其是在字体文件较大的情况下。 那前端有没有比使用@font-face更好的方案呢? 不好意思暂时都是通过@font-face来引入。
但是我们可以对需要的字体按需引入,就不用去导那么大的ttf包了,怎么字体还能按需引入?怎么按需,这里同样我在本地去模拟一个基本场景。
这里先推荐一个ttf文件切割工具fontmin
我在后端将使用fontmin来切割,前端可以通过params来传递需要切割的文案,当后端收到请求后,便开始切割出需要的字符返回给前端。
import express from 'express';
import path from 'path';
import fs from 'fs';
import cors from 'cors';
import { default as Fontmin } from 'fontmin';
import { fileURLToPath } from 'url';
// 修正 __dirname 的获取方式
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const port = 3000;
app.use(cors());
// 设置字体文件所在目录和输出目录
const fontDir = path.join(__dirname, 'fonts');
const outputDir = path.join(__dirname, 'output');
// 确保目录存在
if (!fs.existsSync(fontDir)) {
fs.mkdirSync(fontDir, { recursive: true });
}
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/cut-font', async (req, res) => {
try {
const { text } = req.body;
console.log('收到请求,文本内容:', text);
console.log('当前工作目录:', process.cwd());
console.log('字体目录:', fontDir);
console.log('输出目录:', outputDir);
if (!text) {
return res.status(400).json({ error: '请提供要裁剪的文字' });
}
const sourceFontPath = path.join(fontDir, 'LongCang-Regular.ttf');
// 检查字体文件是否存在
if (!fs.existsSync(sourceFontPath)) {
console.error('字体文件不存在:', sourceFontPath);
return res.status(404).json({
error: '字体文件不存在',
path: sourceFontPath,
dirs: {
fontDir,
outputDir,
currentDir: __dirname,
},
});
}
// 创建唯一的输出文件名
const outputFileName = `font-${Date.now()}.ttf`;
const outputPath = path.join(outputDir, outputFileName);
const fontmin = new Fontmin()
.src(sourceFontPath)
.use(
Fontmin.glyph({
text: text,
hinting: false,
}),
)
.dest(outputDir);
fontmin.run((err, files) => {
if (err) {
console.error('字体处理错误:', err);
return res
.status(500)
.json({ error: '字体处理失败', details: err.message });
}
if (!files || files.length === 0) {
return res.status(500).json({ error: '未生成字体文件' });
}
const outputFontPath = files[0].path;
console.log('生成的字体文件路径:', outputFontPath);
// 检查生成的文件是否存在
if (!fs.existsSync(outputFontPath)) {
return res.status(500).json({ error: '生成的字体文件未找到' });
}
res.setHeader('Content-Type', 'font/ttf');
res.setHeader(
'Content-Disposition',
`attachment; filename="${outputFileName}"`,
);
const stream = fs.createReadStream(outputFontPath);
stream.on('error', (err) => {
console.error('文件读取错误:', err);
res.status(500).json({ error: '文件读取失败' });
});
stream.pipe(res);
stream.on('end', () => {
// 延迟删除文件
setTimeout(() => {
try {
fs.unlinkSync(outputFontPath);
console.log('临时文件已清理:', outputFontPath);
} catch (e) {
console.error('清理临时文件失败:', e);
}
}, 1000);
});
});
} catch (error) {
console.error('服务器错误:', error);
res.status(500).json({ error: '服务器内部错误', details: error.message });
}
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
console.log('字体目录:', fontDir);
console.log('输出目录:', outputDir);
});
前端页面我们将参数拼接上去:
记住我们这里传递的参数,待会儿看页面的字体展示
在首次加载中只使用了1ms的网络耗时,这里相差了30倍就不用多说,毕竟切割出来的字体包大小也是相差了快50倍了。换句话说,减少了用户的流量负担,加快了用户页面打开速度。
这里补充一下:有人会说拿到你后端切割不要时间吗,这里是我模拟开发,生产环境中这些字体都是被切割好分发到CDN中,不需要我们去在每次用户请求时都重新切割。
至于肯定又有人会说,那你直接放一个需要的字体包上去不就行了吗。话是这样说没错,但是制作精美的ttf字体包十分耗时的,文案的修改是常常发生的事情。当需要换文案或者线上字体错误时:
产品计划书->修改项目字体配置的文件->本地验证文案裁剪效果->代码提交CR->流水线构建测试->发布审核、上->css资源有变动,CDN铺量
这些步骤都是是十分耗时的,并且还要去重新让设计师去制作字体。
优化
在最新方案中,我们又有了更加便捷的方法,当我们发布的离线包版本中已经将字体CDN地址注入,而这时又发生线上文案错误,这是重新去更新用户内存中的离线包肯定是不现实的。解决方法便是重新裁剪去需要的ttf包,然后测试分发到另外的CDN中,通过公式内部的代理器方法类似于nginx代理一样的方法,当然如果你们项目就是使用nginx来进行代理请求,那也可以不用更新用户的资源包,直接修改nginx的代理地址便可以了。
生成的新字体文件上传后,代理 中的 font
的映射字段会更新,新的 CDN 地址会被注入到 H5 页面中。在 H5 页面加载时,页面通过注入的 CSS 规则(@font-face
)引用最新的 CDN 地址,确保页面展示最新的字体。
总结
回顾本文我们主要总结的当前流行的字体包下发错误或者文案修改解决方案
- 对于字体包的优化: 使用按需裁剪而不是将整个字体包发送给用户
- 对于修复样式or更新: 使用代理处理转发处理上传新的字体文件和修改指向