3、Font Loading API:FontFace对象、document.fonts集合、load()方法、ready()方法、check()方法
前面我们聊了 CSS 的 @font-face 和 font-display,它们确实能解决一部分问题。但说实话,CSS 的方式有点「被动」——你只能设置策略,没法精确控制字体到底什么时候加载完。
真正让我觉得「嗯,这才对嘛」的,是 JavaScript 层面的 Font Loading API。它给了你一把钥匙,让你能主动监听、控制字体的整个生命周期。
我第一次用这个 API 是在一个电商大促页面。设计师用了三款自定义字体,结果页面加载时字体闪烁了好几次,用户反馈说「看着头晕」。我当时就想,能不能等所有字体都加载完了,再统一渲染?Font Loading API 帮我搞定了。
3.1 FontFace 对象:字体的「身份证」
先说说 FontFace。你可以把它理解成字体的「身份证」——它描述了一个字体的所有关键信息:来源、样式、权重、是否已加载等等。
创建一个 FontFace 对象很简单:
const myFont = new FontFace('MyCustomFont', 'url(/fonts/myfont.woff2)', {
style: 'normal',
weight: '400',
stretch: 'normal'
});
三个参数分别是:
- family:字体名称,对应 CSS 中的
font-family - source:字体文件路径,可以是单个 URL 字符串,也可以是
ArrayBuffer或Blob - descriptors(可选):一个对象,包含
style、weight、stretch、unicodeRange、variant、featureSettings
ArrayBuffer,这意味着你可以从网络请求中拿到字体数据,再动态创建 FontFace。我在一个离线应用里就用过这个方式,先把字体缓存到 IndexedDB,下次直接从本地加载,速度飞快。
FontFace 对象有几个重要的属性,我列个表方便你对照:
| 属性 | 类型 | 说明 |
|---|---|---|
family |
string | 字体名称,只读 |
style |
string | 字体样式,如 normal、italic |
weight |
string | 字重,如 '400'、'700' |
status |
string | 加载状态:unloaded、loading、loaded、error |
loaded |
Promise<FontFace> | 加载完成后 resolve,失败则 reject |
这里最核心的是 status 和 loaded。status 告诉你当前状态,loaded 是一个 Promise,你可以 await 它,等字体加载完再干活。
3.2 document.fonts 集合:字体的「户籍管理处」
光有 FontFace 对象还不够,你得把它「注册」到文档中,字体才能真正生效。这时候就要用到 document.fonts 了。
document.fonts 是一个 FontFaceSet 对象,你可以把它想象成字体的「户籍管理处」。所有页面中用到的自定义字体,都在这里登记。
基本操作就三个:
// 添加字体
document.fonts.add(myFont);
// 删除字体
document.fonts.delete(myFont);
// 清空所有自定义字体
document.fonts.clear();
添加之后,字体并不会自动加载。你得手动调用 load() 方法,或者等浏览器按需加载。我个人习惯是添加完就立刻加载,避免后续使用时出现闪烁。
document.fonts.add() 添加的字体,优先级高于 CSS 中 @font-face 定义的字体。如果你同时用两种方式定义了同名字体,JS 添加的会覆盖 CSS 的。我曾经因为这个踩过坑,排查了半天才发现是优先级问题。
3.3 load() 方法:主动加载,精准控制
load() 方法是你主动触发字体加载的「开关」。它接收一个字体描述字符串,返回一个 Promise。
// 加载单个字体
document.fonts.load('16px MyCustomFont').then(() => {
console.log('字体加载完成');
});
// 加载多个字体
Promise.all([
document.fonts.load('16px MyCustomFont'),
document.fonts.load('bold 16px MyCustomFont')
]).then(() => {
console.log('所有字体都加载完了');
});
参数格式和 CSS 的 font 简写属性一样:[font-style] [font-weight] [font-size] font-family。注意 font-size 是必填的,但实际加载时这个尺寸并不重要,它只是用来匹配字体描述符。
我一般这么用:
async function loadCustomFonts() {
const fonts = [
{ family: 'MyFont', weight: '400', url: '/fonts/myfont-regular.woff2' },
{ family: 'MyFont', weight: '700', url: '/fonts/myfont-bold.woff2' }
];
for (const f of fonts) {
const fontFace = new FontFace(f.family, `url(${f.url})`, { weight: f.weight });
await fontFace.load(); // 先加载 FontFace 对象
document.fonts.add(fontFace); // 再添加到集合
}
// 现在可以放心使用字体了
document.body.style.fontFamily = 'MyFont, sans-serif';
}
这样写的好处是:每个字体加载失败都不会影响其他字体。而且你可以针对每个字体做错误处理,比如加载失败时回退到系统字体。
3.4 ready() 方法:等所有字体就绪
ready() 方法返回一个 Promise,当页面中所有字体(包括 CSS @font-face 定义的)都加载完成或失败后,这个 Promise 才会 resolve。
它和 load() 的区别很明显:
load()是主动加载你指定的字体ready()是等待所有字体自然加载完毕
document.fonts.ready.then(() => {
console.log('所有字体都处理完了');
// 可以安全地渲染文本了
document.getElementById('content').style.visibility = 'visible';
});
我在实际项目中常用 ready() 来做「字体加载完成前隐藏文本」的效果。比 CSS 的 font-display: block 更可控,因为你可以精确控制隐藏多久、显示时的动画效果。
load() 是「我来指定加载哪些字体」,ready() 是「我等所有字体自己加载完」。前者适合精确控制,后者适合全局等待。
3.5 check() 方法:快速检测字体是否可用
check() 方法用来检测某个字体是否已经加载并可用。它返回一个布尔值。
// 检查字体是否可用
if (document.fonts.check('16px MyCustomFont')) {
console.log('字体已就绪,可以直接使用');
} else {
console.log('字体还没加载,需要等待');
}
这个方法特别适合做「渐进增强」:如果字体已经加载了,就用自定义字体;否则先用系统字体顶着,等字体加载完再切换。
我写过这样一个工具函数:
function useFontIfReady(family, fallback) {
const testString = '16px ' + family;
if (document.fonts.check(testString)) {
return family;
}
// 监听字体加载事件
document.fonts.ready.then(() => {
if (document.fonts.check(testString)) {
document.body.style.fontFamily = family;
}
});
return fallback;
}
// 使用
document.body.style.fontFamily = useFontIfReady('MyCustomFont', 'Arial, sans-serif');
这样用户打开页面时,如果字体还没加载完,会先用 Arial 显示内容,等字体加载完再无缝切换。体验比白屏或者闪烁好太多了。
3.6 知识结构总览
说了这么多,我画张图帮你理清这几个概念之间的关系:
3.7 避坑指南
最后分享几个我踩过的坑,你遇到了能少走弯路:
- 跨域问题:字体文件如果放在 CDN 上,记得配
Access-Control-Allow-Origin头。否则FontFace.load()会静默失败,连错误都不抛。我第一次遇到时排查了整整一下午。 - 重复加载:同一个字体不要重复
add()多次。虽然不会报错,但会浪费性能。我习惯在add()之前先check()一下。 - 字体描述符要匹配:
load()的参数必须和FontFace构造时的描述符一致,否则会加载失败。比如你构造时设了weight: '700',但load('16px MyFont')默认是weight: '400',就匹配不上。 - 内存泄漏:如果动态创建了大量
FontFace对象但没有及时清理,会占用内存。页面切换或组件卸载时记得调用document.fonts.delete()。
Font Loading API 给了你前所未有的控制力。用好它,字体加载就不再是玄学,而是可以精确掌控的工程实践。