Skip to content
Go back

浏览器翻译插件上线后,真正麻烦的是适配真实网页

前面几篇写 Parallel Translate,我讲过怎么把网页变成双语阅读视图,也讲过划词面板和查词接口。

这些都属于“把核心链路做出来”。

但真正上线之后,我发现另一个问题更耗时间:真实网页比测试页面复杂得多。

在自己准备的文章页里,正文结构清楚,样式干净,DOM 变化少。扩展开启后扫描正文、插入译文、关闭恢复,看起来都还不错。

可真实网页不是这样。它可能是 SPA,可能无限滚动,可能正文里混着广告、目录、代码块、推荐卡片,可能自己也有复杂浮层和 CSS。用户不是在一个理想 HTML 文件里阅读,而是在整个互联网里阅读。

所以浏览器翻译插件上线后,真正麻烦的不是“还能加什么功能”,而是“它能不能在更多普通网页上别添乱”。

先解释一下 SPA

SPA 是 Single Page Application,单页应用。

简单说,用户在页面里点击跳转时,浏览器不一定真的重新加载整个 HTML。网站可能只是用 JavaScript 改了地址栏,再局部更新页面内容。

对普通用户来说,这是一次页面跳转。对浏览器扩展来说,如果只在页面初次加载时扫描一次,就会出问题。

典型情况是:

打开文章 A  -> 扩展扫描并翻译站内跳到文章 B  -> 浏览器没有完整刷新  -> 扩展还以为自己在文章 A  -> 翻译状态、正文范围、观察器都可能过期

所以后面我补了 URL 变化监听。它大概会覆盖这些情况:

这些都是现代网页常见的“地址变了,但页面没有完整刷新”的方式。

监听到 URL 变化后,不能简单继续在旧状态上工作。更稳的做法是先清理当前翻译状态,再延迟一点重新扫描。

URL 变化  -> 判断当前是否正在翻译  -> 刷新正文范围  -> 停掉旧的扫描状态  -> 等页面新内容稳定一点  -> 重新开启翻译

这里的延迟不需要很长。关键是给前端框架一点时间把新页面内容渲染出来。

页面翻译不能假设一次成功

刚开始做全文翻译时,我更关注的是怎么扫描正文、怎么插入译文。

上线后更常见的问题是:某几个段落失败了。

失败原因可能很多:

如果一段失败就让整页状态变得很重,阅读体验会很差。用户不一定在意下面某一段暂时没翻出来,他更在意当前可读部分别被破坏。

所以我把失败处理改得更局部。

单个翻译单元失败后,只在这个位置显示错误和重试入口。用户点重试时,也只重试这一段,不重新扫描整页。

一个段落翻译失败  -> 当前段落显示失败状态  -> 提供重试  -> 重试时恢复 loading  -> 成功后替换当前段落结果

这比“整页重新翻译”更符合用户预期,也减少了重复请求。

这里还有一个细节:请求回来时,要确认这个结果还适用于当前页面。因为用户可能已经跳转,原来的 DOM 节点也可能被网站替换。如果结果过期,就应该丢弃,而不是硬插回页面。

布局问题通常不是翻译问题

真实网页上的很多 bug,看起来像翻译问题,实际上是布局问题。

比如:

这些问题不是换一个翻译模型能解决的。

前面我已经写过,浏览器双语阅读的关键是不要把网页当成纯文本。上线后我对这点更有体会:页面翻译更像是在别人的排版系统里临时插一层内容。

这要求插入的译文足够克制:

有时候最好的修复不是“让译文更明显”,而是让它更不打扰。

站点规则是补丁,不是产品核心

真实网页适配到后面,很容易想维护一份站点规则库。

比如这个站点正文在 article,那个站点要排除侧栏,另一个站点动态加载评论区。为高频站点写规则确实有用,但我不想让产品依赖一大堆站点补丁。

我的理解是:

通用扫描规则  -> 负责大多数页面的基本可用站点规则  -> 只修正高价值页面的明显边界

站点规则可以处理正文范围、排除区域、特殊容器。但它不应该成为主要算法。否则每新增一个网站都要单独维护,最后会变成永远补不完的名单。

更好的方向还是让通用规则足够稳:

站点规则只在通用规则明显不够时再上。

触发器也要适配页面行为

这轮我还改了划词触发器。

以前我更偏向“选中文字后直接翻译”,但真实使用中并不是所有人都喜欢这种方式。有些用户只是想复制,有些用户只想临时查,有些网页自己也会处理 selection。

所以触发方式后来变成几个选项:

这里看起来是一个 UI 设置,实际也是网页适配问题。

因为不同页面对鼠标事件、选区变化、双击选词的处理不一样。有些页面会吞掉事件,有些页面在你点击扩展浮层时会清空选区。

为了让触发器更稳定,需要处理这些细节:

用户看到的是一个小圆点或小图标,但背后其实是在跟宿主页面抢一点点交互空间。

上线后才会遇到“旧状态”

本地开发时,我经常会清空数据、重新安装、重新加载扩展。这样很容易忽略一个事实:真实用户会带着旧配置升级。

功能迭代多了以后,browser.storage.local 里会留下各种历史字段。比如语言设置、Provider 配置、站点偏好、学习模式、划词触发方式。

如果每个地方都自己读 storage,再自己补默认值,迟早会出错。

所以我后来加了一层配置 schema 迁移。

schema 可以理解成配置结构的版本。扩展启动时,先把旧配置归一化成当前版本稳定结构,再交给 Popup、Options、Content Script 和 Background 使用。

这样做的好处是:

这类工作不显眼,但对上线后的扩展很重要。因为用户的真实环境永远不会像开发环境那么干净。

别让缓存无限长大

真实使用以后,另一个问题是缓存。

翻译结果、查词结果、AI 句子详解、TTS 音频,这些都可以缓存。缓存能让体验更快,也能减少请求。

但缓存不是越多越好。

特别是 TTS 音频,它比普通文本大很多。如果不清理,IndexedDB 迟早会被撑大。

所以缓存要分两类:

用户资产  -> 生词本、查询历史、用户设置  -> 不能自动随便删可重建缓存  -> 翻译结果、词典结果、AI 解释、音频块  -> 可以按时间或容量清理

这个边界非常重要。

删除一条过期翻译缓存,用户下次最多重新请求。删除用户生词本,那就是数据丢失。

所以后台定期清理只应该碰可重建缓存。对音频这种大对象,还要加容量上限。

结语

浏览器翻译插件最开始做通时,成就感很强:选中页面、扫描正文、翻译、插入译文,一条链路跑起来了。

但上线后真正花时间的,是各种没那么漂亮的问题:

这些都不是“再加一个 AI 功能”能解决的。

做到后面我越来越觉得,浏览器扩展的难点不在它能不能控制网页,而在它能不能克制地进入网页,然后稳定地退出。

真实网页不会配合我,用户也不应该为网页差异买单。

所以后续迭代里,我更愿意把时间花在这些看起来琐碎的适配上。它们不一定适合写在功能介绍里,但它们决定了一个阅读工具能不能被长期打开。


Share this post on: