强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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.sendMessageService 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 connectionContent Script 未注入先注入再发送消息
多个接收者响应冲突多个监听器都响应使用消息路由,只匹配一个处理器

9.8 扩展阅读