破解 Chrome 扩展的「两世界难题」:MV3 下的 ISOLATED 与 MAIN World 桥接之道
破解 Chrome 扩展的「两世界难题」MV3 下的 ISOLATED 与 MAIN World 桥接之道你的 Content Script 能调用chrome.storage却摸不到网页的window.data你的页面脚本能读取任何 DOM 和 JS 对象却喊不动任何扩展 API。这就是 Manifest V3 中最经典的两世界难题。而它的解法叫做Bridge。1. 两个世界两种能力在 Chrome 扩展的 MV3 架构中每个标签页里至少存在两个完全隔离的 JavaScript 执行环境ISOLATED World扩展默认的“沙盒世界”。这里的脚本即 Content Script拥有完整的chrome.*API 权限可以操作 DOM但无法访问网页自身定义的任何 JavaScript 变量、函数或对象例如window.myApp、window.$。MAIN World网页本身的“原生世界”。这里的脚本就是页面加载的 JS能与页面的所有 JS 对象深度交互但无法调用任何扩展 API如chrome.storage、chrome.runtime。这种设计是出于安全和隔离的考虑既防止恶意网页污染扩展的特权上下文也避免扩展的变量意外覆盖网页的逻辑。扩展进程浏览器标签页ISOLATED WorldMAIN Worldchrome.runtime APIwindow.postMessagewindow.postMessage网页自身脚本可访问 window、DOM、fetch扩展注入的 MAIN 脚本需通过 manifest 指定 world:MAINContent Script默认 world:ISOLATEDBackground Service Worker处理存储、网络、生命周期✅ 可读取网页任意 JS 变量❌ 无 chrome API✅ 完整 chrome API 权限❌ 看不到网页 JS 对象2. 两世界难题为什么非得 Bridge假设你要写一个针对 YouTube 的扩展需要读取ytInitialData这个页面内部对象然后调用chrome.storage保存起来。单独任何一个世界都做不到在 ISOLATED World 中console.log(window.ytInitialData)→undefined隔离了在 MAIN World 中能读到ytInitialData但chrome.storage.local.set(...)→TypeError: Cannot read property local of undefined于是你必须同时注入两个世界的脚本并让它们相互通信。这就是 Bridge 模式的由来。3. Bridge 的核心window.postMessage浏览器提供了postMessage方法允许不同执行环境包括不同 World之间发送消息。结合自定义事件或直接监听message事件就能搭建一座安全的消息桥。3.1 基础版从 MAIN World 发送数据到 ISOLATED Worldstep 1在manifest.json中声明同时注入两个脚本注意world字段{manifest_version:3,name:Two Worlds Bridge Demo,content_scripts:[{js:[content.js],matches:[all_urls],run_at:document_idle},{js:[main-world.js],matches:[all_urls],run_at:document_start,world:MAIN// 关键指定注入到 MAIN World}]}step 2main-world.js读取页面数据通过postMessage发送// main-world.js (运行于 MAIN World)(function(){// 读取网页内部的私有数据constpageDatawindow.__SECRET_DATA__||{user:anonymous};window.postMessage({source:my-extension-main,type:PAGE_DATA,payload:pageData},*);})();step 3content.jsISOLATED World监听消息并调用扩展 API// content.js (运行于 ISOLATED World)window.addEventListener(message,(event){// 必须验证消息来源防止恶意网页伪造消息if(event.source!window)return;if(event.data?.source!my-extension-main)return;if(event.data.typePAGE_DATA){chrome.storage.local.set({pageData:event.data.payload},(){console.log(数据已保存,event.data.payload);});}});3.2 双向通信ISOLATED World 向 MAIN World 发送请求有时候我们需要 MAIN World 去做一些事情比如修改页面上的某个全局变量或者调用页面提供的一个函数。同样用postMessage反向发送即可。// content.js (ISOLATED)chrome.storage.local.get(config,(result){window.postMessage({source:my-extension-isolated,type:UPDATE_CONFIG,payload:result.config},*);});// main-world.js (MAIN)window.addEventListener(message,(event){if(event.source!window)return;if(event.data?.source!my-extension-isolated)return;if(event.data.typeUPDATE_CONFIG){// 直接修改页面的全局配置window.myAppConfigevent.data.payload;}});完整的双向 Bridge 流程如下图所示Background (Service Worker)ISOLATED WorldMAIN WorldBackground (Service Worker)ISOLATED WorldMAIN World读取网页内部对象反向流程扩展主动更新页面postMessage({type:PAGE_DATA, data})chrome.storage.set(data)存储完成postMessage({type:STORED_OK})chrome.runtime.sendMessage({cmd:updateTheme})postMessage({type:THEME_UPDATE, theme:dark})修改页面 CSS / 全局变量4. 安全加固别让你的桥成为后门window.postMessage广播的消息网页自身的恶意脚本也能监听到。反之恶意网页也可以伪造消息发给你的 Content Script。因此一个安全的 Bridge 必须包含身份验证和消息过滤。4.1 使用唯一令牌Token在扩展初始化时生成一个随机令牌并通过安全方式例如chrome.storage 动态注入传递给 MAIN World 脚本。所有postMessage携带这个令牌接收方首先校验令牌。// 简化示例在 ISOLATED 生成令牌并注入到 MAINconsttokencrypto.randomUUID();// 通过 DOM 属性传递MAIN 脚本可以读取document.documentElement.dataset.bridgeTokentoken;// MAIN World 脚本读取令牌并携带在所有消息中consttokendocument.documentElement.dataset.bridgeToken;window.postMessage({source:ext,token,type:DATA,payload},*);// ISOLATED 监听时校验 tokenif(event.data.token!expectedToken)return;4.2 严格校验消息结构和类型使用 TypeScript 或运行时 schema 校验如 Zod拒绝任何不符合预期格式的消息。constALLOWED_TYPES[PAGE_DATA,UPDATE_CONFIG,PING];if(!ALLOWED_TYPES.includes(event.data.type))return;4.3 最小化*目标postMessage的第二个参数尽量指定具体的 origin而不是*。不过由于你的扩展可能运行在任意网站通常只能写*因此必须加强消息内容校验。5. 高级技巧动态注入与chrome.scripting除了在manifest.json中静态声明world: MAIN的脚本你也可以使用chrome.scriptingAPI 动态注入。这种方式更灵活适合按需注入的场景。// 在 Background 或 Content Script 中执行asyncfunctioninjectMainWorld(){const[tab]awaitchrome.tabs.query({active:true,currentWindow:true});awaitchrome.scripting.executeScript({target:{tabId:tab.id},func:(){// 这段代码会运行在 MAIN Worldwindow.customData{from:dynamic injection};},world:MAIN// 关键参数});}6. 实战案例Hookfetch请求并记录到扩展存储这个例子展示了 Bridge 解决实际问题的典型流程MAIN World拦截全局fetch获取请求 URL 和响应数据。ISOLATED World接收到数据后调用chrome.storage保存。Background负责将存储的数据同步到远端服务器。捕获请求/响应页面发起 fetchMAIN World HookpostMessage 到 ISOLATEDISOLATED 调用 chrome.storageBackground 同步到云端核心代码片段MAIN World// main-world.jsconstoriginalFetchwindow.fetch;window.fetchasyncfunction(...args){constresponseawaitoriginalFetch.apply(this,args);constcloneresponse.clone();constbodyawaitclone.text();window.postMessage({source:fetch-hook,url:args[0],status:response.status,body:body.substring(0,500)// 避免过大},*);returnresponse;};7. 总结与最佳实践需求使用哪个 WorldBridge 角色调用chrome.storage、runtime.sendMessageISOLATED消息接收者 / 发送者读取window.ytInitialData、HookfetchMAIN数据采集 / 页面操作修改页面全局变量或原型链MAIN执行者将页面数据持久化到扩展存储协作MAIN采集 → ISOLATED存储从扩展存储读取配置并应用到页面协作ISOLATED读取 → MAIN应用记住三个核心原则注入两个世界– 通过world: MAIN或chrome.scripting让脚本进入 MAIN World。安全通信– 使用令牌 来源校验 schema 验证防止消息伪造。最小权限– 只监听必要的消息类型及时清理监听器。掌握了 Bridge 模式你就拿到了 Chrome 扩展开发中“既要也要”的万能钥匙。无论是爬取动态渲染的页面数据还是深度定制网站行为都可以游刃有余。本文所有代码示例基于 Manifest V3Chrome 111 验证通过。遇到问题欢迎留言讨论。进一步阅读Chrome 官方文档Content ScriptsMDN: Window.postMessageUnderstanding Isolated Worlds