我如何构建了一个并排字体比较工具(并意外地学到了太多关于 Browser APIs 的知识)
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,甚至在字体加载完之前隐藏文本。所有方法都感觉很卡顿。最后我还是保留了原状;闪烁时间很短,大多数用户不会注意到,但我每次都会看到。
我会做的不同之处
- 缓存权限状态。 目前每次点击 “System” 时模态框都会出现。将用户的选择存入
localStorage,以免反复打扰他们。 - 更好的错误处理。 有时 API 会悄悄失败。需要捕获这些情况并优雅地回退。
- Safari 的回退方案。 Safari 完全不支持此 API。与其使用静态的流行字体列表,我可以提供更健全的回退方案(例如,一套精选的网页安全字体或可下载的字体预览包)。
代码(如果你想要)
整个项目在 FontPreview 上,如果你想看到实际效果。
查看源代码 — 它全部是客户端的,没有后端,没有跟踪,仅仅是 HTML、CSS 和 JavaScript。
我不是一个很棒的开发者。我大部分是通过弄坏东西并在 Google 上搜索错误来学会的。但它能工作,而且有人在使用,这对我来说已经足够。
如果你正在使用 Local Font Access API 构建某些东西,联系我。我可能已经遇到了你正在遇到的同样的 bug。
标签: javascript, webdev, tutorial, showdev, api