做前端或者设计相关工作的朋友应该都有过这种经历看到一个网站排版特别舒服想搞清楚它用的什么字体结果打开控制台翻半天定位元素、找样式被一堆font-family回退列表搞得头大。尤其是遇到那种用了font-face自定义字体的站点光看 CSS 根本分不清实际渲染出来的是哪个。我最早是在 Chrome 商店里用 WhatFont 这类插件但用着总有些别扭要么识别不准要么 UI 太老要么更新停摆。后来干脆花了一个周末自己写了个浏览器插件起名 Font Picker专门干网页字体识别这件事把鼠标悬停识别、字体属性复制、快捷排障这几个核心功能都做了进去项目一直迭代到现在成了我日常工作台里使用频率最高的工具之一。这篇文章我会把 Font Picker 从需求分析、技术选型到核心实现、常见坑位完整梳理一遍。如果你正打算自己写一个浏览器插件或者对“网页字体识别”这种需要读取页面实时渲染样式的能力感兴趣这篇内容应该能给到不少可以直接落地的参考。1. 想做网页字体识别先摸清需求与实现路径1.1 网页字体识别到底识别什么很多人以为字体识别就是查一下font-family实际上真做起来要拿的数据比这细得多。一个元素最终呈现出来的字体效果是字体族、字重、字号、行高、字距、颜色、斜体、修饰线等一堆属性叠加的结果。尤其是font-family本身是候选列表浏览器会按顺序逐个匹配直到找到系统里存在的那一个所以你从控制台看到的font-family: Helvetica Neue, Arial, PingFang SC, sans-serif并不能直接告诉你用户最终看到的是什么。Font Picker 做识别时会同时采集三类信息。第一类是字体族也就是用户感知最强烈的“这是什么字体”这里需要处理候选列表和实际渲染字体的差异第二类是排版数值包括字号、行高、字重、字距、字母间距这些排版相关参数第三类是色彩信息即文字颜色和背景色这在排查“为什么这里看不清”时特别有用。只显示一个字体名的那种工具当然也有价值但既然是自己写我一开始就把数据维度定义得比较全。1.2 现有方案对比为什么选择自己写市面上同类工具我基本都用过一轮。WhatFont 是老牌选择交互是鼠标悬停高亮然后点击弹提示但它的字体信息面板比较简略没有提供一键复制样式的能力而且多年没怎么更新在 Chrome 新版本上偶尔会抽风。Fonts Ninja 的功能更丰富能直接预览字体并跳转到购买页面但它的定位偏向字体商店导购对只想快速排查页面问题的开发者和设计师来说多了不少干扰信息。还有一款叫 Font Finder 的功能倒是直接但数据呈现要跳到弹窗页面里看不能像取色器一样就地浮层展示。对比下来我给自己定的需求很明确悬浮即识别不需要点击展示面板要浮在目标元素附近少遮挡字体家族信息要拆成实际渲染字体和原始声明两层所有关键样式支持一键复制开发部署轻量没有后端依赖。这个定位介于 WhatFont 和 Fonts Ninja 之间更偏向“开发排查工具”而不是“设计灵感收集器”。2. 技术选型与工程初始化2.1 Manifest V3 下的插件工程结构Chrome 从 2020 年开始逐步推广 Manifest V3现在新提交的插件基本都要求使用 V3 规范。V3 最核心的变化是 background 脚本从常驻后台改成了 service worker生命周期变成了事件驱动同时在跨域请求、内容脚本执行策略上有更严格的限制。对字体识别这个场景来说其实没有复杂的后台逻辑所以从第一天起我就直接按 V3 设计免去后面迁移的麻烦。工程结构用最简单清晰的方式组织font-picker/ ├── manifest.json # 插件描述文件 ├── content.js # 注入页面的核心识别脚本 ├── content.css # 浮层与探针的样式 ├── popup.html # 插件图标弹出面板 ├── popup.js # 弹出面板逻辑 ├── background.js # 后台 service worker保持空状态 └── icons/ # 插件图标 ├── icon16.png ├── icon48.png └── icon128.png这里涉及一个关键设计插件识别操作全程都在content script里完成不需要 background 参与也不需要页面授权之外的任何权限。既然没有远程代码需求manifest.json只需要声明content_scripts即可。这样做的好处是权限最小化插件上架审核时会少很多麻烦同时在 Chrome 和 Edge 上都能直接加载使用。manifest.json的核心配置如下{ manifest_version: 3, name: Font Picker - 网页字体识别工具, version: 2.1.0, description: 悬停识别网页字体展示字体族、字号、行高、字重、颜色等完整排版信息。, permissions: [activeTab], action: { default_popup: popup.html, default_title: Font Picker, default_icon: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png } }, content_scripts: [ { matches: [all_urls], js: [content.js], css: [content.css], run_at: document_idle } ], icons: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png } }两个细节值得注意一个是run_at用了document_idle确保脚本在 DOM 解析完成后注入避免 DOM 还没渲染完导致探针挂载失败另一个是permissions里只申请了activeTab实际它只在处理 popup 里的“查看当前页字体列表”功能时才需要纯悬停识别甚至不需要任何权限。2.2 工具链选型原生 JS 还是框架写内容脚本和写常规前端页面有个本质区别content script 是注入到第三方页面环境里执行的它不能影响页面本身的逻辑也不应该被页面的全局变量干扰。因此隔离性是第一优先级。Vue、React 这类框架不是不能用但它们会引入较大的运行时且脱离组件体系后价值有限。对于识别浮层这种轻量 UI原生 DOM 操作加少量状态管理完全够用。我最后选择了原生 JavaScript 少量 CSS 变量的方案整个 content.js 打包前只有不到 300 行加载和执行速度都比引框架快一个量级。当然如果你后续想在这个插件基础上加复杂的配置面板或数据统计可视化引入框架是合理的。我的建议是按功能复杂度动态判断不要一开始就上一个全家桶。“能用原生解决的就不堆依赖”这条原则在插件这类追求轻量的场景里尤其适用。3. 核心插件逻辑从页面探针到字体信息采集3.1 探针模式让鼠标帮你选中元素Font Picker 最核心的交互是“悬浮即识别”这与“点击后识别”的工具体验差异巨大。为了在鼠标扫过页面时快速定位目标元素我在 content script 里实现了一个探针机制。探针的核心是一个会跟随鼠标移动的半透明高亮框同时监听mousemove事件实时判断当前命中的元素。关键是把命中逻辑和浮层展示解耦防止鼠标一移动就频繁触发 UI 重建let highlightedElement null; let highlightOverlay null; let debounceTimer null; document.addEventListener(mousemove, (e) { if (!pickerEnabled) return; clearTimeout(debounceTimer); debounceTimer setTimeout(() { const target document.elementFromPoint(e.clientX, e.clientY); if (target target ! highlightedElement) { highlightedElement target; moveHighlightOverlay(target); } showInfoTooltip(e.clientX, e.clientY); }, 40); });40 毫秒的防抖间隔是我调出来的平衡点。太短会导致高亮框闪烁因为鼠标在元素边缘抖动时频繁切换目标太长则会让浮层有明显的粘滞感。实测 40ms 在大多数机器上都能保持 25fps 的刷新率交互已经足够顺滑。高亮框用的是一个独立的position: fixed的 divpointer-events: none确保它不会拦截鼠标事件。这里有个容易踩的坑如果不加pointer-events: none高亮框自身会成为elementFromPoint的命中对象然后循环触发自身高亮直接死循环。3.2 字体信息采集的完整链路鼠标定位到目标元素后接下来就是采集字体信息。这里最核心的 API 是window.getComputedStyle()它能拿到元素渲染后的最终样式值。注意它是“最终样式”意味着它会合并所有 CSS 规则、继承关系和浏览器默认样式。采集逻辑如下function getFontInfo(element) { const styles window.getComputedStyle(element); const fontFamilyRaw styles.fontFamily; const fontSize styles.fontSize; const lineHeight styles.lineHeight; const fontWeight styles.fontWeight; const color styles.color; const backgroundColor styles.backgroundColor; const letterSpacing styles.letterSpacing; const info { fontFamilyRaw, fontSize, lineHeight, fontWeight, color, backgroundColor, letterSpacing }; const actualFont detectRenderedFont(fontFamilyRaw); if (actualFont) { info.fontFamilyActual actualFont; } return info; }lineHeight有一个关键细节当 CSS 里写的是纯数字比如line-height: 1.5时getComputedStyle返回的是计算后的像素值而不是原始数字。这会导致复制出来的样式跟源 CSS 不一致。我后来加了一个辅助判断如果行高值是 px 结尾且元素字号已知就反推出无单位倍数并额外展示这样既能看懂实际渲染也能方便地复制原始意图。3.3 字体回退与“真实渲染字体”的探测前面提到了font-family候选列表的问题这导致你在控制台看到一堆字体名却不能确定浏览器实际用了哪个。Font Picker 采用了一个技巧性方案来解决动态插入隐藏探测元素用字体度量差异来判断浏览器匹配到了哪个字体。基本原理是用document.fonts.check()这个 API。它接受一个字体描述字符串和一段文本返回该字体在当前环境中是否可用function detectRenderedFont(fontFamilyRaw) { const candidates parseFontFamilyList(fontFamilyRaw); const fontSize 16px Arial; // 基准描述 const testText abcdefghijklmnopqrstuvwxyz0123456789; for (const fontName of candidates) { const cleanedName fontName.trim().replace(/^[]|[]$/g, ); const fontDesc 16px ${cleanedName}; if (document.fonts.check(fontDesc, testText)) { return cleanedName; } } return sans-serif系统默认; }document.fonts.check()是 CSS Font Loading API 的一部分它返回布尔值指示当前文档中指定字体是否已经加载完成且可用于渲染。需要注意它只对已经加载或系统自带的字体返回true那些声明了但还没加载完的font-face字体可能会误判。所以稳妥的做法是先遍历候选列表如果第一个能 check 通过就确认它是实际渲染字体如果命中字体系列但属于font-face动态加载则需要配合document.fonts.ready来等待加载完成后再做判断。实际使用中这个 API 的准确率能达到九成以上剩下的边界情况我会在常见问题部分展开。4. 交互体验设计探针、快捷键与悬浮卡片4.1 悬浮卡片的布局与防遮挡策略字体信息展示如果用浏览器原生的弹窗必然会把用户视线从目标元素带偏。Font Picker 的解决方案是做一个跟随鼠标的定位浮层样式上借鉴了取色器的交互模式。浮层本身是position: fixed定位到鼠标右下方的偏移位置同时要防止它在靠近视口边缘时被截断。实现防截断的逻辑也不复杂。鼠标当前位置加上浮层宽高后如果超出视口就往左或往上反向偏移function positionTooltip(x, y) { const tooltipWidth tooltip.offsetWidth; const tooltipHeight tooltip.offsetHeight; const padding 16; let left x padding; let top y padding; if (left tooltipWidth window.innerWidth) { left x - tooltipWidth - padding; } if (top tooltipHeight window.innerHeight) { top y - tooltipHeight - padding; } tooltip.style.left ${left}px; tooltip.style.top ${top}px; }这个浮层默认展示摘要信息包括字体名和字号。如果把鼠标停住不动超过 300 毫秒就展开详细面板展示完整的排版属性。这样设计是为了避免鼠标扫过页面时浮层乱跳干扰浏览。还有个小细节是浮层的层级问题。有些页面会用到非常高z-index的弹层如果浮层被挡住体验会很差。我最初把浮层z-index设成 999999后来在某个网站还是被盖住了排查发现那站点的弹窗用了z-index: 2147483647。最后我改成动态设置在显示浮层时把它 push 到页面的顶层元素附近而不是单纯依赖 z-index。具体做法是检查 document 下所有元素里的最大 z-index然后在此基础上加 1。4.2 快捷键与一键复制再好的浮层如果每次看字体信息都要移动鼠标到工具栏点开关也还是不够高效。Font Picker 加了键盘快捷键默认的快捷键是AltShiftF按下后开启或关闭拾取模式。开启模式下鼠标移动自动高亮并显示摘要此时还可以按住Alt临时冻结识别目标方便细细查看密集区域的字体。数据复制这块做了一整套快捷操作在浮层上点击任意一行属性就能复制该属性的键值对点击浮层底部复制按钮则把所有信息格式化成标准 CSS 代码。一次复制出来的内容可以直接粘到代码评审的评论区里格式大概是font-family: Inter, -apple-system, sans-serif; font-size: 14px; line-height: 1.6; font-weight: 500; letter-spacing: 0.2px; color: #1a1a1a; background-color: #ffffff;复制功能通过navigator.clipboard.writeText实现但这里有个兼容性坑content script 里直接调 clipboard API 在某些页面上会失败原因是非用户手势触发的写操作不安全。解决方式是在点击事件处理函数里同步执行写操作不要异步延迟这样浏览器会认为这是用户主动触发的安全操作。如果你在实现时发现复制偶尔失效优先检查是不是事件回调里加了防抖延迟。5. 开发、调试与发布从本地加载到商店上架5.1 本地加载插件三分钟跑通核心功能开发过程中最频繁的操作就是“加载解压的扩展程序”。打开 Chrome 的扩展管理页面地址栏输入chrome://extensions开启右上角的开发者模式点击“加载已解压的扩展程序”选中项目根目录即可。修改代码后只需点击扩展卡片上的刷新图标就能重新注入不用反复卸载重装。但这里有一个比较耽误时间的点content script 的改动刷新扩展后需要刷新目标页面才能生效。因为 content script 是在页面加载时注入的扩展重载并不会重新执行已注入页面的脚本。我推荐安装一个扩展开发辅助工具每次保存代码后自动刷新插件和当前标签页能把开发反馈循环从几十秒压缩到两三秒。如果同时要调试多个浏览器Edge 的扩展管理页面地址是edge://extensions同样开启开发人员模式后就能加载同一个目录。这意味着插件可以一套代码横跨 Chrome 和 Edge 发布不需要工程层面做任何改动。5.2 跨浏览器兼容与 Manifest 版本的坑Firefox 的插件规范是 AMOAdd-ons for Firefox它同时支持 V2 和 V3但对 V3 的支持有一些细节差异。比如在 Chrome 里很常见的chrome.scripting.executeScript在 Firefox 里就要求额外声明scripting权限和host_permissions。如果你的插件只想覆盖 Chrome 和 EdgeManifest V3 是稳妥的想覆盖 Firefox就需要增加 Firefox 的专门适配文件或者退回到 V2 兼容写法。从我实际使用的角度来说绝大多数用户跑在 Chrome 系内核上优先把这块打磨扎实才是正路。有句话想送给第一次写插件的人尽早清理浏览器里的无关插件。我在调试 Font Picker 期间遇到过几次浮层显示异常排查半天发现是另一个广告拦截插件把某些 DOM 节点给隐藏了导致页面布局跟预期完全不搭边。干净的环境能排除至少一半的干扰项。5.3 发布到 Chrome 应用商店前的准备清单如果你只是自己用本地加载就够了。要想发布到 Chrome 应用商店开发者账号需要一次性支付注册费然后准备好以下材料插件介绍文案和截图截图建议至少 1280x800展示插件在实际页面中的运行效果。隐私政策页面链接。虽然 Font Picker 不收集任何数据商店也要求提交隐私政策说明。明确的权限用途说明。因为只申请activeTab审核时理由天然简短清晰。提交后商店会走一次自动审核加一次人工抽检一般情况下两三天内能出结果。慢的时候也遇到过三四天的多留一点余量别掐着 Deadline 提交就好。整个发布流程给我的最大感受是权限越小审核越顺畅。这一点在立项之初就要想清楚临时补救很折腾。6. 常见问题与排障速查表开发和使用 Font Picker 的过程中我整理了这些出现频率最高的问题方便直接照表排查。症状可能原因解决思路插件在页面上完全没反应content script 未注入或被浏览器企业策略禁用检查扩展详情页权限换普通页面测试刷新目标页面高亮框跟随鼠标时闪烁pointer-events未禁用给高亮浮层加上pointer-events: none;字体名跟设计稿不符字体回退列表命中系统默认字体查看真实渲染字体字段对比是否缺少字体文件浮层被页面元素遮挡页面使用了极端z-index动态检测页面最高层级在其基础上加 1复制字体无效clipboard API 非用户手势触发确保复制逻辑在 click 事件回调内同步执行在 iframe 页面无法识别content script 默认不注入 iframe需要在配置中启all_frames: true识别到的字号与 CSS 不一致页面使用了transform: scale参考元素盒模型与 transform 变换计算实际尺寸动态渲染的 SPA 页面识别不上mousemove监听时元素已销毁用防抖 elementFromPoint实时获取新元素6.1 最常踩的 iframe 识别坑iframe 是字体识别工具的一个重灾区。很多网页的主体内容嵌在 iframe 里比如文档预览、富文本编辑器、第三方表单。Chrome 的 content script 默认不注入 iframe 内部即使所有 URL 都匹配了也只在顶层文档执行。解决办法是在manifest.json中给 content script 声明all_frames: true。开了all_frames之后又会引入一个新问题iframe 内的脚本和外层脚本同时存在鼠标可能在两者边界移动时出现重复高亮。解决方式是给每个脚本上下文加一个唯一标识通过window.top window.self判断当前是否在顶层页面再配合跨 iframe 的消息通信来协调高亮状态。跨域 iframe 的场景下没法直接访问彼此 DOM只能通过postMessage做行为协调这个设计需要提前考虑到。6.2 动态渲染与 SPA 页面的应对策略现代网页大量采用前端框架动态渲染DOM 元素会在交互后反复重建。如果脚本只是启动时绑定一次事件监听就会漏掉后续生成的元素。上面代码里的mousemove监听是绑在document上的它天然覆盖所有动态生成的子元素所以 SPA 场景基本不受影响。真正的坑在收集信息那一步。页面可能在你展示浮层的同时由框架重新渲染并替换了目标元素。此时再读取getComputedStyle拿到的可能是新元素的信息或者更糟拿到一个空对象。稳妥的做法是在展示详细面板前校验当前元素是否还连接在文档里用element.isConnected判断。如果为false直接隐藏浮层等下一次鼠标事件重新定位。另外悬停状态下点击浮层和页面元素的位置要处理好。如果用户点击的是一个存在点击事件的元素页面可能会弹出模态框或者跳转导致识别中断。我的做法是给浮层加一个 8ms 的延迟关闭策略如果鼠标从页面元素快速移到浮层上识别目标会被保留避免来回闪断。7. 字体数据如何展示才能提升使用效率7.1 信息分组与视觉层级设计浮层里的字体信息如果平铺展示用户需要花时间找重点。Font Picker 将信息分成三个视觉组第一组是字体族用大字号和醒目的字重展示名称为主回退列表缩进显示第二组是排版参数用等宽字体垂直排列方便粘贴到代码里第三组是颜色提供色块预览点击还能切换 HEX、RGB 和 HSL 三种格式。这个小细节比想象中重要。大多数前端拿到字体信息后都会粘到代码或设计稿里不同场景要求的颜色格式不一样。设计稿里常用 HEX代码评审时 RGB 更直观做动态主题时 HSL 更方便。能一键切换省掉的复制转换时间虽然小但累计起来很可感。7.2 相似字体检测与“常规化”处理还有一个值得一提的细节网页里经常出现同一字体不同的别名。比如PingFang SC和-apple-system在 Windows 上实际渲染的是Microsoft YaHei在 macOS 上是PingFang SC。Font Picker 在信息面板里提供了一个“常规化”按钮会把常见的系统字体别名映射成统一名称。这样在团队协作时不管成员用什么系统讨论同一个网站时指代的字体名是一致的。这个功能相当于给字体识别工具加了一层“翻译层”。它并不改变页面渲染只调整插件展示的字体名。我最初觉得这个功能可有可无直到有一次用 Windows 的同事和用 macOS 的同事对着同一个页面争论字体才发现别名统一是刚需。8. 发布后的数据反馈与版本迭代方向8.1 关注哪些指标如何分析使用情况To B 或 To C 产品的指标和浏览器插件有本质区别。浏览器插件的核心指标不是注册量和留存而是“活跃安装量”和“卸载率”。Chrome 应用商店后台会提供完整的安装、卸载趋势数据你可以把它理解成一种「轻量级用户流失分析」。在实际迭代 Font Picker 的过程中我注意到一个反常现象每次版本更新后的三天内卸载率会有小幅上升。后来在评论区发现原因有人反馈“更新后快捷键被重置”“浮层样式变了不适应”。这个现象启发我插件的功能迭代要非常克制尤其是改动快捷键绑定和交互布局这类高频操作点的时候最好在版本更新说明里醒目标出变更必要时候还要提供一键切换回旧布局的选项。8.2 已经规划的方向字体收藏夹与页面字体总览目前 Font Picker 还在迭代中。我计划下一个版本引入两个比较实用的扩展功能。第一个是“页面字体总览”。很多开发者拿到一个页面会很想知道它总共用了几种字重、哪几种字体而不仅仅依赖鼠标去悬停猜测。这个功能需要遍历 DOM 并聚合所有元素的字体信息性能开销不小我打算用requestIdleCallback分段执行避免阻塞主线程。毕竟插件的底线是不能影响宿主页面本身的性能。第二个是“字体收藏夹”。把识别到的字体一键保存到本地支持打标签和备注方便在做设计走查时积累一份“可复用字体库”。存储就走chrome.storage.local单机保存没有同步压力量大之后再考虑升级为账号同步。这两个方向都不复杂但要保证体验的流畅度平滑聚合和存储去重是核心难点。如果之后做完了我会再来写一篇插件功能迭代记录看看这两个功能在实际场景中究竟能帮到多少忙。最后再分享一点做插件的心得从需求萌生到 Font Picker 跑到 2.1.0 版本这个项目给我最大的收获不是技术实现本身而是“做工具的人要和工具的受众保持同一种使用姿势”。如果你打算写插件不管是什么用途都建议给自己留一段“禁用所有外部插件只用自己写的工具”的时间。这个过程里你会像普通用户一样遭遇各种反直觉的交互问题然后立刻意识到应该改哪里。很多我上面提到的小细节比如浮层防遮挡、防抖间隔、快捷键说明都是在那段“自我折磨”的过程中磨出来的。造工具给别人的第一步是先当它最挑剔的用户。