所见即所查:让 PV/UV 长在业务页面上

You,31 min read

所见即所查:让 PV/UV 长在业务页面上

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

XVVLkD eK310r

先说说为什么做

很多团队都会遇到一个相似的问题:数据已经被采集了,分析平台也很强大,但普通业务同学、产品同学,甚至一部分研发同学,依然很难在日常工作中快速回答几个朴素的问题:

如果每次都要打开数据平台、选择项目、拼条件、找事件、填 URL、复制 selector,再把结果和页面元素对上,整个过程就会变得很重。工具不是没有,只是离实际工作现场有一点远。

我们做这个浏览器扩展,最初的目标并不宏大:让大家在当前页面里就能更快地看到关键数据。随着使用场景增加,它逐渐变成了一个面向页面分析、可视化埋点分析、国际化辅助和截图导出的综合效率工具。

这篇文章想复盘的是:一个看起来不复杂的浏览器扩展,怎样把数据平台能力、页面上下文和用户操作串起来;以及在这个过程中,我们做了哪些技术取舍,踩过哪些不算高级但很真实的坑。

这个工具想解决什么

这个工具的核心定位可以概括为一句话:

把原本需要在数据平台里手动查询、对照和整理的工作,前移到用户正在访问的业务页面中完成。

它面向的不是专业数据分析师,而是更广泛的技术和互联网从业者,包括产品、研发、运营、翻译、测试、BI 等角色。因此产品设计上没有追求“覆盖所有分析能力”,而是优先解决高频、明确、低门槛的问题。

目前可以抽象成四类能力:

  1. 页面访问分析:基于当前页面路径,快速查看 PV、UV 和趋势。
  2. 可视化埋点分析:把页面元素和点击数据直接关联,在页面上高亮展示。
  3. 单元素下钻:点击被标记的页面元素后,查看该元素在指定时间范围内的趋势。
  4. I18N 与截图辅助:在页面中定位多语言 key,并批量生成带截图的协作材料。

为什么从浏览器扩展开始

这个需求天然发生在浏览器里。用户一边浏览业务页面,一边想知道页面背后的数据情况。如果让用户离开页面去另一个系统查数,就会出现几个问题:

浏览器扩展的优势在于,它可以同时拿到三类上下文:

这三类上下文刚好构成了工具的基本闭环。

为什么用了 Plasmo

Manifest V3 的浏览器扩展开发,常见路径大概有几种:

方案优点成本
手写 Manifest + 构建配置灵活,可控配置、热更新、消息通信都要自己维护
基于 Vite 的轻量方案构建体验好扩展运行时基础设施仍需补齐
PlasmoReact/TypeScript、文件路由、消息通信、Storage 等能力开箱可用框架会接管一部分约定

我们最后选择 Plasmo,主要是因为这个项目的目标不是研究扩展构建体系,而是尽快把业务现场和数据能力串起来。Plasmo 把很多 MV3 中繁琐但重复的事情封装掉了,例如:

这个选择并不意味着 Plasmo 适合所有扩展。如果扩展非常底层、运行时非常特殊,手写 Manifest 可能更可控。但对这种工具型、UI 较重、需要快速迭代的内部扩展来说,约定式框架带来的收益比较明显。

整体是怎么串起来的

整体架构采用浏览器扩展中比较常见的分层方式:

这种职责切分不是为了显得架构漂亮,而是浏览器扩展运行时决定的:Content Script 能接触页面 DOM,但不适合直接承担复杂跨域请求;Background 能处理权限和请求,但没有页面 DOM;Side Panel 适合承载重 UI,但它也不能直接操作宿主页面。

换句话说,扩展里的很多复杂度,来自“看起来是一个工具,实际上跑在多个运行时里”。

先回答:当前页面怎么样

页面分析的用户路径很直接:

  1. 用户打开一个业务页面。
  2. 扩展识别当前页面是否属于支持范围。
  3. 用户在侧边栏选择时间范围。
  4. 工具读取当前 Tab 的 URL 和 path。
  5. 将动态路径做规则化处理。
  6. 生成查询参数,请求数据平台。
  7. 返回 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 辅助能力主要解决三件事:

这里的实现同样体现了浏览器扩展的运行时分工:任务管理界面负责维护待截图列表和触发动作,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 }
    })
  }
}

这里有几个经验比较实用:

这类代码看起来像胶水,但胶水代码写得是否清楚,直接决定扩展后续好不好维护。

查询适配:和已有平台好好相处

这个扩展依赖一个内部数据分析平台的查询能力。平台本身能力很强,但它的查询模型并不是专门为浏览器扩展设计的。

我们的做法不是把平台查询参数直接散落到 UI 组件里,而是加了一层适配:

对外层用户来说,他点击的是“分析当前页面”;对内层实现来说,工具会转换成平台可识别的查询参数。

同时,我们把和分析平台耦合的逻辑集中在少数服务与工具函数中,包括:

这个封装并不神奇,但很重要。因为只要分析平台字段或返回结构变化,我们希望影响范围尽量停留在适配层,而不是扩散到页面组件、图表组件和 Content Script。

页面标记:尽量准确,但不逞强

页面元素标记并不是一个百分百可靠的问题。selector 可能变化,文案可能重复,页面可能异步渲染,组件结构也可能随着迭代调整。

因此我们没有把“能找到一个 DOM”当成标记成功,而是做了更保守的判断:

这类工具最怕“看起来很确定,但其实标错了”。宁可少标一点,也不要把错误信息以很自信的方式呈现给用户。

另外,在页面可见性变化、Tab 切换、页面重新激活等场景下,原先的标记状态可能丢失或失效。工具需要具备一定的重放能力,而不是只假设用户在一个稳定页面上完成完整流程。

工具自己也要能被观察

内部工具常常容易忽略自身的数据和异常。我们一开始只是想解决业务问题,但工具上线后也需要回答:

因此扩展本身也做了基础行为上报。用户首次安装或初始化时生成一个临时匿名标识,后续操作事件会带上这个标识、扩展版本和必要的用户配置类型。这里同样需要注意边界:只统计工具使用情况,不采集真实业务页面数据。

异常监控也很重要。浏览器扩展的错误排查成本比普通 Web 页面更高,尤其是 Background Service Worker 生命周期短,很多问题不能靠用户截图复现。当前实践中,至少应覆盖主要 UI 页面和关键请求链路;如果后续要进一步完善,可以继续补齐 Background 和 Content Script 的异常聚合。

发版流程:越轻越能持续迭代

内部工具如果发版成本很高,很快就会停在“能用但不好用”的状态。我们在流程上尽量让发版动作标准化:

这些事情技术含量不高,但能明显降低维护心智负担。对内部效率工具来说,能不能持续迭代,往往比第一版做得多漂亮更重要。

一些取舍

1. 把复杂查询封装成用户能理解的动作

数据平台通常需要用户理解事件名、聚合方式、字段、过滤条件、时间粒度等概念。对专业分析师来说这些是基本能力,但对普通使用者来说门槛偏高。

所以工具没有把数据平台的表单完整搬进扩展,而是把高频场景封装成几个明确动作:

也就是说,扩展承担的是“把上下文准备好”的工作,而不是替代数据平台本身。

2. Background 统一处理请求和消息

浏览器扩展里,Side Panel、Content Script、Background 的运行环境不同。如果每一层都各自请求接口、管理状态,后面很容易变乱。

因此我们把数据请求、跨上下文通信、生命周期初始化尽量收在 Background 中。Side Panel 只表达用户意图,Content Script 只负责页面呈现,Background 负责把两边连起来。

这个方式会让消息链路稍长一点,但长期看更容易维护。

3. 页面标记要“尽力而为”

DOM 匹配并不是一个百分百可靠的问题,尤其当页面存在动态渲染、组件重构、A/B 变化、同文案重复出现等情况时。

所以工具在标记元素时采用的是尽力而为的策略:selector 能匹配时优先匹配,同时结合元素内容做校验;匹配不到就跳过,而不是强行展示错误数据。

4. 脱离页面的能力少做,贴近页面的能力多做

扩展天然适合做贴近页面上下文的事情,例如读取当前 URL、标记 DOM、生成截图、打开侧边栏。至于复杂分析、报表建模、长期监控,更适合交给数据平台或后端系统。

这个边界感很重要。一个内部工具如果什么都想做,很快就会变成另一个复杂平台。我们更愿意让它成为现有平台旁边的一层轻量入口。

对外分享时怎么脱敏

如果要把类似实践公开分享,建议至少处理以下信息:

这篇文章中的示例也遵循这个原则,只保留工程上有共性的部分。

实际带来的变化

从使用结果看,这类工具的价值主要体现在三个方面。

第一,降低查数门槛。用户不需要记住复杂查询条件,也不需要反复在页面和分析平台之间切换。

第二,提升排查效率。研发和产品可以更快判断“是没有数据、数据异常,还是页面元素本身没有被正确采集”。

第三,改善跨角色协作。I18N 截图、页面标记、趋势图这些材料更容易被不同角色理解,减少了很多口头解释成本。

当然,它也不是银弹。浏览器扩展受限于页面结构、浏览器权限、用户本地环境和数据平台能力。它更适合作为效率入口,而不是最终的数据治理方案。

还可以继续改的地方

后续如果继续演进,我们会优先考虑几个方向:

这些方向都不算炫技,但比较贴近日常使用。内部工具的生命力,往往也来自这些细小但持续的改进。

最后回头看

回头看,这个浏览器扩展并不是一个技术上特别复杂的系统。它的关键价值在于把几个原本分散的环节串了起来:当前页面、数据查询、元素定位、趋势分析和协作材料生成。

对用户来说,它减少的是来回切换和手工整理;对研发来说,它提供了一个把平台能力贴近工作现场的例子。

从工程角度看,真正花时间的地方也不是某个很炫的技术点,而是几个朴素问题:如何理解浏览器扩展不同运行时的边界,如何让跨上下文异步流程看起来足够顺滑,如何在分析平台不是专门为扩展设计的前提下做好适配,如何尽量避免页面标记误导用户。

很多内部效率工具一开始都很朴素,甚至有些粗糙,但只要它真实解决了高频问题,就值得被认真对待。希望这次实践能给正在做类似工具的同学一点参考:不一定要从大平台开始,有时候,从用户正在看的那个页面开始,就已经足够有价值。

2026 © Lizhenyui.