Chrome executeScript 为什么拿不到外层变量:注入函数的序列化边界
扩展的 Popup 或 service worker 里定义了一个常量,再把用到它的函数交给 chrome.scripting.executeScript(),编辑器没有报错,普通函数调用也正常。真正注入页面时,却出现 ReferenceError。
问题出在执行边界:Chrome 传过去的是函数的序列化结果,原来模块的词法环境不会一起进入目标页面。
看起来是回调,实际已经离开模块
const storageKey = 'reader-theme';
await chrome.scripting.executeScript({
target: { tabId },
func: () => localStorage.getItem(storageKey),
});
这里的 storageKey 属于扩展模块,localStorage 属于目标执行环境。两者写在同一个函数体里,却有不同的来源。Chrome 的 ScriptInjection 文档说明,func 会序列化再反序列化,绑定参数与执行上下文会丢失;args 则要求 JSON 可序列化。
把 const 改成 let、给函数加 async、挪到另一个文件,都不会把这个词法环境带过去。导入的 helper 也是模块变量;helper 内部再引用外层配置,问题只是多绕了一层。
让输入穿过 args
await chrome.scripting.executeScript({
target: { tabId },
args: [storageKey],
func: (key: string) => localStorage.getItem(key),
});
需要小型辅助函数时,可以放进注入函数内部,让它与函数体一起传输。需要较多模块能力时,评估 files 注入及对应构建产物,不把整个应用依赖压进 args。DOM 节点、函数和复杂实例也不能凭原来的引用跨过去;先整理成普通数据,再在目标环境重建操作。
配套示例同时提供真正的注入函数和一个 Node 隔离上下文实验。实验将函数转成源码,再放进新的 vm context:直接调用能读取外层变量,隔离执行会失败,显式参数能成功。这验证了闭包丢失这一点;它没有模拟 Chrome 权限、执行 world 或页面导航。
读取 https://shengsheng.fun/files/chrome-execute-script-closure-boundary/kits/chrome-injection-boundary/ 的 README.md 和 FILES.json,按项目入口迁移 copy/ 中适用示例,并验证实际注入结果。
完整实现与接入示例见完整源码与接入方法弹窗。
world、权限和导航还需要分别检查
ISOLATED 是默认执行 world,MAIN 才与宿主页面的 JavaScript 环境共享 world。切到 MAIN 也不会恢复扩展模块的闭包。执行环境里的全局变量是否存在、能不能访问目标站点、tab 是否已经导航到别处,是另外几项检查。
验收时至少在真实扩展中读取 InjectionResult,检查目标 frame 和结果,并覆盖导航后重试。对会修改页面或存储的注入,使用可回滚的测试页,不把「没有异常」当成业务操作成功。
ESLint 可以提前指出哪一部分
@sheng/chrome-no-execute-script-closure检查可识别的注入函数引用。项目 wrapper 用 calleeNames 配置,目标环境确实存在的全局对象用 allowedGlobals 表达。
这是静态检查的边界:无法保证某个页面运行时一定挂了同名全局,也不能验证授权、frame 或异步导航。配置允许项前,要先拿目标 world 的实际证据。闭包来源由 lint 尽早提示,权限和注入结果继续由扩展运行测试确认。