
浏览器扩展升级 Manifest V3 后 Service Worker 总被回收、eval 又被禁Zotero Connector 是怎么把翻译器救活的【免费下载链接】zotero-connectorsChrome, Firefox, Edge, and Safari extensions for Zotero项目地址: https://gitcode.com/gh_mirrors/zo/zotero-connectors本文适合正在把 MV2 扩展迁移到 MV3、或者因为eval被禁用而发愁的浏览器扩展开发者也适合想搞懂offscreen 文档 沙箱 iframe这套组合拳怎么打的技术读者。Zotero Connector 是 Zotero 官方出品的浏览器扩展负责在 Chrome、Firefox、Edge、Safari 里抓取网页并翻译成文献条目。本文要拆解的是它迁移到 Manifest V3 之后遇到的两个硬约束——Service Worker 说睡就睡、eval说禁就禁——以及它用一套三层消息架构给出的答案。一、故障现场翻译器怎么一夜之间全哑了事情发生在把扩展从 Manifest V2 迁移到 Manifest V3 的改造期间。改完的第一版QA 随手打开一篇文章点保存到 Zotero结果页面右下角弹出保存进度然后卡住不动再点一次连进度条都不出来了。控制台里躺着一条很扎眼的报错Refused to evaluate a string as JavaScript because unsafe-eval is not an allowed source of script in the following Content Security Policy directive翻译成人话你刚写的代码里有一个eval而 MV3 的 CSP 不允许它存在。但问题不止这一个。同一版测试里还出现了另一个诡异现象扩展图标点开之后前两次点击毫无反应第三次突然活了。这其实是 Service Worker 被回收、重新唤醒时的冷启动延迟。两个问题叠在一起翻译功能基本等于残废。而 Zotero Connector 的翻译器translator恰恰是这套扩展的灵魂——网页能不能被正确抓成文献条目全靠它们。所以这不是修个小 bug是核心功能被 MV3 的规则卡死了。二、抽丝剥茧为什么偏偏是 eval 和 Service Worker先说eval为什么是刚需。Zotero 的翻译器不是编译进扩展里的而是以源码形式从 Zotero 仓库实时拉取、按需执行的。每个翻译器本质是一段独立的 JS 代码针对某个网站比如知网、PubMed、arXiv定义怎么从 DOM 里提取标题、作者、DOI。Zotero 团队希望翻译器能像翻译插件一样随时热更新而不需要每次改一个翻译规则就重新走一遍浏览器商店审核。在 MV2 时代background page 是个常驻页面eval随便用翻译器拿到代码直接执行一切岁月静好。MV3 一上来两条铁律直接把路堵死background page 没了换成 Service Worker。它没有 DOM不常驻浏览器默认约 30 秒没有事件就把它回收。你的全局状态、内存里的翻译器代码全跟着一起蒸发。CSP 全面收紧unsafe-eval被禁。Service Worker 和扩展页面里都不许再执行字符串形式的 JS。更麻烦的是这两个约束互相咬合就算你想办法在 Service Worker 里开个后门执行翻译器Worker 30 秒一睡翻译器代码就没了下次还得重新拉取、重新加载——性能上根本没法看。我一开始的假设是换个执行方式比如把翻译器全部打包编译进扩展。查了代码之后立刻否掉了翻译器的更新节奏远快于扩展发版节奏捆绑发布等于把热更新能力亲手砍掉团队明确写过这是untenable不可持续的方案。那还剩什么路答案是给 Service Worker 找一间带 DOM、能 eval、且可以长期住着的屋子。三、破局方案一间 offscreen 小屋 一个沙箱 iframeMV3 里其实留了一扇窗chrome.offscreenAPI。它允许扩展创建一个隐藏的 offscreen document离屏文档这个页面拥有完整的 DOM也不受 30 秒回收的限制。它就是那间屋子。但屋子里还有个讲究offscreen 页面本身同样受 CSP 限制eval照样被禁。那eval去哪儿跑——Chrome 的 sandbox 机制。MV3 允许扩展页面里内嵌一个sandbox属性的 iframe沙箱里的代码不受扩展 CSP 管束eval可以光明正大地跑。于是 Zotero Connector 搭出了这样一套三层结构对应源码里的三个文件src/browserExt/offscreen/offscreen.html离屏文档本体里面就干一件事——内嵌一个指向offscreenSandbox.html的沙箱 iframe然后负责两头对接。src/browserExt/offscreen/offscreenSandbox.js沙箱里的翻译执行环境。翻译器代码在这里被eval出来配合offscreenTranslate.js管理每个标签页各自的翻译实例。src/browserExt/background/offscreenManager.js后台 Service Worker 侧的管家负责创建离屏文档、转发消息、定时清理死掉的翻译实例。三层之间用什么通信MessageChannel。通信链路长这样内容脚本 ── Service Worker ── offscreen 页面 ── 沙箱 iframe翻译器在这执行消息从页面里的内容脚本出发先到 Service Worker再由它转给离屏文档最后通过端口送进沙箱。反过来翻译器的回调、select弹窗结果也沿着这条管道原路返回。代码里offscreen.js用MessageChannel把port1交给沙箱 iframe、port2交给 Service Worker一个双向管道就这么搭起来了。至于内容脚本那边src/browserExt/inject/virtualOffscreenTranslate.js提供了一个虚拟翻译器它用Proxy把Zotero.Translate.Web上的所有方法拦截下来翻译器看到的是一个正常的translate对象但每个方法调用其实都被序列化成消息、发到远处沙箱里执行。调用方无感知远处干活近处拿结果。这套方案的关键取舍是不碰 eval 的禁令本身而是给 eval 找一个合法存在的容器。翻译器代码仍然热更新、仍然按需拉取只是执行地点从扩展自己的页面搬到了沙箱 iframe里。代价是消息走了一条很长的管道异步链路变深但换来的是核心能力完整保留。四、实战演示从点击到出条目的完整链路把整条链路串起来看一遍你就知道这套架构平时是怎么工作的。第一步内容脚本抓页面。用户在网页上点保存src/common/inject/inject.jsx里拿到页面 HTML调用虚拟翻译器的setDocument()把序列化后的文档、URL、cookie 一并发出去。第二步Service Worker 中转。后台收到消息offscreenManager.js检查离屏文档是否已存在getOffscreenPage()。不存在就调用browser.offscreen.createDocument()现场创建reasons填DOM_PARSER——翻译器要用DOMParser解析抓来的 HTML这是官方认可的创建理由之一。第三步沙箱执行翻译。翻译器代码在沙箱 iframe 里被 eval读取传进来的文档 HTML抽出标题、作者、DOI最后把条目数据沿管道送回。第四步写回 Zotero。条目数据回到内容脚本通过itemSaver.js调起本机 Zotero 客户端完成保存页面上弹出已保存。这套链路里最有意思的一个细节是Service Worker 重启之后的恢复逻辑。前面说过 Worker 会被回收但它睡着之后离屏文档并不会一起消失——这俩是独立生命周期。所以offscreenManager.js在发现离屏文档还活着时会给它postMessage(service-worker-restarted)离屏页收到后立刻重新init()重新建立 MessageChannel。Worker 睡醒回来管道自动接上用户几乎无感。想亲手验证这条链路可以这样复现加载扩展后打开chrome://serviceworker-internals/手动停止那个 service worker然后回网页点一次保存到 Zotero。你会看到离屏文档收到重启通知、重新握手、翻译照常跑完——这一套从假死到自愈的过程就是这套架构最值钱的地方。五、避坑指南我们踩过的坑你可以绕着走这套方案能跑通但坑也不少挑几个最典型的说。坑一MessageChannel 的端口不是永久的。Service Worker 每重启一次之前建立的端口就失效了必须重连。如果恢复逻辑没写好最典型的现象就是第一次点击没反应第二次突然好了——因为第一次点击把 Worker 唤醒但消息顺着断掉的管道丢了。我们的做法是让离屏页监听service-worker-restarted事件主动重连而不是等调用方去撞墙。坑二离屏文档创建理由必须合规。browser.offscreen.createDocument()的reasons字段是枚举值DOM_PARSER只适用于解析文档AUDIO_PLAYBACK之类另有用处。滥用理由会被 Chrome 拒绝创建而且这个拒绝发生在异步的 promise 里不仔细看日志根本发现不了。翻译器用DOMParser所以选DOM_PARSER是名正言顺的——但如果你顺手在这里塞别的事等着被拒吧。坑三别把 keep-alive 当万能药。源码里keep-mv3-alive.js的做法是每 20 秒调一次chrome.runtime.getPlatformInfo()给 Worker续命而且设置了上限——超过 10 分钟就不续了让它正常睡去。这个设计很克制无脑续命会让 Service Worker 永不休眠白白烧资源还可能被 Chrome 反制。Google 给这类保活行为留的口子越来越小正确的姿势是让它快睡、醒了能快速自愈而不是硬撑着不睡。坑四Firefox 不吃这一套。这套 offscreen 架构是 Chrome 系MV3的专属解法。Firefox 的 MV3 实现里没有offscreenAPI而且它的 background 脚本存活策略比 Chrome 宽松得多eval的 CSP 也允许通过配置放开。所以代码里到处是Zotero.isChromium、Zotero.isManifestV3之类的分支判断——同一个功能不同浏览器走的完全是两条路迁移时千万别默认改完 Chrome 就万事大吉。坑五内存泄漏比想象中隐蔽。翻译实例是按标签页建的如果标签页关闭时不清理离屏文档里会积攒一堆死掉的翻译对象。offscreenManager.js专门监听tabs.onRemoved并每 15 分钟做一次全量清理把活着的标签页列表发给离屏页、逐个核对回收。别小看这一步跑久了你会发现离屏文档的内存一路往上爬。六、写在最后被规则卡住时先找合法的后门回头看这次迁移最值得琢磨的不是 MessageChannel 怎么接、沙箱怎么配而是一个思路转换当新规范堵死了你习惯的路与其硬闯不如重新审视规则本身留了什么口子。MV3 禁eval但 sandbox iframe 合法MV3 不留常驻页面但 offscreen document 合法。Zotero Connector 没有跟规则对抗而是把需要 eval 的翻译器和需要 DOM 的解析精确地搬进规则允许的容器里再靠一条显式的消息管道把它们和主流程缝合起来。这套思路对任何 MV3 迁移项目都适用先别急着骂规范把旧功能按需要什么能力拆开——要 DOM要 eval要长驻——然后逐个去找对应的官方解法。规则是死的但规则允许的空间往往比你以为的大得多。两个可以立刻上手的行动建议打开你自己的 MV3 扩展用chrome://serviceworker-internals/手动杀掉 service worker再触发一次核心功能观察消息链路是否会自动恢复。把Worker 重启后自愈当成 MV3 扩展的必备能力来测而不是可选项。如果你的扩展也依赖动态代码脚本注入、翻译器、规则引擎照着src/browserExt/offscreen/和src/browserExt/inject/virtualOffscreenTranslate.js的思路用沙箱 iframe 执行 Proxy 虚拟对象转发的套路搭一版最小原型跑通之后再考虑消息序列化、超时、重连这些细节。【免费下载链接】zotero-connectorsChrome, Firefox, Edge, and Safari extensions for Zotero项目地址: https://gitcode.com/gh_mirrors/zo/zotero-connectors创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考