我如何构建了一个并排字体比较工具(并意外地学到了太多关于 Browser APIs 的知识)

发布: (2026年2月19日 GMT+8 17:24)
7 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的完整文本内容(除代码块、URL 和标题之外的正文),我将为您翻译成简体中文并保持原有的 Markdown 格式。

想法

我创建了 FontPreview 是因为我厌倦了只能猜测字体在实际文本中的显示效果。设计师们经常问的一个问题是:

“我能把这个 Google Font 和我电脑上已有的字体进行对比吗?”

结果发现,这比想象的要困难得多。

浏览器并不真的想让你随意访问用户系统中的字体。这是有原因的——想象一下,每个你访问的网站都能获取你电脑上安装的所有字体列表。这将是一次隐私噩梦。

不过,现在有一个更新的 API 可以实现这一点,只要用户同意即可。

本地字体访问 API

有一个叫做 Local Font Access API 的功能。它相对较新,并不是所有浏览器都已经支持(比如 Safari)。在 Chrome 和 Edge 中,你可以这样使用:

const fonts = await window.queryLocalFonts();

这行代码会返回一个数组,包含用户系统中安装的所有字体。

Important: 浏览器会先请求用户授权。会弹出一个提示框,显示 “This site wants to see your fonts.” 如果用户选择 No,则什么也得不到。这是好的——我们不希望网站在未获授权的情况下随意抓取字体列表。

权限舞蹈

下面是我处理权限请求的方式:

function checkSystemFontsPermission() {
  if (!window.queryLocalFonts) {
    showToast('Local Font Access API not supported in this browser');
    useFallbackFonts();
    return;
  }

  // Show the permission modal
  document.getElementById('permissionModal').style.display = 'flex';
}
  • 如果浏览器不支持该 API,我会回退到一份流行系统字体的列表。虽然不完美,但总比没有好。
  • 如果浏览器支持,我会显示一个模态框,解释我为何请求以及我会如何使用这些数据(剧透:什么也不做。我只是把字体显示在下拉列表中)。

当他们说“是”时会发生什么

当用户点击 Allow 时,会运行以下代码:

async function requestFontPermission() {
  try {
    const fonts = await window.queryLocalFonts();

    // Clean up font names (remove "Regular", "Bold", etc.)
    const fontMap = new Map();

    fonts.forEach(f => {
      let name = f.family;
      const suffixes = [' Regular', ' Bold', ' Italic', ' Light', ' Medium'];

      suffixes.forEach(suffix => {
        if (name.endsWith(suffix)) {
          name = name.substring(0, name.length - suffix.length);
        }
      });

      if (!fontMap.has(name)) {
        fontMap.set(name, {
          family: name,
          fullName: f.family,
          style: f.style,
          weight: f.weight
        });
      }
    });

    // Sort and store
    allSystemFonts = Array.from(fontMap.values()).sort((a, b) =>
      a.family.localeCompare(b.family)
    );

    showToast(`Loaded ${allSystemFonts.length} system fonts`);
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      showToast('Permission denied. Using fallback fonts.');
    } else {
      showToast('Error loading fonts. Using fallback.');
    }
    useFallbackFonts();
  }
}

Map 很重要,因为许多字体会返回多个不同样式的条目——例如 “Arial Regular”、 “Arial Bold”、 “Arial Italic”。我只想要一次 “Arial”,所以 Map 会对它们进行去重。

我没预料到的大问题

一旦我拿到了字体列表,我就需要在预览中实际使用这些字体。

在 CSS 中,你可以直接写:

font-family: 'Arial', sans-serif;

如果用户已经安装了 Arial,就能正常显示。很好。

但我还想让用户能够并排比较系统字体和 Google Fonts。如果有人在左侧选择 Arial,在右侧选择 Roboto,我就需要从 Google Fonts 加载 Roboto。这就意味着要动态注入一个 <link> 标签:

function loadGoogleFontForPanel(fontName, panel) {
  const fontFamily = fontName.replace(/ /g, '+');

  const linkId = `panel-font-${panel}`;
  const oldLink = document.getElementById(linkId);
  if (oldLink) oldLink.remove();

  const link = document.createElement('link');
  link.id = linkId;
  link.rel = 'stylesheet';
  link.href = `https://fonts.googleapis.com/css2?family=${fontFamily}&display=swap`;

  document.head.appendChild(link);
}

每个面板可以独立加载自己的字体:左侧面板使用系统字体(不需要样式表),右侧面板加载 Google Font(需要样式表)。

仍然困扰我的部分

当你动态加载 Google 字体时,会有一个极短的延迟,直到字体可用。在此期间,文本会先以默认字体显示,然后切换为所选字体——出现未样式化文本的闪烁(FOIT)。

我尝试了许多修复方法:预加载、font-display: swap,甚至在字体加载完之前隐藏文本。所有方法都感觉很卡顿。最后我还是保留了原状;闪烁时间很短,大多数用户不会注意到,但我每次都会看到。

我会做的不同之处

  1. 缓存权限状态。 目前每次点击 “System” 时模态框都会出现。将用户的选择存入 localStorage,以免反复打扰他们。
  2. 更好的错误处理。 有时 API 会悄悄失败。需要捕获这些情况并优雅地回退。
  3. Safari 的回退方案。 Safari 完全不支持此 API。与其使用静态的流行字体列表,我可以提供更健全的回退方案(例如,一套精选的网页安全字体或可下载的字体预览包)。

代码(如果你想要)

整个项目在 FontPreview 上,如果你想看到实际效果。
查看源代码 — 它全部是客户端的,没有后端,没有跟踪,仅仅是 HTML、CSS 和 JavaScript。

我不是一个很棒的开发者。我大部分是通过弄坏东西并在 Google 上搜索错误来学会的。但它能工作,而且有人在使用,这对我来说已经足够。

如果你正在使用 Local Font Access API 构建某些东西,联系我。我可能已经遇到了你正在遇到的同样的 bug。

标签: javascript, webdev, tutorial, showdev, api

0 浏览
Back to Blog

相关文章

阅读更多 »