一个没测暗色模式的 Bug,吃掉了我一半 谷歌扩展用户

我做了个 Telegram 视频下载 Chrome 扩展 TGDown

商店里 4.9 分、800 多用户、57 条评论,看起来一切正常。但后台有一个数字一直让我难受:

卸载率长期在 47% 左右。

我改过引导页、按钮文案、下载速度,数据都没什么起色。直到一次夜里调试,我才发现:问题不在用户不想用,而在不少用户装完后,扩展其实根本没工作。

这篇记录几个我踩过的坑。它们不复杂,但足够让一个"自己电脑上一直正常"的扩展,在真实用户那里直接失效。

文中讨论的下载场景仅限用户拥有版权或已获得保存授权的媒体,不涉及绕过付费、访问控制或他人版权限制。

暗色模式根本没测过

说出来有点丢人:我开发时一直用亮色主题。

在我的环境里,打开 Telegram Web,视频旁边有下载按钮,点一下能下载,一切正常。我就上线了。

但 Telegram Web 在亮色和暗色下会渲染不同的 DOM 结构与 className。我的第一版探测逻辑依赖了一个具体的 class:

dart 复制代码
document.querySelectorAll('.video-player-wrapper video')

亮色下能匹配,暗色下这个 className 变了,结果就是:

  • 页面不报错;
  • 扩展没有崩;
  • 下载按钮也不出现;
  • 用户只会觉得"这扩展没用",然后卸载。

后来我没有继续追 className,而是把策略改成"先找媒体元素,再验证媒体地址",例如读取:

  • video.currentSrc
  • video.src
  • 子元素 <source> 的地址

核心思路从"相信某个 Telegram 私有 className"变成"识别浏览器原生媒体元素"。

javascript 复制代码
function getVideoUrl(video: HTMLVideoElement) {
  const sourceUrls = Array.from(video.querySelectorAll('source'))
    .map((source) => source.src)
    .filter(Boolean)

  return [video.currentSrc, video.src, ...sourceUrls]
    .find(Boolean) ?? ''
}

上线这一版后,我观察到卸载率从约 47% 降到约 31%。

这里不能严谨地说"下降全部由暗色模式修复造成"------用户来源、版本、季节性都会影响指标。但它至少说明了一件事:

之前有相当一批用户,装完后连扩展入口都没看见。

这类用户通常不会给反馈,也不会留差评。他们只是沉默地卸载。

Telegram Web 的 A/K 不是同一个产品

Telegram Web 至少有两个常见客户端入口:

  • web.telegram.org/a/
  • web.telegram.org/k/

它们不是"同一套页面换了个皮肤"。

A 版是 Telegram 的 Web Z 客户端,采用自研的 Teact(React 风格)架构;K 版使用 Solid.js。两边的 DOM 结构、媒体加载时机和虚拟列表行为都不同。

我一开始只测了 A 版。

于是出现了第二个问题:A 版用户能看到按钮,K 版用户可能什么也看不到。

A 版相对直接,视频元素通常能较早暴露可用的媒体地址。K 版则更依赖交互和按需渲染:媒体可能直到预览层打开后才真正挂载,页面列表还会因为虚拟滚动不断复用节点。

所以后来我的逻辑不再假设"页面存在一个静态 video 标签,读到 src 就结束",而是改成按客户端分别处理:

  1. 识别当前位于 A 还是 K;
  2. 在用户主动触发下载后,定位对应的消息和预览区域;
  3. 等待真实媒体元素挂载;
  4. 优先读取 currentSrc,同时兼容 src<source>
  5. 对节点复用、预览关闭和重复绑定做清理。

这也是我后来最重要的一条经验:

不要把 URL 路径不同理解成"小兼容性差异";对扩展来说,它往往意味着两个独立的适配目标。

blob URL 不是可以交给后台的普通下载链接

第三个坑来自受访问控制媒体的下载。

有些媒体在页面侧表现为 blob: URL。它不是稳定的远程文件地址,而是浏览器在当前页面执行上下文中创建的对象 URL。

我最初的想法很自然:

content script 拿到 blob URL,发给 Service Worker;Service Worker 再调用下载 API。

结果失败了。

原因不在 chrome.downloads 本身,而在于:页面创建的 blob URL 不能被当成普通字符串跨扩展上下文使用。

扩展的 Service Worker 运行在独立的 extension origin 中;它不能可靠地解引用 Telegram 页面上下文创建的 blob URL。更何况 MV3 的 Service Worker 会按需休眠,也不适合把长下载任务的全部状态只放在内存里。

最后我把职责拆开:

  • 页面侧:在能访问媒体对象 URL 的页面上下文中读取数据;
  • 下载触发:页面侧将数据转成 Blob,通过用户已触发的下载流程保存;
  • Service Worker:负责消息协调、配额、状态持久化、徽标和普通 URL 下载等任务。

简化后的页面侧触发逻辑类似这样:

ini 复制代码
function saveBlob(blob: Blob, filename: string) {
  const objectUrl = URL.createObjectURL(blob)
  const link = document.createElement('a')

  link.href = objectUrl
  link.download = filename
  link.click()

  setTimeout(() => URL.revokeObjectURL(objectUrl), 4000)
}

这里真正重要的不是代码,而是边界:

blob URL 不是"下载地址",而是某个页面上下文里暂时可访问的对象引用。

一旦把它当普通 URL 在不同上下文之间传递,就很容易遇到 Failed - Error 这种看起来毫无信息量的问题。

我也踩过 Chrome 商店审核的坑

功能能跑通,不代表能顺利上架。

我前后收到过三类审核反馈:

  • 文案中的 "free unlimited download" 被认为是过度承诺,改成了更克制的描述;
  • host_permissions 最初写得过宽,后来收窄到 https://web.telegram.org/*
  • 商店截图中出现了 Telegram 标识,审核反馈要求移除。

这些不一定是所有扩展都会遇到的固定规则,但至少对我来说,它们说明了一件事:

审核员会按最严格、最字面的方式理解你的文案、权限和素材。

现在我会在提交前额外检查:

  • 有没有 "unlimited""best""guaranteed" 这类难以证明的绝对化措辞;
  • 权限是否只覆盖当前已实现的功能;
  • 截图、图标、文案里是否使用了第三方商标或容易引起误解的素材;
  • 隐私说明、商店描述与实际行为是否一致。

Chrome 商店对最小权限有明确要求;宽泛的 host 权限也会增加审核和用户信任成本。

最后复盘:卸载率不是一个"运营指标"

我以前把卸载率当成一个结果数据:高了就改引导、改 UI、加功能。

后来才意识到,它更像一个报警器。

用户卸载不一定是因为"不喜欢",也可能是因为:

  • 根本没看到入口;
  • 当前主题下按钮没有渲染;
  • 使用的是另一套 Web 客户端;
  • 点击后没有任何可理解的反馈;
  • 下载失败,但扩展没有告诉他为什么。

所以现在我更关心一条漏斗,而不是只盯卸载率:

复制代码
扩展安装
  → Telegram 页面打开
  → 下载按钮成功展示
  → 用户点击按钮
  → 媒体地址解析成功
  → 下载任务成功完成
  → 用户保留扩展

只看最后一步,很难知道问题发生在哪里。

这次最让我后怕的不是某个选择器写错了,而是我在自己的环境里验证成功后,就默认它对所有用户都成立。

暗色模式要测。

A/K 两个客户端都要测。

blob URL 要按执行上下文设计,不能把它当普通链接。

商店文案、权限和截图,要按审核员会挑刺的标准准备。

用户不会替你做兼容性测试;他们只会在第一次用不了时离开。


参考链接:

相关推荐
liangshanbo12151 小时前
React useState 函数式更新面试题整理
前端·react.js·前端框架
涛涛ing2 小时前
为什么你的页面在 Safari 上总出问题?Interop 2027 正在解决这个 20 年老毛病
前端
不一样的少年_2 小时前
WebP 压缩到底在干嘛?小白也能看懂的原理拆解
前端·后端·图片资源
问心无愧05132 小时前
ctf show web 177
前端·笔记
不一样的少年_2 小时前
PNG/JPG 如何变成 WebP?真相不是改后缀!
前端·后端·图片资源
杉氧2 小时前
RN 性能调优指南:重渲染(Re-renders)控制与长列表(FlatList)优化
android·前端·react native
一_个前端2 小时前
[JS] 一站式搞定 PDF、图片、Dom弹窗、表格的浏览器打印功能
前端
JavaGuide2 小时前
阿里 Qoder 又开源了一个专门给 Claude Code、Codex 做“体检”的项目
前端·后端
不一样的少年_2 小时前
JPEG 压缩到底在干嘛?小白也能看懂的 8 步拆解
前端·后端·图片资源