2、字体加载事件:@font-face规则详解、font-display属性、字体加载的生命周期

字体加载,说白了就是浏览器去服务器「取」字体文件的过程。这个过程不像你想的那么简单——它涉及网络请求、解析、渲染,还有一堆你可能没注意到的细节。我刚开始做前端时,就因为在字体加载上栽过跟头,页面白屏了好几天才找到原因。

这一节,我们就把 @font-face 规则、font-display 属性,以及字体加载的完整生命周期讲透。你掌握了这些,以后遇到字体加载问题,心里就有底了。

2.1 @font-face 规则详解

@font-face 是 CSS 中定义自定义字体的核心规则。它的作用,就是告诉浏览器:「嘿,我这里有个字体文件,你拿去用吧。」

基本语法长这样:

@font-face {
  font-family: 'MyCustomFont';
  src: url('fonts/myfont.woff2') format('woff2'),
       url('fonts/myfont.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

这里面有几个关键属性,我一个个说。

2.1.1 font-family:给字体起个名字

这个属性定义字体的「别名」。你在页面其他地方用 font-family: 'MyCustomFont' 时,浏览器就知道要加载这个字体文件了。

命名时注意:

  • 名字可以包含空格,但要用引号括起来
  • 避免和系统字体重名,比如别叫 'Arial' 或 'Helvetica'
  • 我习惯用项目名+字体名的方式,比如 'ProjectName-Regular'

2.1.2 src:告诉浏览器字体文件在哪

src 属性指定字体文件的路径和格式。这里有个优先级问题:浏览器会按顺序尝试加载,第一个能用的就用它。

我建议的写法:

src: url('fonts/myfont.woff2') format('woff2'),
     url('fonts/myfont.woff') format('woff'),
     url('fonts/myfont.ttf') format('truetype');

为什么 woff2 放前面?因为它的压缩率最高,体积最小。浏览器支持 woff2 就用它,不支持就降级到 woff,再不行就用 ttf。我在项目中遇到过,有些老旧浏览器只认 ttf,所以保留一个兜底格式很重要。

小技巧: 使用 local() 可以优先检查用户本地是否已安装该字体。比如 src: local('MyFont'), url('...')。这样能省一次网络请求。

2.1.3 font-weight 和 font-style:定义字体的「变体」

这两个属性告诉浏览器,这个字体文件对应的是哪个字重和样式。比如:

@font-face {
  font-family: 'MyFont';
  src: url('myfont-bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
}

@font-face {
  font-family: 'MyFont';
  src: url('myfont-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
}

这样,你在 CSS 里写 font-weight: 700 时,浏览器就会加载粗体版本。写 font-style: italic 时,加载斜体版本。每个变体都需要单独定义 @font-face。

注意: 不要在一个 @font-face 里放多个字重。每个 @font-face 只对应一个字体文件。否则浏览器会强制拉伸字体,效果惨不忍睹。我曾经见过有人把整个字体族塞进一个 @font-face,结果页面上的字全是变形金刚。

2.2 font-display 属性:控制字体加载时的「等待策略」

font-display 是 @font-face 里的一个属性,但它太重要了,值得单独拿出来讲。它决定了浏览器在字体加载完成之前,怎么处理页面上的文字。

说白了,就是三个选择:

  • 等字体加载完再显示文字(FOIT:Flash of Invisible Text)
  • 先用系统字体显示,加载完再替换(FOUT:Flash of Unstyled Text)
  • 等一会儿,加载不完就用系统字体(折中方案)

font-display 有五个可选值:

值 行为描述 适用场景
auto 浏览器默认行为,通常是 FOIT,等待时间很短 不关心加载策略时
block 先隐藏文字,等字体加载完再显示(无限等待) 图标字体、品牌字体,必须用自定义字体
swap 先用系统字体显示,加载完立即替换 正文内容,用户体验优先
fallback 先隐藏约 100ms,然后用系统字体显示;如果 3 秒内加载完就替换 折中方案,兼顾品牌和体验
optional 先隐藏约 100ms,然后用系统字体显示;如果加载慢就不替换了 网络差时,避免字体闪烁

我个人习惯:正文用 swap,图标字体用 block,品牌标题用 fallback。你想想看,用户打开你的页面,如果正文文字一直空白,他肯定以为页面坏了。先用系统字体显示,等自定义字体加载完再替换,体验就好很多。

核心原则: 字体加载不应该阻塞页面内容的可读性。优先保证用户能看到文字,再考虑字体好不好看。

2.3 字体加载的生命周期

字体加载不是一瞬间完成的。它有一个完整的状态机,包含四个阶段:pending、loading、loaded、error。

我用一张图来展示这个流程:

字体加载生命周期流程图 字体加载生命周期 Pending Loading Loaded Error 开始加载 加载成功 加载失败 Pending 阶段 字体声明已解析 但尚未发起网络请求 Loaded 阶段 字体文件已下载并解析 页面开始使用自定义字体

2.3.1 Pending(待加载)

这是字体加载的起点。浏览器解析到 @font-face 规则后,会创建一个字体对象,但还没发起网络请求。此时字体处于「待命」状态。

触发条件:

  • 页面中某个元素使用了这个字体
  • 或者 CSS 中引用了这个字体

如果没有任何元素使用这个字体,浏览器不会发起加载。嗯,这里要注意:你定义了 @font-face 但没用它,浏览器是不会下载字体文件的。

2.3.2 Loading(加载中)

浏览器开始下载字体文件。这个阶段,网络请求已经发出,字体数据正在传输。

这个阶段的特点:

  • 浏览器会检查字体文件的格式是否合法
  • 如果字体文件很大,加载时间会很长
  • font-display 属性在这个阶段开始生效

我在项目中遇到过,一个中文字体文件动辄几 MB,加载时间能到好几秒。这时候如果 font-display 设置不当,页面就会长时间空白。

2.3.3 Loaded(加载完成)

字体文件下载完成,浏览器解析成功。此时页面上的文字会从系统字体切换到自定义字体。

这个阶段会触发一个事件:document.fonts.ready。你可以用 JavaScript 监听这个事件:

document.fonts.ready.then(function() {
  console.log('所有字体加载完成!');
  // 可以在这里做一些后续操作,比如隐藏加载提示
});

或者监听单个字体的加载状态:

var font = new FontFace('MyFont', 'url(myfont.woff2)');
font.load().then(function() {
  console.log('MyFont 加载成功');
  document.fonts.add(font);
}).catch(function() {
  console.log('MyFont 加载失败');
});

2.3.4 Error(加载失败)

字体加载失败的原因有很多:

  • 网络问题:服务器挂了、网络断了
  • 文件问题:字体文件损坏、格式不对
  • CORS 问题:跨域请求没有设置正确的响应头
  • 路径问题:URL 写错了
避坑指南: 我曾经因为字体文件放在 CDN 上,但 CDN 没有设置 Access-Control-Allow-Origin 头,导致字体加载失败。页面上的文字全部变成了系统字体,而且控制台没有任何报错。排查了半天才发现是 CORS 问题。记住:跨域字体请求必须设置正确的 CORS 头。

加载失败后,浏览器会使用 fallback 字体(你指定的后备字体或系统默认字体)。页面不会崩溃,但视觉效果会打折扣。

2.4 实战:监听字体加载事件

说了这么多理论,我们来点实战。怎么在代码里监听字体的加载状态?

方法一:使用 CSS Font Loading API

// 检查字体是否已加载
if (document.fonts.check('12px MyFont')) {
  console.log('字体已加载');
} else {
  console.log('字体尚未加载');
}

// 监听所有字体加载完成
document.fonts.ready.then(function() {
  console.log('所有字体就绪');
});

方法二:使用 FontFace 对象

// 手动创建并加载字体
var myFont = new FontFace('MyFont', 'url(myfont.woff2)');

myFont.load().then(function(loadedFont) {
  // 加载成功,添加到文档
  document.fonts.add(loadedFont);
  document.body.style.fontFamily = 'MyFont, sans-serif';
}).catch(function(error) {
  // 加载失败,使用后备字体
  console.error('字体加载失败:', error);
  document.body.style.fontFamily = 'sans-serif';
});
个人建议: 在实际项目中,我习惯把字体加载状态和页面 loading 动画结合起来。字体加载完成前显示一个简单的加载提示,加载完成后隐藏。这样用户体验会好很多。

好了,关于字体加载事件的核心内容就这些。你掌握了 @font-face 的配置、font-display 的策略选择,以及字体加载的四个生命周期阶段,以后遇到字体加载问题,就能从容应对了。