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
小技巧:source 参数支持 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 知识结构总览

说了这么多,我画张图帮你理清这几个概念之间的关系:

Font Loading API 核心结构 FontFace 对象 字体的「身份证」 document.fonts 字体的「户籍管理处」 三个核心方法 load / ready / check add() load() 主动加载指定字体 返回 Promise<FontFace[]> ready() 等待所有字体就绪 返回 Promise<FontFaceSet> check() 检测字体是否可用 返回 boolean 实战建议 load() 精确控制 + ready() 全局等待 + check() 渐进增强

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 给了你前所未有的控制力。用好它,字体加载就不再是玄学,而是可以精确掌控的工程实践。