JS `Font Loading API`:优化自定义字体加载与 FOUT/FOIT

各位靓仔靓女,晚上好!我是今晚的字体加载问题专家(之一,毕竟专家太多了)。今天咱们聊聊前端字体加载那些事儿,尤其是 JS Font Loading API,帮你告别 FOUT 和 FOIT 的烦恼。保证让你的网页字体加载又快又稳,逼格瞬间提升!

开场白:字体,网页的颜值担当

话说,咱们做前端的,谁不想让自己的页面美美的?字体,绝对是提升颜值的关键因素。想想那些设计精美的网站,哪个不是在字体上下足了功夫?

但是!理想很丰满,现实很骨感。自定义字体用起来爽,加载起来却可能让你吐血。最常见的就是 FOUT (Flash of Unstyled Text) 和 FOIT (Flash of Invisible Text)。

  • FOUT: 先显示默认字体,然后突然切换成自定义字体,页面“Duang”的一下,丑爆了。
  • FOIT: 页面先啥也不显示,等自定义字体加载完才出现,用户体验极差。

这俩货就像网页界的“牛皮癣”,影响美观不说,还影响用户体验。还好,咱们有 Font Loading API 这把利剑,可以斩妖除魔,让字体加载变得可控。

第一部分:认识 Font Loading API

Font Loading API 是一组 JS API,允许你:

  • 检测字体是否加载完成。
  • 监听字体加载事件。
  • 控制字体加载行为。

简单来说,就是让你可以更精确地控制字体加载的过程,从而避免 FOUT 和 FOIT。

核心接口:FontFacedocument.fonts

Font Loading API 的核心是 FontFace 接口和 document.fonts 对象。

  • FontFace 接口: 代表一个自定义字体。你可以用它来定义字体的 font-familysrcfont-weight 等属性。

  • document.fonts 对象: 代表当前文档中所有已加载和正在加载的字体集合。它提供了一些方法来管理字体,比如添加、删除、检测字体等。

举个栗子:创建并加载一个字体

// 1. 创建 FontFace 对象
const myFont = new FontFace('MyCustomFont', 'url(./fonts/MyCustomFont.woff2)');

// 2. 加载字体
myFont.load().then(function() {
  // 3. 将字体添加到 document.fonts
  document.fonts.add(myFont);

  // 4. 应用字体
  document.body.style.fontFamily = 'MyCustomFont, sans-serif';

  console.log('字体加载完成!');
}).catch(function(error) {
  console.error('字体加载失败:', error);
});

这个例子做了以下几件事:

  1. 创建了一个名为 MyCustomFontFontFace 对象,并指定了字体文件的 URL。
  2. 调用 myFont.load() 方法开始加载字体。这个方法返回一个 Promise,可以在加载成功或失败时执行相应的操作。
  3. 如果字体加载成功,就调用 document.fonts.add(myFont) 将字体添加到文档中。
  4. 最后,将 document.bodyfontFamily 设置为 MyCustomFont,这样整个页面的字体就变成了自定义字体。

第二部分:实战:优雅地加载字体,告别 FOUT 和 FOIT

现在,咱们来点实际的,看看如何利用 Font Loading API 解决 FOUT 和 FOIT 问题。

策略一:先隐藏,后显示(FOIT 的变种,可控的 FOIT)

这种策略的核心是,在字体加载完成之前,先隐藏页面内容或者只显示占位符,等字体加载完成后再显示真实内容。

<!DOCTYPE html>
<html>
<head>
  <title>Font Loading API Example</title>
  <style>
    body {
      font-family: sans-serif; /* 默认字体 */
      opacity: 0; /* 初始隐藏 */
      transition: opacity 0.5s ease-in-out; /* 添加过渡效果 */
    }

    body.fonts-loaded {
      opacity: 1; /* 字体加载完成后显示 */
    }
  </style>
</head>
<body>
  <h1>Hello, World!</h1>
  <p>This is a paragraph of text.</p>

  <script>
    const myFont = new FontFace('MyCustomFont', 'url(./fonts/MyCustomFont.woff2)');

    myFont.load().then(function() {
      document.fonts.add(myFont);
      document.body.style.fontFamily = 'MyCustomFont, sans-serif';
      document.body.classList.add('fonts-loaded'); // 添加类名,显示内容
    }).catch(function(error) {
      console.error('字体加载失败:', error);
      document.body.classList.add('fonts-loaded'); // 即使加载失败也显示,防止页面一直空白
    });
  </script>
</body>
</html>

这个例子的关键点:

  1. body 初始设置 opacity: 0; 让页面内容初始隐藏。
  2. transition: opacity 0.5s ease-in-out; 添加过渡效果,让显示过程更平滑。
  3. body.fonts-loaded 类名用于控制 opacity 的值,当字体加载完成后,添加这个类名,让页面内容显示出来。
  4. 加载失败处理: 即使字体加载失败,也要添加 fonts-loaded 类名,防止页面一直空白。

优点: 简单粗暴,兼容性好。
缺点: 用户可能会看到一段时间的空白,体验稍差。

策略二:使用 font-display (CSS)

font-display 是一个 CSS 属性,用于控制字体在加载过程中的显示行为。它有几个可选值:

  • auto 浏览器自行决定,通常和 block 行为类似。
  • block 先隐藏文本,直到字体加载完成。 (FOIT)
  • swap 先显示默认字体,字体加载完成后切换为自定义字体。(FOUT)
  • fallback 先显示默认字体,一段时间后如果字体还没有加载完成,则继续显示默认字体。
  • optional 浏览器自行决定,如果字体加载速度慢,则不使用自定义字体。
@font-face {
  font-family: 'MyCustomFont';
  src: url('./fonts/MyCustomFont.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap; /* 或者 fallback */
}

body {
  font-family: 'MyCustomFont', sans-serif;
}

这个例子的关键点:

  1. font-display: swap; 告诉浏览器先显示默认字体,字体加载完成后再切换为自定义字体,可以避免 FOIT。
  2. font-display: fallback; 则更加灵活,先显示默认字体,一段时间后如果字体还没有加载完成,则继续显示默认字体,避免长时间的 FOUT。

优点: 简单易用,只需要修改 CSS。
缺点: 无法精确控制字体加载过程,可能会出现短暂的 FOUT。

策略三:结合 Font Loading API 和 CSS 类名

这种策略结合了 Font Loading API 的精确控制和 CSS 类名的灵活性,可以实现更优雅的字体加载效果。

<!DOCTYPE html>
<html>
<head>
  <title>Font Loading API Example</title>
  <style>
    body {
      font-family: sans-serif; /* 默认字体 */
    }

    body.fonts-loading {
      /* 可以添加加载中的样式,比如 loading 图标 */
    }

    body.fonts-loaded {
      font-family: 'MyCustomFont', sans-serif;
    }

    @font-face {
        font-family: 'MyCustomFont';
        src: url('./fonts/MyCustomFont.woff2') format('woff2');
        font-weight: normal;
        font-style: normal;
        font-display: swap;
    }
  </style>
</head>
<body>
  <h1>Hello, World!</h1>
  <p>This is a paragraph of text.</p>

  <script>
    document.body.classList.add('fonts-loading'); // 添加 loading 类名

    const myFont = new FontFace('MyCustomFont', 'url(./fonts/MyCustomFont.woff2)');

    myFont.load().then(function() {
      document.fonts.add(myFont);
      document.body.classList.remove('fonts-loading'); // 移除 loading 类名
      document.body.classList.add('fonts-loaded'); // 添加 loaded 类名
    }).catch(function(error) {
      console.error('字体加载失败:', error);
      document.body.classList.remove('fonts-loading'); // 移除 loading 类名
      document.body.classList.add('fonts-loaded'); // 即使加载失败也添加 loaded 类名,使用默认字体
    });
  </script>
</body>
</html>

这个例子的关键点:

  1. body.fonts-loading 类名用于在字体加载过程中添加加载中的样式,比如显示 loading 图标。
  2. body.fonts-loaded 类名用于在字体加载完成后应用自定义字体。
  3. font-display: swap;@font-face 规则中设置 font-display,可以进一步优化字体加载体验。

优点: 可以精确控制字体加载过程,并添加自定义的加载样式。
缺点: 代码稍微复杂一些。

第三部分:高级技巧和注意事项

  • 字体预加载: 使用 <link rel="preload"> 标签可以提前加载字体,提高加载速度。
<link rel="preload" href="./fonts/MyCustomFont.woff2" as="font" type="font/woff2" crossorigin>
  • 字体格式优化: 选择合适的字体格式,比如 WOFF2 格式,可以减小字体文件的大小,提高加载速度。
  • 字体子集化: 只包含页面中用到的字符,可以显著减小字体文件的大小。可以使用工具,比如 Font subsetter
  • 缓存: 合理配置 HTTP 缓存,可以避免重复加载字体文件。
  • 错误处理: 一定要处理字体加载失败的情况,避免页面出现错误。
  • 跨域问题: 如果字体文件和页面不在同一个域名下,需要配置 CORS,允许跨域访问。 在服务器端设置 Access-Control-Allow-Origin
  • document.fonts.ready 使用 document.fonts.ready Promise 来确保所有字体都已加载完成。这个 Promise 在所有字体加载完毕后 resolve。
document.fonts.ready.then(() => {
  console.log('所有字体加载完成!');
  document.body.classList.add('fonts-loaded');
});

表格总结:各种策略对比

策略 优点 缺点 适用场景
先隐藏,后显示 简单粗暴,兼容性好 用户可能会看到一段时间的空白,体验稍差 对体验要求不高,希望避免 FOUT 的情况
font-display (CSS) 简单易用,只需要修改 CSS 无法精确控制字体加载过程,可能会出现短暂的 FOUT 对体验有一定要求,允许短暂的 FOUT,但不希望出现 FOIT 的情况
结合 Font Loading API 和 CSS 类名 可以精确控制字体加载过程,并添加自定义的加载样式 代码稍微复杂一些 对体验要求高,希望精确控制字体加载过程,并添加自定义加载样式的情况

第四部分:常见问题解答

  • Q:FontFace 对象可以重复使用吗?
    A:不可以。每个 FontFace 对象只能使用一次。如果你需要在多个地方使用同一个字体,需要创建多个 FontFace 对象。

  • Q:document.fonts.add() 方法可以重复调用吗?
    A:可以,但是没有意义。重复调用只会添加一次字体。

  • Q:如何判断字体是否已经加载完成?
    A:可以使用 document.fonts.check() 方法或者监听 document.fonts.onloadingdone 事件。

  • Q:document.fonts.onloadingdonedocument.fonts.onloadingerrordocument.fonts.onloading 有什么区别?
    A:

    • onloadingdone:所有字体加载完成时触发。
    • onloadingerror:字体加载失败时触发。
    • onloading:字体开始加载时触发。

第五部分:结束语

好了,今天的分享就到这里。希望大家通过今天的学习,能够掌握 Font Loading API 的使用方法,告别 FOUT 和 FOIT 的烦恼,让你的网页字体加载又快又稳,颜值爆表!记住,字体虽小,细节决定成败。

最后,给大家留个小作业:尝试使用 Font Loading API 和 CSS 类名,实现一个带有加载动画的字体加载效果。期待你们的优秀作品!

散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注