用户脚本的边界:什么时候该升级成正式扩展
用户脚本写久了,你会一次次撞上同一堵墙:这个需求脚本做不到。这篇把墙的位置画清楚,并给出「该搬家了」的判断清单——什么时候值得把它重写成 Manifest V3 扩展。
四堵墙
| 边界 | 具体表现 | 对应的扩展能力 |
|---|---|---|
| 页面之外 | 管不了标签页、书签、下载、浏览器设置 | chrome.tabs、chrome.downloads、chrome.bookmarks |
| 没有 UI | 只有面板菜单,弹不出窗口和面板 | popup、options、side panel |
| 没有后台 | 页面关了脚本就死,无法定时待命 | Service Worker、chrome.alarms |
| 分发无力 | 文本传播无审核、无统计、无自动恢复 | 商店上架、版本管理、崩溃上报 |
逐个展开。页面之外是最硬的一条:chrome.tabs 这类 API 用户脚本完全没有等价物,GM_ 模拟不了。想「检测重复标签页」「自动整理下载文件」,只能写扩展。没有 UI:脚本能往页面里塞节点,能加面板菜单,但它没有属于自己的「地盘」——弹窗、设置页、侧边栏都是扩展上下文。塞进页面的 UI 还会与页面样式互相污染。没有后台:脚本生命周期绑定页面,页面一关全没了;想要「每天早上八点检查一次消息」「监听标签页创建事件」,需要常驻的事件中枢,这正是扩展 Service Worker 的本职。分发无力*:脚本靠复制粘贴传播,没有版本回滚、没有使用数据,用户装了出错你都不知道。
自测清单
对照下面的问题,命中两条以上就该考虑升级:
- 需求涉及两个以上互不相干的站点(脚本要为每个站点重复声明匹配)?
- 需要记住的结构化数据超过几 KB,或要在设备间同步?
- 需要「页面没开着也在工作」的逻辑?
- 想给不认识的陌生人用,且希望有反馈渠道?
- 碰到了「这个 API 我想要但 GM_* 没有」的时刻?
一条都不中?那就安心写脚本,别过度工程化。
升级路线上概念怎么映射
脚本和扩展的概念对应度极高,搬家基本是翻译工作:
// 脚本元数据
// @match https://example.com/*
// @grant GM_setValue
// @run-at document-start
翻译成扩展 manifest:
{
"manifest_version": 3,
"name": "自动展开全文",
"version": "1.0.0",
"content_scripts": [
{ "matches": ["https://example.com/*"],
"js": ["content.js"],
"run_at": "document_start" }
],
"permissions": ["storage"]
}
@match 变成 content_scripts.matches,@run-at 变成 run_at,GM_setValue 变成 chrome.storage.local.set,GM_xmlhttpRequest 变成扩展上下文里自带 host 权限的 fetch。真正的新概念只有两个:Service Worker 与消息通信——脚本时代所有代码都在一个文件里,扩展时代它们分处不同上下文,必须靠消息传递协作。这两块正是第三章的重点。
升级前想清楚的三件事
扩展不是免费的午餐,升级意味着三笔新成本:审核成本——上架 Chrome Web Store 要过审,隐私声明、权限说明都得写;维护成本——Chrome 版本迭代快,API 行为变化需要跟进;兼容成本——Firefox、Edge 各有细节差异,脚本时代一个浏览器通吃的好日子结束。
所以正确姿势是渐进:脚本验证想法,核心逻辑搬进扩展,脚本版本继续保留当轻量分发渠道。两者代码层的正则与匹配规则完全通用,正则匹配写法可参考本站《正则表达式从入门到精通》:正则实战。
本章小结
用户脚本的舒适区是「单页面、纯改造、无后台、自己用」;一旦需求越出页面、需要 UI 和后台、要面向大众,就该搬进扩展。下一章我们正式进入 Manifest V3 的世界观,从 manifest.json 逐字段讲起。工具链不变,HBuilderX 等编辑器技巧照旧适用,见 HBuilderX 效率技巧。
双轨维护的实践细节
前面说「渐进迁移」,具体怎么管两条线?社区常见的做法有三种,按投入从小到大:
方案一:脚本为主,扩展为壳。 核心逻辑全部在 content script(脚本与扩展各一份,内容几乎相同),扩展版只多 manifest 与后台空壳。适合页面改造类需求,双端同步靠一个共享源码文件。缺点是 GM API 与 chrome API 的调用层要各写一份。
方案二:扩展为主,脚本做轻量版。 扩展是完整产品,脚本只保留最高频的一两个功能。适合扩展功能复杂、脚本渠道只为引流的场景。注意脚本的 @match 与扩展的 host_permissions 要保持一致,否则两版行为对不上。
方案三:彻底二选一。 判断标准回到维护责任:如果 90% 的用户从商店来,脚本版本可以停止更新并置顶公告指向扩展;反之亦然。工具的价值在被使用,不在形态完整。
无论哪种方案,都建议把「纯逻辑」(选择器解析、文本处理、规则匹配)抽成与 API 无关的函数,两端直接复制。这类纯函数也最容易写单元测试,双端质量一起保。
一个真实的升级案例
把过程具体化:假设你的「自动展开全文」脚本火了,用户提了三类新需求——支持更多站点、记住「哪些站点不展开」、提供开关面板。逐条判断:
- 支持更多站点:脚本改 @match 就能做,不触发升级;
- 记住站点黑名单:GM 存储能做到,但用户要手改代码或你做菜单管理,体验勉强;
- 开关面板:脚本的 UI 只能塞进页面,样式冲突难看,这是硬需求了。
第三条触发了升级。扩展版里,黑名单存 storage.sync 跨设备同步,开关做进 popup,两小时完工——而脚本版做同样的东西要多花几倍力气还效果打折。这个决策过程比代码本身更值得学:先算清楚每条需求的实现成本差,成本差悬殊的那条就是升级触发器。
迁移时的坑位清单
真要动手搬,这几个坑提前知道:
- 沙箱差异:
@grant none的脚本直接跑在页面环境,能访问页面变量;扩展 content script 永远在隔离世界,原脚本里依赖页面变量的代码要改写成 world: MAIN 注入或消息代理; - 运行时机:脚本 @run-at 与扩展 run_at 语义一致,但扩展静态注入比脚本注入更早、更稳定,原来靠延时 setTimeout 等元素的代码可以改用 MutationObserver;
- 存储语义:GM 存储按脚本隔离,chrome.storage 按扩展隔离——多脚本合一后,注意键名冲突;
- 分发预期:上架后用户量级与脚本渠道完全不同,异常处理与日志要补齐,别让第一批商店用户当测试员。
两条路的选择其实也是产品观
最后跳出技术,谈谈怎么想这件事。脚本与扩展的取舍背后是三个产品问题:你为谁做(自己、小圈子还是公众)?你承诺多久(一次性的小工具还是长期维护的产品)?你愿意承担什么(没有审核的自由,还是没有商店的信任背书)?三个答案定了,方案自然定了。技术只是答案的执行。
本书的立场是:先做脚本练手,把「改网页」的手感建立起来;需求越界时果断升级扩展,把「信任与分发」的完整链路走通。两条路都走过一遍的开发者,面对任何浏览器侧需求都能快速给出正确的形态选择——这正是这本书想交付的能力,而不是某个具体 API 的记忆。
把自测清单与三个问题放在手边,每次需求评审时过一遍,你会发现大多数争论在动手前就有了答案。
动手任务:拿你手头最长的一个脚本对照自测清单打分,命中两条以上就写下它的扩展版需求说明——这份说明会成为下一章最好的阅读材料。
补充一个判断技巧:当你开始为了绕开脚本限制而写「古怪代码」时——比如用定时器模拟后台、把设置界面硬塞进页面角落、用 cookie 存配置——这些别扭的代码本身就是升级信号。工具不会告诉你它不合适,但代码的别扭程度会。
反过来也一样:如果某个需求你已经在扩展里实现了,却发现用户只在页面上用得着它,那把这一部分抽成脚本反哺社区,往往比维护完整的扩展更有效率。
本章由 WorkBuddy 原创整理,API 行为依据 Chrome 扩展官方文档(Manifest V3)。
