Manifest V3 Chrome扩展后台脚本消息传递报错解决方案
错误原因分析
你遇到的Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist错误由三个核心问题直接导致:
chrome.tabs.create是异步API,调用后浏览器需要完成标签页创建、HTML加载、JS执行全流程,你在调用创建接口后立刻执行发消息逻辑,此时popup.js的消息监听器还未完成注册,自然找不到接收端。- 现有逻辑没有判断popup.html是否已经打开,每次收到content script的消息都会新建一个标签页,哪怕你提前打开了popup页面,代码还是会创建新的未加载完成的标签页,第一次执行必然报错。
chrome.runtime.sendMessage默认会向所有扩展上下文广播消息,只要有一个上下文不存在可用接收端就会抛出Promise异常,你没有添加异常捕获逻辑,错误直接打到控制台。- 额外隐患:MV3的background service worker是事件驱动、随时可能被回收的,把待发送数据存在内存里可能出现数据丢失。
可直接运行的修复代码
修正后的background.js
// 缓存待传递数据,避免页面加载慢导致丢数 let pendingSelectedText = null; chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // 处理content script发来的选中文本 if (request.type === 'selectedText') { pendingSelectedText = request.data; // 先查询popup页面是否已经打开 chrome.tabs.query({url: chrome.runtime.getURL('popup.html')}, (tabs) => { if (tabs.length > 0) { // 已存在则直接向对应标签页发消息,加catch避免无接收端时报错 chrome.tabs.sendMessage(tabs[0].id, { type: 'bgForward', data: pendingSelectedText }).catch(err => console.log('消息发送异常:', err)); } else { // 不存在则新建标签页,等加载完成后再发消息 chrome.tabs.create({url: 'popup.html'}, (newTab) => { const tabLoadListener = (tabId, changeInfo) => { if (tabId === newTab.id && changeInfo.status === 'complete') { chrome.tabs.sendMessage(newTab.id, { type: 'bgForward', data: pendingSelectedText }).catch(err => console.log('消息发送异常:', err)); // 移除监听器避免内存泄漏 chrome.tabs.onUpdated.removeListener(tabLoadListener); } }; chrome.tabs.onUpdated.addListener(tabLoadListener); }); } }); return true; } // 处理popup页面初始化时拉取缓存数据的请求 if (request.type === 'getPendingData') { sendResponse({data: pendingSelectedText}); // 按需决定是否清空缓存 // pendingSelectedText = null; return true; } });
修正后的content.js
// 避免隐式全局变量 function getSelectionText() { let text = ""; if (window.getSelection) { text = window.getSelection().toString(); } else if (document.selection && document.selection.type != "Control") { text = document.selection.createRange().text; } return text; } function doc_keyUp(e) { const selectedText = getSelectionText(); if (selectedText && e.key === '1') { chrome.runtime.sendMessage({ type: 'selectedText', data: selectedText }).catch(err => console.log('向后台发送数据异常:', err)); } } console.log('The program has started!!!'); document.addEventListener('keyup', doc_keyUp, false);
修正后的popup.js
// 监听后台实时转发的消息 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'bgForward') { console.log("from popup = \n" + request.data); } }); // 页面加载完成后主动拉取缓存数据,避免加载过慢错过实时消息 chrome.runtime.sendMessage({type: 'getPendingData'}, (res) => { if (res?.data) { console.log("初始化拿到缓存数据 = \n" + res.data); } });
popup.html和manifest.json无需额外修改,保持原有结构即可正常运行。
附加问题解答
1. 能否跳过background直接从content script发消息到自定义HTML页面脚本?
没有直接通信的通道。content script运行在目标网页的隔离上下文,和扩展自有HTML页面(popup页、选项页、自定义标签页)的上下文完全隔离,不存在直连的通信端口。
如果不想写background转发逻辑,可以用chrome.storage做中转实现类似效果:
- content script拿到选中文本后直接写入
chrome.storage.local - 自定义HTML页面监听
chrome.storage.onChanged事件,数据更新时直接读取即可
这种方案不需要等页面提前打开,数据会持久化在存储中,页面打开后也能主动读取历史数据,也不会出现「接收端不存在」的报错。
2. MV3下能否不通过消息传递直接获取background内的变量?
没有直接获取的方法。MV3将后台上下文替换为Service Worker,和扩展页面运行在不同线程,且生命周期完全独立(随时可能被浏览器回收销毁),内存数据不共享,因此chrome.extension.getBackgroundPage()被彻底移除,没有等价API可以直接访问Service Worker的内存变量。
可行的替代方案只有两种:
- 将需要全局共享的变量写入
chrome.storage持久化,所有上下文直接读写存储即可 - 保持消息传递逻辑:其他页面向background发请求,background将变量值通过响应返回
注意不要把需要长期保留的数据存在Service Worker的内存中,Service Worker被回收时内存数据会直接丢失。
内容的提问来源于stack exchange,提问作者Consider Non-Trivial Cases
相关产品推荐
相关产品推荐

