简介:这是一份Chrome插件开发demo,示例名为chrome-ctx-msg,聚焦V3版本中不同JavaScript环境之间的通信,适合有基础前端知识、想上手浏览器扩展开发的读者;通过阅读代码可掌握扩展开发中的消息驱动设计思路。Chrome插件里背景脚本、内容脚本、弹出窗口脚本与选项页脚本运行在不同沙箱内,无法直接互相访问;该demo围绕这一难题,演示如何借助chrome.runtime.sendMessage、onMessage以及chrome.storage API完成跨脚本消息传递与数据共享,也覆盖了消息监听的注册与storage异步读写的基本写法。压缩包共11个文件,包含manifest.json、background.js、content_script.js、popup.js等核心文件,搭配HTML、CSS和多个尺寸的图标PNG,整体仅43KB,结构清晰便于快速查阅。目前已有525人学习,适合对照源码理清内容脚本与背景脚本之间“发送—接收—回传”的完整链路,也能看到popup与background互通方式,可作为后续开发工具栏按钮、网页内容注入、选项页配置等功能的起步模板。
1. 还没动手就被卡住:Chrome 插件里根本不止一种 JS
做 Chrome 插件最反直觉的一件事,就是你写的 JS 可能根本不和你写的另一段 JS 在一个世界里。一个最小的 chrome 插件 demo 往往包含 popup、content script 和 background(现在是 service worker),它们各自运行在独立的 JavaScript 上下文里:页面脚本拿不到插件脚本的变量,插件脚本也碰不到页面里的 window 对象。第一次做这方向的人基本都会在这里翻车——明明都是 JS,怎么就互相找不到。
这个标题拆开看有两层:一是在 Chrome 插件内部,popup、background、content script 之间如何通信;二是 content script 和网页自身的 JS 之间如何打通。后者往往才是真正交付时要解决的问题,比如你想随网页操作自动填充数据、采集表单、接管按钮点击事件,所有这些都绕不过“跨上下文”这一步。这篇文章就是把这套东西按可复现的最小 demo 讲清楚,适合刚接触 chrome 插件开发、或者被各种 message 报错折磨过的前端和爬虫方向从业者。
2. 三个 JS 环境各管一摊:先搞清楚谁和谁天然隔离
2.1 popup、content script、background 的分工与隔离边界
Chrome 插件常见的三种脚本,职责完全不同。popup 是点击工具栏图标弹出的面板,它有自己的 HTML 和 JS,只要面板一关,里面的变量和状态全部销毁。background(manifest V3 里叫 service worker)是插件级别的常驻逻辑,负责跨标签页协调、处理菜单事件、发起网络请求、维护全局状态,但它和页面没有直接 DOM 关联。content script 是注入到网页里的脚本,能操作页面 DOM、监听页面事件,但它运行在一个独立的“隔离世界”(isolated world)里,拿不到页面自身的 JS 变量,window 上的属性也和你直接打开 DevTools 控制台看到的不是同一个。
这三者之间的隔离是浏览器安全模型决定的,不是设计缺陷。content script 与页面共享 DOM,但不共享 JS 执行环境;popup、background、content script 彼此也不共享作用域。所以“通信”不是可选项,而是必选项。任何跨越这些边界的操作,都要走消息通道(messaging)。
下面是一个最基础的 manifest.json,它同时声明了 background 和 content script,这是后面所有通信 demo 的骨架:
{
"manifest_version": 3,
"name": "js-messaging-demo",
"version": "1.0",
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"permissions": ["activeTab", "scripting"]
}
这段配置的关键在两点: action.default_popup 指向 popup 的 HTML 文件, content_scripts 声明了注入目标和脚本文件。 matches 用 <all_urls> 表示对所有网页注入,实际开发里建议收窄到自己的目标站点,否则每个页面都会挂一个脚本,占内存也容易出权限问题。 service_worker 在 MV3 里替代了 MV2 的 background page,它不是一直活着的进程,而是事件驱动的,没有事件时会被浏览器回收,这一点后面会踩坑。
2.2 一张表看懂三类消息通道怎么选
Chrome 插件通信可以分四种典型路径:popup 到 content script、popup 到 background、background 到 content script、content script 之间的桥接。每条路径用的 API 不同,语义也不同。
| 通信路径 | 发送 API | 接收 API | 备注 |
|---|---|---|---|
| popup / background 发给 content script | chrome.tabs.sendMessage(tabId, message) | chrome.runtime.onMessage | 需要知道 tabId,通过 chrome.tabs.query 获取 |
| content script 发给 background | chrome.runtime.sendMessage(message) | chrome.runtime.onMessage | 由 background 监听 |
| popup 发给 background | chrome.runtime.sendMessage(message) | chrome.runtime.onMessage | 同一个 channel,background 区分来源即可 |
| background 发广播给所有 content script | chrome.tabs.query 拿到所有 tab,逐个 tabs.sendMessage | chrome.runtime.onMessage | 常见做法是遍历 |
| content script 与页面 JS 通信 | window.postMessage(message, "*") | window.addEventListener("message", ...) | 唯一的跨隔离世界通道 |
这张表的核心结论是: 插件内部的 JS 之间用 runtime.sendMessage + tabs.sendMessage 这一套,插件和页面之间用 window.postMessage 。前者是插件系统的内部总线,后者是 HTML5 标准的跨窗口通信协议。两个体系不要混用。
理解这一层之后,还要注意回调的写法。MV3 底层是 Promise 风格的 API,但很多网上代码还在用 MV2 的回调式写法。两种写法都能跑,但混在一起特别容易出问题。我一般统一用 async/await,错误处理更清晰。
3. 跑通第一套通信:popup 和 content script 互发消息
3.1 从 popup 主动发消息给页面:查 tab、发消息、收响应
最常见的需求是:用户点击 popup 里的按钮,然后把消息发给当前页面的 content script,由它去操作 DOM。先写 popup 的 HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { width: 200px; padding: 12px; }
button { width: 100%; margin: 4px 0; }
</style>
</head>
<body>
<button id="highlightBtn">高亮页面标题</button>
<button id="extractBtn">提取页面文本</button>
<div id="result"></div>
<script src="popup.js"></script>
</body>
</html>
然后 popup 侧的 JS:
// popup.js
async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab;
}
async function sendToContent(message) {
const tab = await getActiveTab();
// tab.id 存在且是 http/https 页面时才发送,防止发到 chrome:// 页面报错
if (!tab.id || !/^https?:/.test(tab.url)) return;
const response = await chrome.tabs.sendMessage(tab.id, message);
return response;
}
document.getElementById("highlightBtn").addEventListener("click", async () => {
const resp = await sendToContent({ type: "HIGHLIGHT_TITLE" });
document.getElementById("result").textContent = resp ? `页面响应:${resp.result}` : "无响应";
});
document.getElementById("extractBtn").addEventListener("click", async () => {
const resp = await sendToContent({ type: "EXTRACT_TEXT" });
document.getElementById("result").textContent = resp ? `提取到 ${resp.result.length} 个字符` : "无响应";
});
这里有个细节值得注意: chrome.tabs.query 返回的是一个数组,取第一个元素就是当前活动标签页。 sendMessage 如果发送成功,会返回一个 Promise,resolve 的值是 content script 里 sendResponse 返回的内容。如果 content script 没有监听器,或者监听器没有调用 sendResponse ,这里拿到的是 undefined——这是新手最常见的“发了没回”问题的来源。
再看 content script 一侧的接收逻辑:
// content.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "HIGHLIGHT_TITLE") {
const title = document.title;
document.title = `【已高亮】${title}`;
sendResponse({ result: "标题已修改,原标题:" + title });
} else if (message.type === "EXTRACT_TEXT") {
const text = document.body.innerText;
sendResponse({ result: text });
}
// 注意:这里的 sendResponse 是同步调用的
// 如果有异步操作,需要 return true 标记为异步响应
return true;
});
逻辑说明: onMessage 的监听器接收三个参数, message 是发送方传来的对象, sender 包含发送方的 tab 信息, sendResponse 是回传通道。 如果你的处理函数里没有异步操作,直接调用 sendResponse 即可;如果有 await,必须在函数里 return true 把消息通道保持打开 ,否则 sendResponse 会在函数执行完前就被回收,接收方只能拿到 undefined。
这里还有个脏坑: action.default_popup 的弹窗一旦打开,popup 里的 JS 就会一直运行,直到你手动关闭弹窗。如果你的按钮点击逻辑里有耗时操作,界面可能会看起来卡住,因为 popup 页面是同步阻塞的渲染进程。常见的做法是把耗时操作丢到 background 去做,popup 只做发送和展示。
3.2 反过来走通:content script 主动发消息给 popup
反过来的场景也很多:页面里发生了某个交互事件,需要弹窗展示状态,但 popup 是用户手动打开的才有实例。这里有个关键事实: content script 向 popup 发消息,本质上发到的是 runtime,而不是 popup 实例 。如果 popup 没打开,消息就没人接收,不会报错,也不会排队。想要让 popup 响应 content script 的需求,你得保证 popup 是打开的,否则只能把逻辑下沉到 background。
content script 发消息的代码很简单:
// content.js 内部
function notifyPopup() {
chrome.runtime.sendMessage({ type: "PAGE_CLICKED", data: window.location.href }, (response) => {
if (chrome.runtime.lastError) {
// 这里必然会出现 "Could not establish connection. Receiving end does not exist."
// 意思是 popup 没开,或者没有监听器
console.warn("popup 不在线");
return;
}
console.log("收到 popup 响应:", response);
});
}
document.addEventListener("click", () => {
notifyPopup();
});
报错 Could not establish connection. Receiving end does not exist. 是三条最典型的 chrome 插件报错之一,另外两条是 Extension context invalidated 和 Service worker registration failed 。这条错误最常见的两个原因:一就是接收方不存在(popup 没打开、background 没起来),二是发送的消息里带了无法序列化的内容(比如函数、DOM 节点)。
那我怎么在连不上 popup 的情况下还不崩呢?用 chrome.runtime.lastError 兜底判断。这个 API 是 Chrome 的约定:回调式 API 出错时,不会抛出异常,而是把错误挂在 chrome.runtime.lastError 上。你不检查它,Chrome 就会在控制台里输出一段 uncaught error。很多人第一次遇到这个报错以为自己的消息格式写错了,其实只是没处理失败分支。
3.3 长连接场景:port 通信何时优于一次性消息
上面聊的都是“发一次、回一次”的短连接。如果两个 JS 环境之间需要持续交换数据(比如 content script 实时上报鼠标位置,或者播放进度变化),就该用长连接了,对应 API 是 chrome.runtime.connect 。
// background.js
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== "monitor") return;
port.onMessage.addListener((msg) => {
console.log("收到长连接消息:", msg);
port.postMessage({ type: "ACK", ts: Date.now() });
});
port.onDisconnect.addListener(() => {
console.log("长连接断开");
});
});
// content.js
const port = chrome.runtime.connect({ name: "monitor" });
port.postMessage({ type: "START", url: location.href });
port.onMessage.addListener((msg) => {
if (msg.type === "ACK") {
console.log("收到后台确认", msg.ts);
}
});
长连接相比一次性消息的差别在于:建立连接后,双方都可以随时主动发消息,不需要每次重新握手;而且端口对象是持久的引用,内部状态可以在这个连接的生命周期内维护,比如累计上报多少次、记录连接的起始时间。代价是它占资源,如果页面每帧都 postMessage,background 根本忙不过来,这种场景应该做节流,比如 500ms 合并一次。
4. 补齐各类型 JS 通信的完整拼图:background、content 与页面脚本的消息链路
4.1 background 作为“消息中转站”:跨标签页通信的常见做法
当两个 content script 之间要通信,比如在标签页 A 上采集的数据要传回标签页 B 的页面里,直接互相发是不可能的,常见的做法是让 background 当中转站:content script A 发给 background,background 再转给 content script B。
// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "BROADCAST_TO_TABS") {
// sender.tab.id 是来源标签页,不要转发回来源页
chrome.tabs.query({}, (tabs) => {
tabs.forEach((tab) => {
if (tab.id === sender.tab && tab.id) return;
chrome.tabs.sendMessage(tab.id, { type: message.type, payload: message.payload }, () => {
// 如果目标 tab 没有 content script,这里会报错,忽略即可
if (chrome.runtime.lastError) {
// 吞掉目标页没有监听器的错误
}
});
});
});
sendResponse({ ok: true });
}
});
这里的判断有一个易错点:遍历 tab 时,不能只判断 tab.id === sender.tab.id ,因为 sender 可能来自 popup 或 background 自身,它们没有 tab 属性,取 sender.tab.id 会抛 TypeError。更稳妥的做法是先判断 sender.tab && sender.tab.id 。跨标签页消息很容易被自己的逻辑搞混,我的建议是在 message 结构里统一加上 source 字段,标明消息是哪个模块发出来的,方便调试链路。
4.2 直接在页面 JS 和 content script 之间通信:content script 注入到页面环境的 window.postMessage
如果需求是让网页自身的脚本主动和插件通信,比如页面上有个按钮在用户点击后想让插件去保存页面数据,那 content script 得在页面里加一个监听器,两者靠 window.postMessage 对话。
// content.js
// 监听页面发来的消息
window.addEventListener("message", (event) => {
// 必须校验 origin,否则任意网站都可以往这个 window 里发消息
if (event.source !== window) return;
if (event.data && event.data.source === "PAGE_SCRIPT") {
console.log("页面脚本发来:", event.data.payload);
// 在这里重新发到 background 或 popup
chrome.runtime.sendMessage({ type: "FROM_PAGE", payload: event.data.payload });
}
});
// 向页面脚本发送消息
function dispatchToPage(data) {
window.postMessage(
{ source: "CONTENT_SCRIPT", payload: data, _ts: Date.now() },
"*"
);
}
dispatchToPage({ action: "PING" });
然后页面脚本侧的监听逻辑是:
// 这个 JS 通过 <script src> 或 SPA 的构建流程直接注入到网页里
window.addEventListener("message", (event) => {
if (event.data && event.data.source === "CONTENT_SCRIPT") {
console.log("插件给我发消息了:", event.data.payload);
// 页面脚本可以在这里调用页面自己的函数、访问自己的全局变量
window.postMessage({ source: "PAGE_SCRIPT", payload: { ok: true } }, "*");
}
});
这一段的细节很重要:事件对象 event.source 要判断是不是当前 window。如果不判断,页面里引入的第三方 iframe 发来的消息也会被监听器捕获,你无法区分消息来源。 targetOrigin 参数用了 "*" ,表示不限制接收方,如果你的页面本身是 http/https 的固定站点,可以写成 location.origin 更安全。
还有一个常见的误用:有的开发者想着在 content script 里直接 eval 页面上的 JS 函数。这是行不通的,因为 content script 运行在隔离世界,即使拿到函数名字符串也没有对应的函数引用。除非你在 content script 里往页面注入 <script> 标签,把一段真实的 JavaScript 代码挂到页面上下文里执行——这确实是可行的,而且不用 eval 。特殊场景下可以用 chrome.scripting.executeScript 的 func 参数把一个函数序列化后在页面上下文执行。
4.3 通过 chrome.scripting.executeScript 动态注入并传参
上一种场景的前提是 content script 已经存在于页面里了。但有些需求是“页面已经加载完,用户点击 popup 后我才想临时往这个页面注入一段 JS”,这时不需要声明 content_scripts,而是在 popup 里动态注入:
// popup.js
async function injectAndRun(tabId) {
const results = await chrome.scripting.executeScript({
target: { tabId },
func: (url) => {
// 这个函数会序列化后在目标页面的隔离世界执行
// 可以操作 DOM,但不能访问页面 JS 变量
return {
title: document.title,
url: url,
bodyLength: document.body.innerText.length
};
},
args: ["http://example.com"]
});
return results[0] && results[0].result;
}
func 里拿到的不是页面 JS 效果,而是在 content script 隔离世界里运行的效果——和声明式 content script 的可见能力是一样的。但好处是它在执行时可以传参( args ),返回值可以直接拿回来用,不需要额外走一遍消息通道。如果你在 func 里想访问页面自己的全局变量,比如页面用 window.__pagename 存了一个变量,在 executeScript 里同样拿不到,这跟 content script 的隔离是同一个原因。
真正想访问页面自己的变量,还得回到 window.postMessage 或者往页面注入 script 标签。动态注入适合的是“临时采集信息 + 修改 DOM”这一类,不涉及页面 JS 上下文的场景。
5. 常见通信故障避坑:五个翻车现场和对应解法
5.1 popup 一关就失联:消息发到 closed popup 上没有任何回执
现象:popup 打开时功能正常,一关掉再通过 background 发状态消息就杳无音信。
原因:popup 的 JS 上下文在面板关闭时销毁,已经注册的 onMessage 监听器跟着没了。Chrome 不会帮你暂存消息,丢了就是丢了。
解决:涉及全局状态的消息一律发给 background,由 background 维护状态副本;popup 打开时从 background 拉取当前状态来渲染,而不是依赖推送。popup 本质是一个“可随时销毁的视图”,不要在里面放任何业务逻辑。
5.2 MV3 service worker 被回收导致的掉线:onMessage 没注册上
现象:background 里的 onMessage 监听器有时候能收到消息,有时候收不到。打开扩展管理页的 Inspect 面板看,service worker 状态是 inactive 甚至停止。
原因:MV3 的 service worker 是事件驱动的,空闲 30 秒左右就会被 Chrome 终止。它重新醒来的时候会重新执行 background.js,理论上监听器会重新注册,但如果你的 background.js 顶层有异步初始化逻辑,比如 fetch 远程配置后再 addListener,那监听器的注册就存在竞态条件。
解决:把 chrome.runtime.onMessage.addListener 放在 background.js 顶层,不要包在 promise 或 setTimeout 里。同时注意,service worker 被回收后,内存中的全局状态会全部丢失,需要持久化到 chrome.storage 或 IndexedDB。如果你在 background 里维护了一个大型对象,等到下次用的时候发现它变成空对象,别惊讶,这是 MV3 的机制。
5.3 发送对象包含函数或 DOM 节点导致序列化失败
现象:点击按钮发送消息后,控制台报 Error in invocation of tabs.sendMessage: Error at parameter 2: Invalid value ,消息发送不了。
原因:消息通道不是内存引用传递,而是结构化克隆(structured clone)序列化。函数、DOM Element、WeakMap、带循环引用的对象都过不了这个序列化。
解决:发送前把数据纯净化,只保留 JSON 兼容的数据结构。如果你的对象里有 DOM 元素,先把需要的属性抽出来,比如 innerText 、 href 、 dataset ,不要贪图方便直接传节点对象。采集大段 DOM 内容时尤其注意,用 document.body.innerText 抽取要比传一个巨大的 HTML 节点安全得多。
5.4 content script 已经注入但监听器没触发:常见于 iframe 页面
现象:主页面里 content script 正常,但有些弹窗、富文本编辑器、广告位是在 iframe 里的,content script 进不去,消息也发不到。
原因:content script 默认只在顶层 window 注入,iframe 内的文档是独立的 document,不会自动注入。你发消息时用的是顶层 tab id,iframe 里的脚本不在接收方。
解决:在 manifest 的 content_scripts 声明里加 "all_frames": true ,这样 iframe 里的文档也会注入一份 content script。此时要注意,同一条消息可能被多个 frame 的监听器收到,需要在 content script 里判断 window.self === window.top 来决定是否真的处理,避免重复执行。如果只是想在主 frame 执行,加这个判断;如果所有 frame 都要执行,就得用 sender 信息里的 frameId 区分来源了。
5.5 消息确实发出去但响应迟迟不回来:sendResponse 的异步陷阱
现象:content script 的 onMessage 里做了 await ,但 popup 等到的响应永远是 undefined,没有任何报错。
原因: onMessage 的回调函数如果没有显式 return true ,Chrome 认为你是同步处理,回调结束后通道立即关闭,sendResponse 就算之后被调用也传不回去。
解决:处理函数里有异步操作时,加一行 return true :
// content.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "FETCH_DATA") {
(async () => {
const data = await someAsyncFunction();
sendResponse({ data });
})();
return true; // 标记异步,保持通道存活
}
});
这条规则是 chrome 插件通信里最深、最隐蔽的坑,因为报错不出现,纯粹是行为不对。如果你要维护比较老的代码库,见到那种回调里套 setTimeout 然后没 return true 的代码,基本可以断定是同一个问题。顺手再提醒一句: return true 必须放在顶层同步代码里,不能放在 async 分支内,否则没用。
6. 从 demo 走向“可调试”的通信工具:给消息加追踪层、写日志轮盘与验证手段
6.1 消息追踪层:用统一包装函数替代裸的 sendMessage
真实插件项目里的消息类型多了以后,裸写 sendMessage 会让调用链变成一团乱麻。我一般会在项目里维护一个独立的 messenger.js 模块,把消息的发送和接收都统一包装起来,顺手带上日志和错误上报:
// messenger.js
const tag = `[${new Date().toISOString()}]`;
function log(direction, type, payload) {
console.log(tag, direction, { type, payload });
}
export function sendMessageToBackground(message) {
log("content -> background", message.type, message.payload);
return chrome.runtime.sendMessage(message);
}
export function sendMessageToTab(tabId, message) {
log("background -> tab:" + tabId, message.type, message.payload);
return chrome.tabs.sendMessage(tabId, message);
}
export function onMessage(handler) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
log("incoming", message.type, message.payload);
const shouldReply = handler(message, sender, sendResponse);
return shouldReply === true ? true : undefined;
});
}
调用方只关心业务,日志统一在模块里打,排查问题时打开 background 的 Inspect 面板能看到完整链路。这个追踪层在联调阶段能省大量时间,因为消息通信的 bug 几乎都是链路问题,挂上日志就能定位是哪一段断了。
6.2 通信链路自检验证:用事件驱动的方式做集成测试
demo 阶段可以写一段自检脚本,在 background 启动的时候依次发预设消息,检查每条链路是否可达,结果上报到 chrome.storage ,popup 打开时读取展示:
// selfcheck.js(加载到 background 里)
async function runSelfCheck() {
const results = [];
try {
await chrome.runtime.sendMessage({ type: "PING" });
results.push({ channel: "runtime", ok: true });
} catch (e) {
results.push({ channel: "runtime", ok: false, error: e.message });
}
const [tab] = await chrome.tabs.query({ active: true });
if (tab && tab.id) {
try {
const resp = await chrome.tabs.sendMessage(tab.id, { type: "PING" });
results.push({ channel: "tabs", ok: resp && resp.pong === true });
} catch (e) {
results.push({ channel: "tabs", ok: false, error: e.message });
}
} else {
results.push({ channel: "tabs", ok: false, error: "no active tab" });
}
await chrome.storage.local.set({ lastSelfCheck: results });
return results;
}
在网页环境里验证 content script 到页面脚本的通道,跑集成测试相对麻烦,因为需要真实打开一个页面,但至少 runtime 和 tabs 两条主链路能通过这个脚本快速覆盖。跑完看一眼 chrome.storage.local.get("lastSelfCheck") 就能判断插件状态,比靠人肉点来点去靠谱得多。
6.3 收尾的一个习惯:消息类型统一维护成一个常量表
做到这一步,再回头看你写的消息结构,大概率会遇到一个问题:字符串 type 散落在各处,编辑器里全局搜索能搜出一堆,但分不清哪些是发送方在用、哪些是接收方在监听。我在项目里会把消息类型集中到一个常量对象里:
// message-types.js
export const MessageType = {
PING: "PING",
PONG: "PONG",
HIGHLIGHT_TITLE: "HIGHLIGHT_TITLE",
EXTRACT_TEXT: "EXTRACT_TEXT",
PAGE_CLICKED: "PAGE_CLICKED",
FROM_PAGE: "FROM_PAGE",
BROADCAST_TO_TABS: "BROADCAST_TO_TABS"
};
发送方和接收方都引用这个常量,拼错字符串的概率直接归零。消息的 payload 结构尽量用点表示法,比如 payload.user.id 、 payload.article.url ,不要搞一堆一层套一层的嵌套对象,打印日志时看不出重点。
做了十几年前端,Chrome 插件里的消息通信大概是我见过最容易“看着简单、一用就崩”的领域。很多问题不是逻辑不清晰,而是对“隔离世界”这个底层模型没有足够敬畏。吃掉这个教训之后,我的习惯是每写一个通信链路就先跑一边自检脚本,确认通了再继续往下开发,而不是一口气写完所有模块再来联调。希望这组思路能帮你在自己的调试过程里少熬几个夜。
本文系统解析Chrome扩展Manifest V3中popup、content script与background/service worker三类脚本间的隔离机制与消息通信方案,涵盖chrome.runtime.sendMessage/onMessage、port长连接、window.postMessage跨页面通信及chrome.scripting.executeScript动态注入等核心API;重点说明MV3下service worker回收、sendResponse异步陷阱、序列化限制、iframe注入等典型故障的成因与规避方法,并提供可调试的消息追踪层与链路自检实践。
3500

被折叠的 条评论
为什么被折叠?



