所见即所查:让 PV/UV 长在业务页面上
所见即所查:让 PV/UV 长在业务页面上
这篇文章整理自一次浏览器可视化分析扩展的建设过程。为了方便公开分享,文中的业务域名、数据平台名称、事件字段、接口路径、权限申请方式和组织信息都做了脱敏。下面主要聊通用问题、产品思路和工程取舍,不涉及真实业务数据或内部配置。

先说说为什么做
很多团队都会遇到一个相似的问题:数据已经被采集了,分析平台也很强大,但普通业务同学、产品同学,甚至一部分研发同学,依然很难在日常工作中快速回答几个朴素的问题:
- 这个页面最近访问量怎么样?
- 页面上这个按钮有没有人点?
- 当前页面有哪些埋点,数据是否正常?
- 多语言文案在页面里的实际呈现位置在哪里?
- 需要截图留档或协作时,能不能少一点手工操作?
如果每次都要打开数据平台、选择项目、拼条件、找事件、填 URL、复制 selector,再把结果和页面元素对上,整个过程就会变得很重。工具不是没有,只是离实际工作现场有一点远。
我们做这个浏览器扩展,最初的目标并不宏大:让大家在当前页面里就能更快地看到关键数据。随着使用场景增加,它逐渐变成了一个面向页面分析、可视化埋点分析、国际化辅助和截图导出的综合效率工具。
这篇文章想复盘的是:一个看起来不复杂的浏览器扩展,怎样把数据平台能力、页面上下文和用户操作串起来;以及在这个过程中,我们做了哪些技术取舍,踩过哪些不算高级但很真实的坑。
这个工具想解决什么
这个工具的核心定位可以概括为一句话:
把原本需要在数据平台里手动查询、对照和整理的工作,前移到用户正在访问的业务页面中完成。
它面向的不是专业数据分析师,而是更广泛的技术和互联网从业者,包括产品、研发、运营、翻译、测试、BI 等角色。因此产品设计上没有追求“覆盖所有分析能力”,而是优先解决高频、明确、低门槛的问题。
目前可以抽象成四类能力:
- 页面访问分析:基于当前页面路径,快速查看 PV、UV 和趋势。
- 可视化埋点分析:把页面元素和点击数据直接关联,在页面上高亮展示。
- 单元素下钻:点击被标记的页面元素后,查看该元素在指定时间范围内的趋势。
- I18N 与截图辅助:在页面中定位多语言 key,并批量生成带截图的协作材料。
为什么从浏览器扩展开始
这个需求天然发生在浏览器里。用户一边浏览业务页面,一边想知道页面背后的数据情况。如果让用户离开页面去另一个系统查数,就会出现几个问题:
- 页面 URL 需要手动复制和转换。
- 动态路由、ID 参数等需要人为归一化。
- 数据平台里的 selector、文案、路径不一定能和真实页面一眼对应。
- 查完数据还要回到页面做判断,认知切换比较频繁。
浏览器扩展的优势在于,它可以同时拿到三类上下文:
- 当前页面上下文:URL、DOM、页面可见状态、元素内容。
- 扩展 UI 上下文:侧边栏、表单、图表、用户配置。
- 后台服务上下文:统一处理跨域请求、权限、存储和消息转发。
这三类上下文刚好构成了工具的基本闭环。
为什么用了 Plasmo
Manifest V3 的浏览器扩展开发,常见路径大概有几种:
| 方案 | 优点 | 成本 |
|---|---|---|
| 手写 Manifest + 构建配置 | 灵活,可控 | 配置、热更新、消息通信都要自己维护 |
| 基于 Vite 的轻量方案 | 构建体验好 | 扩展运行时基础设施仍需补齐 |
| Plasmo | React/TypeScript、文件路由、消息通信、Storage 等能力开箱可用 | 框架会接管一部分约定 |
我们最后选择 Plasmo,主要是因为这个项目的目标不是研究扩展构建体系,而是尽快把业务现场和数据能力串起来。Plasmo 把很多 MV3 中繁琐但重复的事情封装掉了,例如:
- Content Script 的注入入口。
- Background 消息处理器的注册。
- Side Panel 页面组织。
- Storage 的基础封装。
- 与 React/TypeScript 的集成。
这个选择并不意味着 Plasmo 适合所有扩展。如果扩展非常底层、运行时非常特殊,手写 Manifest 可能更可控。但对这种工具型、UI 较重、需要快速迭代的内部扩展来说,约定式框架带来的收益比较明显。
整体是怎么串起来的
整体架构采用浏览器扩展中比较常见的分层方式:
- Side Panel:承载主要操作界面,包括页面分析、埋点分析、I18N 工具等。
- Content Script:注入到目标页面,负责 DOM 扫描、元素标记、浮层提示、截图等能力。
- Background Service Worker:负责消息中转、接口请求、存储初始化、扩展生命周期处理。
- Storage:保存用户配置、临时匿名标识、扩展版本等信息。
- 数据分析平台 API:提供事件查询、聚合统计、趋势数据等能力。
这种职责切分不是为了显得架构漂亮,而是浏览器扩展运行时决定的:Content Script 能接触页面 DOM,但不适合直接承担复杂跨域请求;Background 能处理权限和请求,但没有页面 DOM;Side Panel 适合承载重 UI,但它也不能直接操作宿主页面。
换句话说,扩展里的很多复杂度,来自“看起来是一个工具,实际上跑在多个运行时里”。
先回答:当前页面怎么样
页面分析的用户路径很直接:
- 用户打开一个业务页面。
- 扩展识别当前页面是否属于支持范围。
- 用户在侧边栏选择时间范围。
- 工具读取当前 Tab 的 URL 和 path。
- 将动态路径做规则化处理。
- 生成查询参数,请求数据平台。
- 返回 PV、UV 和趋势图。
这里比较关键的一点是“路径规则化”。很多业务系统的 URL 中会包含订单 ID、用户 ID、任务 ID 等动态片段。如果直接用完整路径查询,数据会被拆得很碎,也不利于复用分析条件。
因此工具会把部分动态路径转换成可匹配的规则表达式。例如:
/example/detail/123456 -> /example/detail/*上面只是示意,实际规则会根据业务路由特点配置,并且在公开文章中不展开具体模式。这个处理看似很小,但对查询体验影响很大。用户不需要理解数据平台的 URL 匹配语法,也不需要每次手动调整条件。
再回答:页面元素有没有被点击
可视化埋点分析是这个工具里最有“所见即所得”感受的一部分。
它的基本思路是:先基于当前页面和时间范围查询元素点击数据,再把数据中的元素 selector、元素路径、元素文案等信息映射回页面 DOM,最后在页面上做高亮和提示。
简化后的流程如下:
这一块的难点主要有三个。
第一,selector 不一定稳定。业务页面会演进,DOM 结构也会变化。工具只能尽量利用数据采集时留下的 selector、元素路径和文案做交叉校验,避免把数据标到错误元素上。
第二,页面状态不一定稳定。页面可能是异步渲染的,也可能切换 Tab 后重新激活。工具需要在页面可见性变化时适当重放标记逻辑,减少“刚查完数据,切回来标记没了”的情况。
第三,信息密度要克制。页面上可能有很多可点击元素,如果全部展示复杂信息,会干扰用户正常看页面。因此我们选择把核心指标放在标记和 tooltip 里,更详细的趋势分析仍放在侧边栏中。
这个方案不一定完美,但它让“查埋点”从抽象表格变成了页面里的可见信息。对非数据岗位同学来说,这种变化非常重要。
继续往下看:单个元素的趋势
当用户在页面上看到某个元素有数据后,通常会继续追问:
- 这个按钮最近几天点击趋势是上升还是下降?
- 点击人数和点击次数是否一致?
- 是不是某一天发布后突然变化?
因此工具支持对单个元素继续下钻。用户点击页面上的高亮元素后,Content Script 会把该元素的 selector、路径、文案等信息传给侧边栏;侧边栏再基于当前时间范围生成更细的查询条件,拿到趋势图。
这里有一个实际体验上的取舍:我们没有把所有分析维度都堆进页面 tooltip,而是把 tooltip 做轻,把趋势图留在侧边栏。这样页面仍然是页面,工具仍然是工具,两者之间有联动,但不会互相抢空间。
顺手解决一些协作小麻烦
除了数据分析,这个扩展还承担了一部分国际化协作工作。
在多语言场景中,翻译、产品和研发经常需要确认某个 key 在页面哪里、文案替换后影响哪个区域,以及是否需要截图交付给上下游。传统方式通常是人工查 key、找页面、截图、整理表格,重复而且容易漏。
工具里的 I18N 辅助能力主要解决三件事:
- 在页面上进入可视化模式,定位多语言 key 对应的 DOM。
- 支持编辑/预览模式切换,方便在页面现场确认文案位置。
- 批量截图并导出表格,表格中包含截图、key、项目标识和页面地址等信息。
这里的实现同样体现了浏览器扩展的运行时分工:任务管理界面负责维护待截图列表和触发动作,Background 负责把指令转发给当前 Tab,真正的 DOM 截图和本地文件生成则在 Content Script 中完成,因为只有它能稳定拿到宿主页面的真实 DOM。
截图部分使用前端截图能力将目标元素或容器转换为图片,再用表格库生成可下载文件。这个实现不复杂,但对协作很实用。很多时候,效率工具的价值并不在于用了多高级的技术,而在于它减少了日常协作中的机械动作。
样式隔离:别打扰业务页面
Content Script 注入 UI 最大的风险是污染宿主页。比如组件库的 reset 样式可能影响业务页面,业务页面的全局样式也可能反过来影响扩展组件。
因此我们把页面内浮层、悬浮球、tooltip 等 UI 放进 Shadow DOM 中渲染,让扩展样式和宿主页面尽量隔离。
但这里还有一个小坑:一些 CSS-in-JS 方案默认把样式插入 document.head。如果组件实际渲染在 Shadow DOM 中,样式可能并不会生效。解决思路是显式把样式注入到 Shadow Root 对应的容器中。
示意代码如下:
const HOST_ID = "extension-shadow-host"
export const getShadowHostId = () => HOST_ID
function FloatingWindow() {
const shadowRoot = document.getElementById(HOST_ID)?.shadowRoot
return (
<StyleProvider container={shadowRoot}>
<ThemeProvider>
{/* 页面内扩展 UI */}
</ThemeProvider>
</StyleProvider>
)
}这段逻辑本身不复杂,但如果第一次遇到,排查成本并不低。问题通常表现为组件已经渲染出来了,但样式完全不对,容易误判成打包、作用域或组件库配置问题。
通信链路:让几个运行环境配合起来
MV3 里有多个运行时:Side Panel、Content Script、Background Service Worker。它们之间不能像普通 Web 应用那样直接共享内存或调用函数,只能通过消息通信协作。
我们采用“文件即消息处理器”的组织方式,把 Background 中的消息处理逻辑拆成多个独立文件。Side Panel 或 Content Script 发出明确的消息名,Background 处理后再把结果返回,必要时继续转发给目标 Tab 的 Content Script。
可视化埋点分析中的一次典型请求大致是这样:
async function handleCurrentPageAnalysis(req, res) {
const { tabId, queryParams } = req.body
sendToContentScript({
tabId,
name: "SHOW_LOADING",
body: { visible: true }
})
try {
const [elementData, customEventData, pageData] = await Promise.all([
queryElementEvents(queryParams.element),
queryCustomEvents(queryParams.custom),
queryPagePvUv(queryParams.page)
])
sendToContentScript({
tabId,
name: "MARK_ELEMENTS",
body: {
elements: normalizeElementData(elementData),
page: normalizePageData(pageData)
}
})
res.send({
success: true,
customEvents: normalizeCustomEventData(customEventData)
})
} catch (error) {
res.send({ success: false, error: String(error) })
} finally {
sendToContentScript({
tabId,
name: "SHOW_LOADING",
body: { visible: false }
})
}
}这里有几个经验比较实用:
- 能并行的查询尽量并行,减少用户等待。
- Loading 不只表示“请求中”,也能避免页面标记和用户操作之间产生突兀感。
- Background 中不要默认抛出不可控异常,跨上下文后的错误栈通常不够友好。
- 消息体要尽量结构化,避免让调用方依赖某个接口原始返回。
这类代码看起来像胶水,但胶水代码写得是否清楚,直接决定扩展后续好不好维护。
查询适配:和已有平台好好相处
这个扩展依赖一个内部数据分析平台的查询能力。平台本身能力很强,但它的查询模型并不是专门为浏览器扩展设计的。
我们的做法不是把平台查询参数直接散落到 UI 组件里,而是加了一层适配:
- 页面分析只暴露“页面 + 时间范围”。
- 埋点分析只暴露“当前页面元素 + 时间范围”。
- 自定义事件只暴露少量必要筛选项。
- 更复杂的字段、聚合、过滤条件在工具内部生成。
对外层用户来说,他点击的是“分析当前页面”;对内层实现来说,工具会转换成平台可识别的查询参数。
同时,我们把和分析平台耦合的逻辑集中在少数服务与工具函数中,包括:
- 查询参数构造。
- URL 路径规则化。
- 结果结构归一。
- 趋势图数据转换。
- 跳转到分析平台继续深挖的 URL 生成。
这个封装并不神奇,但很重要。因为只要分析平台字段或返回结构变化,我们希望影响范围尽量停留在适配层,而不是扩散到页面组件、图表组件和 Content Script。
页面标记:尽量准确,但不逞强
页面元素标记并不是一个百分百可靠的问题。selector 可能变化,文案可能重复,页面可能异步渲染,组件结构也可能随着迭代调整。
因此我们没有把“能找到一个 DOM”当成标记成功,而是做了更保守的判断:
- 优先用采集到的 selector 找元素。
- 结合元素内容做校验。
- 已经标记过的元素避免重复标记。
- 找不到或无法确认时跳过。
这类工具最怕“看起来很确定,但其实标错了”。宁可少标一点,也不要把错误信息以很自信的方式呈现给用户。
另外,在页面可见性变化、Tab 切换、页面重新激活等场景下,原先的标记状态可能丢失或失效。工具需要具备一定的重放能力,而不是只假设用户在一个稳定页面上完成完整流程。
工具自己也要能被观察
内部工具常常容易忽略自身的数据和异常。我们一开始只是想解决业务问题,但工具上线后也需要回答:
- 有多少人在用?
- 哪些功能最高频?
- 哪些入口几乎没人用?
- 失败主要发生在哪些环节?
- 用户是否因为权限或配置卡住?
因此扩展本身也做了基础行为上报。用户首次安装或初始化时生成一个临时匿名标识,后续操作事件会带上这个标识、扩展版本和必要的用户配置类型。这里同样需要注意边界:只统计工具使用情况,不采集真实业务页面数据。
异常监控也很重要。浏览器扩展的错误排查成本比普通 Web 页面更高,尤其是 Background Service Worker 生命周期短,很多问题不能靠用户截图复现。当前实践中,至少应覆盖主要 UI 页面和关键请求链路;如果后续要进一步完善,可以继续补齐 Background 和 Content Script 的异常聚合。
发版流程:越轻越能持续迭代
内部工具如果发版成本很高,很快就会停在“能用但不好用”的状态。我们在流程上尽量让发版动作标准化:
- 约定式提交,减少 changelog 整理成本。
- 自动生成版本号和变更记录。
- 构建生产包。
- 打包扩展产物。
- 通过自动化流程提交到浏览器扩展商店或内部发布渠道。
这些事情技术含量不高,但能明显降低维护心智负担。对内部效率工具来说,能不能持续迭代,往往比第一版做得多漂亮更重要。
一些取舍
1. 把复杂查询封装成用户能理解的动作
数据平台通常需要用户理解事件名、聚合方式、字段、过滤条件、时间粒度等概念。对专业分析师来说这些是基本能力,但对普通使用者来说门槛偏高。
所以工具没有把数据平台的表单完整搬进扩展,而是把高频场景封装成几个明确动作:
- 分析当前页面。
- 标记当前页面埋点。
- 查看单个元素趋势。
- 跳转到数据平台继续深挖。
也就是说,扩展承担的是“把上下文准备好”的工作,而不是替代数据平台本身。
2. Background 统一处理请求和消息
浏览器扩展里,Side Panel、Content Script、Background 的运行环境不同。如果每一层都各自请求接口、管理状态,后面很容易变乱。
因此我们把数据请求、跨上下文通信、生命周期初始化尽量收在 Background 中。Side Panel 只表达用户意图,Content Script 只负责页面呈现,Background 负责把两边连起来。
这个方式会让消息链路稍长一点,但长期看更容易维护。
3. 页面标记要“尽力而为”
DOM 匹配并不是一个百分百可靠的问题,尤其当页面存在动态渲染、组件重构、A/B 变化、同文案重复出现等情况时。
所以工具在标记元素时采用的是尽力而为的策略:selector 能匹配时优先匹配,同时结合元素内容做校验;匹配不到就跳过,而不是强行展示错误数据。
4. 脱离页面的能力少做,贴近页面的能力多做
扩展天然适合做贴近页面上下文的事情,例如读取当前 URL、标记 DOM、生成截图、打开侧边栏。至于复杂分析、报表建模、长期监控,更适合交给数据平台或后端系统。
这个边界感很重要。一个内部工具如果什么都想做,很快就会变成另一个复杂平台。我们更愿意让它成为现有平台旁边的一层轻量入口。
对外分享时怎么脱敏
如果要把类似实践公开分享,建议至少处理以下信息:
- 不出现真实业务域名、后台域名、管理系统地址。
- 不出现真实 API Key、项目名、产品线标识、组织名称。
- 不暴露内部事件名、字段名、埋点规范细节。
- 不展示真实用户数据、访问量、转化数据和异常样本。
- 不展示内部文档链接、群组链接、权限申请链接。
- 架构图中统一使用“数据分析平台”“业务页面”“内部服务”等泛化名称。
- 截图中注意打码用户信息、页面参数、客户信息、订单信息等敏感内容。
- 代码片段尽量使用伪代码或通用变量名,不直接复制内部实现。
这篇文章中的示例也遵循这个原则,只保留工程上有共性的部分。
实际带来的变化
从使用结果看,这类工具的价值主要体现在三个方面。
第一,降低查数门槛。用户不需要记住复杂查询条件,也不需要反复在页面和分析平台之间切换。
第二,提升排查效率。研发和产品可以更快判断“是没有数据、数据异常,还是页面元素本身没有被正确采集”。
第三,改善跨角色协作。I18N 截图、页面标记、趋势图这些材料更容易被不同角色理解,减少了很多口头解释成本。
当然,它也不是银弹。浏览器扩展受限于页面结构、浏览器权限、用户本地环境和数据平台能力。它更适合作为效率入口,而不是最终的数据治理方案。
还可以继续改的地方
后续如果继续演进,我们会优先考虑几个方向:
- 更稳定的元素匹配策略:减少 DOM 变化对标记准确率的影响。
- 更清晰的错误提示:区分权限不足、接口失败、无数据、页面不支持等情况。
- 更好的数据缓存:避免用户在同一页面短时间内重复查询。
- 更轻量的配置方式:降低新用户第一次使用的门槛。
- 更标准的埋点质量反馈:从“看到数据”进一步走向“发现埋点问题”。
- 更完整的异常观测:覆盖 Side Panel、Content Script、Background 的关键链路。
这些方向都不算炫技,但比较贴近日常使用。内部工具的生命力,往往也来自这些细小但持续的改进。
最后回头看
回头看,这个浏览器扩展并不是一个技术上特别复杂的系统。它的关键价值在于把几个原本分散的环节串了起来:当前页面、数据查询、元素定位、趋势分析和协作材料生成。
对用户来说,它减少的是来回切换和手工整理;对研发来说,它提供了一个把平台能力贴近工作现场的例子。
从工程角度看,真正花时间的地方也不是某个很炫的技术点,而是几个朴素问题:如何理解浏览器扩展不同运行时的边界,如何让跨上下文异步流程看起来足够顺滑,如何在分析平台不是专门为扩展设计的前提下做好适配,如何尽量避免页面标记误导用户。
很多内部效率工具一开始都很朴素,甚至有些粗糙,但只要它真实解决了高频问题,就值得被认真对待。希望这次实践能给正在做类似工具的同学一点参考:不一定要从大平台开始,有时候,从用户正在看的那个页面开始,就已经足够有价值。
2026 © Lizhenyui.