Chrome 扩展开发完全指南 / 第 9 章:消息通信(Messaging)
第 9 章:消息通信(Messaging)
Chrome 扩展的各个组件(Service Worker、Content Script、Popup、Options、Side Panel)运行在不同的上下文中,消息通信是它们之间协调工作的唯一方式。本章将全面讲解各种通信模式。
9.1 通信架构
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌───────────────┐ chrome.runtime ┌────────────────┐ │
│ │ Service Worker │◄──────.sendMessage──►│ Popup │ │
│ │ │ │ Options │ │
│ │ │◄──────.sendMessage──►│ Side Panel │ │
│ │ │ └────────────────┘ │
│ │ │ ┌────────────────┐ │
│ │ │◄──tabs.sendMessage──►│ Content Script │ │
│ └───────────────┘ └────────────────┘ │
│ ▲ │
│ │ chrome.runtime.connect │
│ │ (长连接端口) │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 其他扩展 / Native App │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
通信方式对比
| 方式 | 方向 | 连接类型 | 适用场景 |
|---|---|---|---|
runtime.sendMessage | 任何 → Service Worker | 一次性 | 简单请求-响应 |
tabs.sendMessage | Service Worker / Popup → Content Script | 一次性 | 控制 Content Script |
runtime.connect | 任何之间 | 长连接 | 实时通信、流数据 |
externally_connectable | 外部网站 → 扩展 | 一次性 | 网页与扩展通信 |
Native Messaging | 扩展 ↔ 本地应用 | 长连接 | 系统级操作 |
9.2 一次性消息
9.2.1 发送与接收
// 发送方(例如 Popup)
async function sendMessage(message) {
try {
const response = await chrome.runtime.sendMessage(message);
console.log('收到响应:', response);
return response;
} catch (error) {
console.error('发送失败:', error.message);
throw error;
}
}
// 接收方(Service Worker)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('收到消息:', message);
console.log('发送者:', sender);
// sender 包含:
// - sender.tab: 消息来自 Content Script 时的标签页信息
// - sender.id: 发送方扩展 ID
// - sender.url: 发送方页面 URL
// - sender.tlsChannelId: TLS 通道 ID
// 同步响应
if (message.type === 'GET_VERSION') {
sendResponse({ version: '1.0.0' });
return;
}
// 异步响应 — 必须 return true
if (message.type === 'FETCH_DATA') {
fetch(message.url)
.then(res => res.json())
.then(data => sendResponse({ success: true, data }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // 保持消息通道开放
}
});
9.2.2 发送到 Content Script
// 从 Service Worker / Popup 发送到 Content Script
async function sendToTab(tabId, message) {
try {
const response = await chrome.tabs.sendMessage(tabId, message);
return response;
} catch (error) {
// Content Script 可能未注入,需要先注入
console.warn('Content Script 未响应,尝试注入...');
await chrome.scripting.executeScript({
target: { tabId },
files: ['content/content.js']
});
return await chrome.tabs.sendMessage(tabId, message);
}
}
// 使用示例
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const result = await sendToTab(tab.id, {
type: 'EXTRACT_DATA',
selectors: ['h1', 'p', '.article']
});
9.2.3 消息路由模式
// Service Worker 中的消息路由器
class MessageRouter {
constructor() {
this.handlers = new Map();
chrome.runtime.onMessage.addListener(
(message, sender, sendResponse) => {
this.handle(message, sender, sendResponse);
return true; // 保持通道开放
}
);
}
on(type, handler) {
this.handlers.set(type, handler);
}
async handle(message, sender, sendResponse) {
const handler = this.handlers.get(message.type);
if (!handler) {
console.warn('未知消息类型:', message.type);
sendResponse({ error: 'Unknown message type' });
return;
}
try {
const result = await handler(message, sender);
sendResponse({ success: true, ...result });
} catch (error) {
sendResponse({ success: false, error: error.message });
}
}
}
// 使用
const router = new MessageRouter();
router.on('GET_TAB_INFO', async (message, sender) => {
const tab = await chrome.tabs.get(sender.tab?.id);
return { tab: { id: tab.id, url: tab.url, title: tab.title } };
});
router.on('SAVE_DATA', async (message) => {
await chrome.storage.local.set({ [message.key]: message.value });
return { saved: true };
});
router.on('API_REQUEST', async (message) => {
const response = await fetch(message.url, message.options);
const data = await response.json();
return { data };
});
9.3 长连接端口(Port Messaging)
9.3.1 建立连接
// 发起方(Content Script 或 Popup)
const port = chrome.runtime.connect({ name: 'my-connection' });
// 发送消息
port.postMessage({ type: 'subscribe', channel: 'updates' });
// 接收消息
port.onMessage.addListener((message) => {
console.log('收到端口消息:', message);
});
// 监听断开
port.onDisconnect.addListener(() => {
console.log('连接已断开');
if (chrome.runtime.lastError) {
console.error('错误:', chrome.runtime.lastError.message);
}
});
// 接收方(Service Worker)
chrome.runtime.onConnect.addListener((port) => {
console.log('新连接:', port.name);
port.onMessage.addListener(async (message) => {
switch (message.type) {
case 'subscribe':
handleSubscribe(port, message.channel);
break;
case 'unsubscribe':
handleUnsubscribe(port, message.channel);
break;
}
});
port.onDisconnect.addListener(() => {
cleanupPort(port);
});
});
9.3.2 发布-订阅模式
// Service Worker 中的 PubSub 系统
class PubSubHub {
constructor() {
this.channels = new Map(); // channel → Set<port>
this.ports = new Map(); // port → Set<channel>
chrome.runtime.onConnect.addListener((port) => {
this.handleConnection(port);
});
}
handleConnection(port) {
this.ports.set(port, new Set());
port.onMessage.addListener((message) => {
switch (message.action) {
case 'subscribe':
this.subscribe(port, message.channel);
break;
case 'unsubscribe':
this.unsubscribe(port, message.channel);
break;
case 'publish':
this.publish(message.channel, message.data);
break;
}
});
port.onDisconnect.addListener(() => {
this.cleanup(port);
});
}
subscribe(port, channel) {
if (!this.channels.has(channel)) {
this.channels.set(channel, new Set());
}
this.channels.get(channel).add(port);
this.ports.get(port)?.add(channel);
console.log(`端口订阅: ${channel} (总计: ${this.channels.get(channel).size})`);
}
unsubscribe(port, channel) {
this.channels.get(channel)?.delete(port);
this.ports.get(port)?.delete(channel);
}
publish(channel, data) {
const subscribers = this.channels.get(channel);
if (!subscribers) return;
const deadPorts = [];
for (const port of subscribers) {
try {
port.postMessage({ channel, data });
} catch (e) {
deadPorts.push(port);
}
}
// 清理已断开的端口
deadPorts.forEach(port => this.cleanup(port));
}
cleanup(port) {
const channels = this.ports.get(port);
if (channels) {
for (const channel of channels) {
this.channels.get(channel)?.delete(port);
}
this.ports.delete(port);
}
}
}
// 初始化
const pubsub = new PubSubHub();
// 客户端使用(Popup 或 Content Script)
class PubSubClient {
constructor() {
this.port = chrome.runtime.connect({ name: 'pubsub-client' });
this.listeners = new Map();
this.port.onMessage.addListener((message) => {
const handlers = this.listeners.get(message.channel);
handlers?.forEach(handler => handler(message.data));
});
}
subscribe(channel, callback) {
if (!this.listeners.has(channel)) {
this.listeners.set(channel, new Set());
this.port.postMessage({ action: 'subscribe', channel });
}
this.listeners.get(channel).add(callback);
}
unsubscribe(channel) {
this.listeners.delete(channel);
this.port.postMessage({ action: 'unsubscribe', channel });
}
publish(channel, data) {
this.port.postMessage({ action: 'publish', channel, data });
}
disconnect() {
this.port.disconnect();
}
}
// 使用
const client = new PubSubClient();
client.subscribe('page-updates', (data) => {
console.log('页面更新:', data);
});
client.subscribe('notifications', (data) => {
showNotification(data.message);
});
9.4 外部通信
9.4.1 扩展间通信
// manifest.json — 接收外部消息
{
"externally_connectable": {
"matches": [
"https://myapp.example.com/*",
"https://partner-site.com/*"
],
"ids": ["other-extension-id"]
}
}
// 接收外部消息的扩展
chrome.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
// sender.url 是外部网站或扩展的 URL
console.log('外部消息来自:', sender.url);
if (message.type === 'AUTH_REQUEST') {
// 验证来源
if (!sender.url?.startsWith('https://myapp.example.com')) {
sendResponse({ error: 'Unauthorized origin' });
return;
}
handleAuthRequest(message).then(sendResponse);
return true;
}
}
);
// 发送外部消息(从网页或另一个扩展)
const extensionId = 'abcdefghijklmnopabcdefghijklmnop';
// 从另一个扩展发送
chrome.runtime.sendMessage(extensionId, {
type: 'AUTH_REQUEST',
token: 'user-token-123'
}, (response) => {
console.log('认证结果:', response);
});
// 从网页发送
chrome.runtime.sendMessage(extensionId, {
type: 'GET_DATA'
}, (response) => {
console.log('扩展数据:', response);
});
9.4.2 Native Messaging
// 连接本地应用
const port = chrome.runtime.connectNative('com.my_company.my_app');
port.onMessage.addListener((message) => {
console.log('来自本地应用:', message);
});
port.onDisconnect.addListener(() => {
console.log('本地应用连接断开');
if (chrome.runtime.lastError) {
console.error('错误:', chrome.runtime.lastError);
}
});
// 发送消息
port.postMessage({ command: 'readFile', path: '/tmp/data.txt' });
Native Host 配置文件(com.my_company.my_app.json):
{
"name": "com.my_company.my_app",
"description": "My Native App",
"path": "/usr/local/bin/my-app",
"type": "stdio",
"allowed_origins": [
"chrome-extension://abcdefghijklmnopabcdefghijklmnop/"
]
}
9.5 广播消息
// 向所有打开的扩展页面广播消息
async function broadcast(message) {
const contexts = await chrome.runtime.getContexts({
contextTypes: ['POPUP', 'SIDE_PANEL', 'OFFSCREEN_DOCUMENT']
});
// 发送到各 UI 页面
for (const context of contexts) {
try {
chrome.runtime.sendMessage(message);
} catch (e) {
// 忽略已关闭的页面
}
}
// 发送到所有标签页的 Content Script
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
try {
await chrome.tabs.sendMessage(tab.id, message);
} catch (e) {
// 忽略没有 Content Script 的标签页
}
}
}
// 使用
broadcast({ type: 'SETTINGS_CHANGED', settings: newSettings });
9.6 业务场景
场景一:实时数据同步
// Service Worker:保持 WebSocket 连接
class RealtimeSync {
constructor() {
this.ws = null;
this.reconnectTimer = null;
this.clients = new Set();
}
connect() {
this.ws = new WebSocket('wss://api.example.com/ws');
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.notifyClients({ type: 'REALTIME_DATA', data });
};
this.ws.onclose = () => {
this.reconnectTimer = setTimeout(() => this.connect(), 5000);
};
}
notifyClients(message) {
for (const port of this.clients) {
try {
port.postMessage(message);
} catch (e) {
this.clients.delete(port);
}
}
}
addClient(port) {
this.clients.add(port);
port.onDisconnect.addListener(() => this.clients.delete(port));
}
}
const sync = new RealtimeSync();
sync.connect();
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'realtime') {
sync.addClient(port);
}
});
9.7 注意事项
| 问题 | 原因 | 解决方案 |
|---|---|---|
sendResponse 未收到 | 忘记 return true | 异步处理时必须 return true |
| 消息发送失败 | 目标不存在或已关闭 | try-catch 捕获错误 |
| 连接立即断开 | Service Worker 被终止 | 实现重连机制 |
Could not establish connection | Content Script 未注入 | 先注入再发送消息 |
| 多个接收者响应冲突 | 多个监听器都响应 | 使用消息路由,只匹配一个处理器 |