Chapter 08 Messaging Mechanisms

Haiyue
38min

Chapter 8: Messaging Mechanisms

Learning Objectives

  1. Understand Chrome extension messaging architecture
  2. Master communication methods between different components
  3. Learn to handle asynchronous messages and errors
  4. Implement efficient data transmission strategies

8.1 Messaging Overview

Chrome extension messaging mechanisms serve as bridges connecting various components, enabling communication between background scripts, content scripts, popup, and options pages.

8.1.1 Messaging Architecture

🔄 正在渲染 Mermaid 图表...

tip Communication Characteristics

  • JSON-based message format
  • Asynchronous communication pattern
  • Supports request-response pattern
  • Can transfer complex data structures
  • Has error handling mechanisms

8.1.2 Message Type Classification

// Message type definitions
const MessageTypes = {
  // One-time messages
  SIMPLE_REQUEST: 'simple_request',

  // Long-lived connection messages
  PORT_CONNECT: 'port_connect',

  // Internal extension communication
  INTERNAL_COMMAND: 'internal_command',

  // External communication
  EXTERNAL_REQUEST: 'external_request'
};

// Message structure standard
const MessageStructure = {
  action: 'string',      // Action type
  data: 'any',          // Data payload
  timestamp: 'number',   // Timestamp
  requestId: 'string',   // Request ID (optional)
  source: 'string'       // Source identifier
};

8.2 One-time Message Passing

8.2.1 Basic Message Sending

// background.js - Message listener
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('Received message:', message, 'from:', sender);

  switch (message.action) {
    case 'getUserData':
      handleGetUserData(message, sendResponse);
      return true; // Keep message channel open

    case 'updateSettings':
      handleUpdateSettings(message.data, sendResponse);
      return true;

    case 'performAction':
      handlePerformAction(message, sender, sendResponse);
      return true;

    default:
      sendResponse({
        success: false,
        error: 'Unknown action type'
      });
  }
});

// Handle get user data
async function handleGetUserData(message, sendResponse) {
  try {
    const userData = await chrome.storage.sync.get(['userProfile', 'preferences']);

    sendResponse({
      success: true,
      data: {
        profile: userData.userProfile,
        preferences: userData.preferences
      }
    });
  } catch (error) {
    sendResponse({
      success: false,
      error: error.message
    });
  }
}

// Handle settings update
async function handleUpdateSettings(settings, sendResponse) {
  try {
    await chrome.storage.sync.set(settings);

    // Notify other components that settings have been updated
    chrome.tabs.query({}, (tabs) => {
      tabs.forEach(tab => {
        chrome.tabs.sendMessage(tab.id, {
          action: 'settingsUpdated',
          data: settings
        }).catch(() => {
          // Ignore tabs that can't receive messages
        });
      });
    });

    sendResponse({ success: true });
  } catch (error) {
    sendResponse({
      success: false,
      error: error.message
    });
  }
}

8.2.2 Sending Messages from Content Script

// content-script.js
class ContentScriptMessenger {
  constructor() {
    this.requestCounter = 0;
    this.pendingRequests = new Map();
  }

  // Generate request ID
  generateRequestId() {
    return `req_${++this.requestCounter}_${Date.now()}`;
  }

  // Send message and return Promise
  sendMessage(action, data = null) {
    return new Promise((resolve, reject) => {
      const requestId = this.generateRequestId();
      const message = {
        action,
        data,
        requestId,
        timestamp: Date.now(),
        source: 'content-script'
      };

      // Set timeout
      const timeout = setTimeout(() => {
        this.pendingRequests.delete(requestId);
        reject(new Error('Message send timeout'));
      }, 10000); // 10 second timeout

      this.pendingRequests.set(requestId, { resolve, reject, timeout });

      chrome.runtime.sendMessage(message, (response) => {
        const request = this.pendingRequests.get(requestId);

        if (request) {
          clearTimeout(request.timeout);
          this.pendingRequests.delete(requestId);

          if (chrome.runtime.lastError) {
            reject(new Error(chrome.runtime.lastError.message));
          } else if (response && response.success === false) {
            reject(new Error(response.error || 'Operation failed'));
          } else {
            resolve(response);
          }
        }
      });
    });
  }

  // Send messages in batch
  async sendBatchMessages(messages) {
    const promises = messages.map(msg =>
      this.sendMessage(msg.action, msg.data)
    );

    try {
      const results = await Promise.allSettled(promises);
      return results.map((result, index) => ({
        index,
        success: result.status === 'fulfilled',
        data: result.status === 'fulfilled' ? result.value : null,
        error: result.status === 'rejected' ? result.reason.message : null
      }));
    } catch (error) {
      throw new Error(`Batch message send failed: ${error.message}`);
    }
  }

  // Listen for messages from background
  setupMessageListener() {
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      console.log('Content script received message:', message);

      switch (message.action) {
        case 'settingsUpdated':
          this.handleSettingsUpdate(message.data);
          sendResponse({ received: true });
          break;

        case 'pageAction':
          this.handlePageAction(message.data, sendResponse);
          return true;

        case 'extractData':
          this.handleDataExtraction(message.data, sendResponse);
          return true;

        default:
          sendResponse({ error: 'Unhandled message type' });
      }
    });
  }

  handleSettingsUpdate(settings) {
    // Apply new settings to page
    console.log('Applying new settings:', settings);

    if (settings.theme) {
      this.applyTheme(settings.theme);
    }

    if (settings.fontSize) {
      this.adjustFontSize(settings.fontSize);
    }
  }

  async handlePageAction(actionData, sendResponse) {
    try {
      let result;

      switch (actionData.type) {
        case 'highlight':
          result = this.highlightText(actionData.text);
          break;

        case 'extract':
          result = this.extractPageData(actionData.selector);
          break;

        case 'modify':
          result = this.modifyContent(actionData.changes);
          break;

        default:
          throw new Error('Unknown page action type');
      }

      sendResponse({ success: true, result });
    } catch (error) {
      sendResponse({ success: false, error: error.message });
    }
  }

  extractPageData(selector) {
    const elements = document.querySelectorAll(selector || '*');
    const data = [];

    elements.forEach((element, index) => {
      if (index < 100) { // Limit quantity to avoid excessive data
        data.push({
          tag: element.tagName.toLowerCase(),
          text: element.textContent.substring(0, 200),
          attributes: this.getElementAttributes(element)
        });
      }
    });

    return {
      count: elements.length,
      data: data,
      url: window.location.href,
      title: document.title
    };
  }

  getElementAttributes(element) {
    const attrs = {};
    for (let attr of element.attributes) {
      attrs[attr.name] = attr.value;
    }
    return attrs;
  }
}

// Initialize message handler
const messenger = new ContentScriptMessenger();
messenger.setupMessageListener();

// Usage example
async function exampleUsage() {
  try {
    // Get user data
    const userData = await messenger.sendMessage('getUserData');
    console.log('User data:', userData);

    // Update settings
    await messenger.sendMessage('updateSettings', {
      theme: 'dark',
      autoSave: true
    });

    // Send messages in batch
    const batchResults = await messenger.sendBatchMessages([
      { action: 'getStats', data: null },
      { action: 'checkUpdates', data: null },
      { action: 'syncData', data: { force: true } }
    ]);

    console.log('Batch operation results:', batchResults);

  } catch (error) {
    console.error('Message send failed:', error);
  }
}

8.2.3 Messaging System Architecture

Message Source Type Definition

// Message source enum
const MessageSource = {
  BACKGROUND: 'background',
  CONTENT_SCRIPT: 'content_script',
  POPUP: 'popup',
  OPTIONS: 'options'
};

// Message object structure
class Message {
  constructor(action, data = null, source = MessageSource.BACKGROUND) {
    this.action = action;
    this.data = data;
    this.source = source;
    this.requestId = this.generateRequestId();
    this.timestamp = Date.now();
  }

  generateRequestId() {
    return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

Message Bus Implementation

// Simulated Chrome extension message bus
class MessageBus {
  constructor() {
    this.listeners = {
      [MessageSource.BACKGROUND]: [],
      [MessageSource.CONTENT_SCRIPT]: [],
      [MessageSource.POPUP]: [],
      [MessageSource.OPTIONS]: []
    };
    this.pendingResponses = new Map();
    this.responseTimeout = 10000; // 10 second timeout
  }

  // Add message listener
  addListener(source, callback) {
    this.listeners[source].push(callback);
    console.log(`Added message listener for ${source}`);
  }

  // Send message
  async sendMessage(message, target) {
    console.log(`Sending message: ${message.action} from ${message.source} to ${target}`);

    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        this.pendingResponses.delete(message.requestId);
        reject(new Error('Message send timeout'));
      }, this.responseTimeout);

      this.pendingResponses.set(message.requestId, { resolve, reject, timeoutId });

      // Find target listeners
      const targetListeners = this.listeners[target] || [];

      if (targetListeners.length === 0) {
        clearTimeout(timeoutId);
        this.pendingResponses.delete(message.requestId);
        reject(new Error(`No listener found for ${target}`));
        return;
      }

      // Call first listener (simulating Chrome's behavior)
      const listener = targetListeners[0];
      this.handleMessage(message, listener).catch(error => {
        const pending = this.pendingResponses.get(message.requestId);
        if (pending) {
          clearTimeout(pending.timeoutId);
          this.pendingResponses.delete(message.requestId);
          reject(error);
        }
      });
    });
  }

  // Handle message
  async handleMessage(message, listener) {
    const sendResponse = (responseData) => {
      const pending = this.pendingResponses.get(message.requestId);
      if (pending) {
        clearTimeout(pending.timeoutId);
        pending.resolve(responseData);
        this.pendingResponses.delete(message.requestId);
      }
    };

    try {
      await listener(message, sendResponse);
    } catch (error) {
      sendResponse({
        success: false,
        error: error.message
      });
    }
  }
}

Background Script Simulation

// Simulated Background Script
class BackgroundScript {
  constructor(messageBus) {
    this.messageBus = messageBus;
    this.storage = {};
    messageBus.addListener(MessageSource.BACKGROUND, this.handleMessage.bind(this));
  }

  async handleMessage(message, sendResponse) {
    console.log(`Background handling message: ${message.action}`);

    try {
      switch (message.action) {
        case 'getUserData':
          sendResponse({
            success: true,
            data: {
              profile: this.storage.userProfile || {},
              preferences: this.storage.preferences || {}
            }
          });
          break;

        case 'updateSettings':
          Object.assign(this.storage, message.data);
          await this.broadcastSettingsUpdate(message.data);
          sendResponse({ success: true });
          break;

        case 'getStats':
          // Simulate async operation
          await new Promise(resolve => setTimeout(resolve, 100));
          sendResponse({
            success: true,
            data: {
              clickCount: 42,
              activeTime: 3600,
              lastUpdate: Date.now()
            }
          });
          break;

        default:
          sendResponse({
            success: false,
            error: `Unknown action: ${message.action}`
          });
      }
    } catch (error) {
      sendResponse({
        success: false,
        error: error.message
      });
    }
  }

  async broadcastSettingsUpdate(settings) {
    const updateMessage = new Message(
      'settingsUpdated',
      settings,
      MessageSource.BACKGROUND
    );

    try {
      await this.messageBus.sendMessage(
        updateMessage,
        MessageSource.CONTENT_SCRIPT
      );
    } catch (error) {
      console.error('Settings update broadcast failed:', error);
    }
  }
}

Content Script Simulation

// Simulated Content Script
class ContentScript {
  constructor(messageBus) {
    this.messageBus = messageBus;
    this.pageData = {
      url: 'https://example.com',
      title: 'Example Page',
      elements: ['div', 'p', 'span', 'a']
    };
    messageBus.addListener(MessageSource.CONTENT_SCRIPT, this.handleMessage.bind(this));
  }

  async handleMessage(message, sendResponse) {
    console.log(`Content Script handling message: ${message.action}`);

    try {
      switch (message.action) {
        case 'extractData':
          sendResponse({
            success: true,
            data: {
              url: this.pageData.url,
              title: this.pageData.title,
              elements: this.pageData.elements.length,
              timestamp: Date.now()
            }
          });
          break;

        case 'settingsUpdated':
          console.log('Applying new settings:', message.data);
          sendResponse({ received: true });
          break;

        case 'pageAction':
          const result = await this.performPageAction(message.data);
          sendResponse({
            success: true,
            result: result
          });
          break;

        default:
          sendResponse({
            success: false,
            error: `Unhandled message type: ${message.action}`
          });
      }
    } catch (error) {
      sendResponse({
        success: false,
        error: error.message
      });
    }
  }

  async performPageAction(actionData) {
    const actionType = actionData.type;

    switch (actionType) {
      case 'highlight':
        return { highlighted: true, text: actionData.text };
      case 'extract':
        return { extracted: this.pageData };
      case 'modify':
        return { modified: true, changes: actionData.changes };
      default:
        throw new Error(`Unknown action type: ${actionType}`);
    }
  }

  async sendToBackground(action, data = null) {
    const message = new Message(action, data, MessageSource.CONTENT_SCRIPT);
    return await this.messageBus.sendMessage(message, MessageSource.BACKGROUND);
  }
}
// Simulated Popup Script
class PopupScript {
  constructor(messageBus) {
    this.messageBus = messageBus;
    messageBus.addListener(MessageSource.POPUP, this.handleMessage.bind(this));
  }

  async handleMessage(message, sendResponse) {
    console.log(`Popup handling message: ${message.action}`);
    sendResponse({ received: true });
  }

  async getUserData() {
    const message = new Message('getUserData', null, MessageSource.POPUP);
    return await this.messageBus.sendMessage(message, MessageSource.BACKGROUND);
  }

  async updateSettings(settings) {
    const message = new Message('updateSettings', settings, MessageSource.POPUP);
    return await this.messageBus.sendMessage(message, MessageSource.BACKGROUND);
  }
}

Usage Example

// Demonstrate messaging system
async function demoMessageSystem() {
  console.log('=== Chrome Extension Messaging System Demo ===\n');

  // Create message bus and components
  const messageBus = new MessageBus();
  const background = new BackgroundScript(messageBus);
  const contentScript = new ContentScript(messageBus);
  const popup = new PopupScript(messageBus);

  try {
    // 1. Popup gets user data
    console.log('1. Popup getting user data:');
    const userData = await popup.getUserData();
    console.log('   Result:', userData);

    // 2. Popup updates settings
    console.log('\n2. Popup updating settings:');
    const updateResult = await popup.updateSettings({
      theme: 'dark',
      autoSave: true
    });
    console.log('   Result:', updateResult);

    // 3. Content Script sends data to Background
    console.log('\n3. Content Script sending data to Background:');
    const statsResult = await contentScript.sendToBackground('getStats');
    console.log('   Result:', statsResult);

    // 4. Send page action command to Content Script
    console.log('\n4. Sending page action command to Content Script:');
    const pageActionMessage = new Message(
      'pageAction',
      { type: 'extract', selector: 'div' },
      MessageSource.BACKGROUND
    );
    const actionResult = await messageBus.sendMessage(
      pageActionMessage,
      MessageSource.CONTENT_SCRIPT
    );
    console.log('   Result:', actionResult);

    console.log('\n=== Demo Complete ===');
  } catch (error) {
    console.error('Error during demo:', error);
  }
}

// Run demo
// demoMessageSystem();

8.3 Long-lived Connections (Port)

8.3.1 Establishing Long-lived Connections

// background.js - Long-lived connection handling
class ConnectionManager {
  constructor() {
    this.activeConnections = new Map();
    this.setupConnectionListener();
  }

  setupConnectionListener() {
    chrome.runtime.onConnect.addListener((port) => {
      console.log('New connection established:', port.name);

      const connectionId = this.generateConnectionId();

      const connection = {
        id: connectionId,
        port: port,
        type: port.name,
        sender: port.sender,
        createdAt: Date.now(),
        lastActivity: Date.now()
      };

      this.activeConnections.set(connectionId, connection);

      // Set up message listener
      port.onMessage.addListener((message) => {
        this.handlePortMessage(connectionId, message);
      });

      // Set up disconnect listener
      port.onDisconnect.addListener(() => {
        this.handlePortDisconnect(connectionId);
      });

      // Send connection confirmation
      port.postMessage({
        type: 'connectionEstablished',
        connectionId: connectionId,
        timestamp: Date.now()
      });
    });
  }

  generateConnectionId() {
    return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  handlePortMessage(connectionId, message) {
    const connection = this.activeConnections.get(connectionId);
    if (!connection) return;

    // Update activity time
    connection.lastActivity = Date.now();

    console.log(`Connection ${connectionId} received message:`, message);

    switch (message.type) {
      case 'subscribe':
        this.handleSubscription(connectionId, message.data);
        break;

      case 'unsubscribe':
        this.handleUnsubscription(connectionId, message.data);
        break;

      case 'dataRequest':
        this.handleDataRequest(connectionId, message.data);
        break;

      case 'liveUpdate':
        this.handleLiveUpdate(connectionId, message.data);
        break;

      default:
        this.sendToConnection(connectionId, {
          type: 'error',
          message: 'Unknown message type'
        });
    }
  }

  handlePortDisconnect(connectionId) {
    const connection = this.activeConnections.get(connectionId);
    if (connection) {
      console.log(`Connection ${connectionId} disconnected`);
      this.activeConnections.delete(connectionId);
    }
  }

  sendToConnection(connectionId, message) {
    const connection = this.activeConnections.get(connectionId);
    if (connection && connection.port) {
      try {
        connection.port.postMessage({
          ...message,
          timestamp: Date.now(),
          connectionId: connectionId
        });
      } catch (error) {
        console.error(`Failed to send message to connection ${connectionId}:`, error);
        this.activeConnections.delete(connectionId);
      }
    }
  }

  // Broadcast message to all connections
  broadcast(message, filter = null) {
    for (const [connectionId, connection] of this.activeConnections) {
      if (!filter || filter(connection)) {
        this.sendToConnection(connectionId, message);
      }
    }
  }

  // Broadcast to specific type of connections
  broadcastToType(connectionType, message) {
    this.broadcast(message, (conn) => conn.type === connectionType);
  }

  handleSubscription(connectionId, subscriptionData) {
    const connection = this.activeConnections.get(connectionId);
    if (!connection) return;

    if (!connection.subscriptions) {
      connection.subscriptions = new Set();
    }

    connection.subscriptions.add(subscriptionData.topic);

    this.sendToConnection(connectionId, {
      type: 'subscribed',
      topic: subscriptionData.topic,
      message: `Subscribed to ${subscriptionData.topic}`
    });

    // Send initial data
    this.sendTopicData(connectionId, subscriptionData.topic);
  }

  sendTopicData(connectionId, topic) {
    let data;

    switch (topic) {
      case 'stats':
        data = {
          clickCount: 42,
          activeTime: 3600,
          lastUpdate: Date.now()
        };
        break;

      case 'notifications':
        data = {
          unreadCount: 5,
          latest: 'New update available'
        };
        break;

      default:
        data = { message: `No data for ${topic}` };
    }

    this.sendToConnection(connectionId, {
      type: 'topicData',
      topic: topic,
      data: data
    });
  }
}

// Instantiate connection manager
const connectionManager = new ConnectionManager();

// Periodically send updates to connections subscribed to stats
setInterval(() => {
  const statsUpdate = {
    type: 'topicData',
    topic: 'stats',
    data: {
      clickCount: Math.floor(Math.random() * 100),
      activeTime: Math.floor(Math.random() * 10000),
      lastUpdate: Date.now()
    }
  };

  connectionManager.broadcast(statsUpdate, (conn) => {
    return conn.subscriptions && conn.subscriptions.has('stats');
  });
}, 5000); // Update every 5 seconds

8.3.2 Long-lived Connection Client

// popup.js or content-script.js - Long-lived connection client
class LongConnectionClient {
  constructor(connectionName) {
    this.connectionName = connectionName;
    this.port = null;
    this.connectionId = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.reconnectDelay = 1000; // 1 second
    this.subscriptions = new Set();
    this.messageHandlers = new Map();

    this.connect();
  }

  connect() {
    try {
      console.log(`Establishing connection: ${this.connectionName}`);

      this.port = chrome.runtime.connect({ name: this.connectionName });

      this.port.onMessage.addListener((message) => {
        this.handleMessage(message);
      });

      this.port.onDisconnect.addListener(() => {
        this.handleDisconnect();
      });

    } catch (error) {
      console.error('Failed to establish connection:', error);
      this.scheduleReconnect();
    }
  }

  handleMessage(message) {
    console.log('Received port message:', message);

    switch (message.type) {
      case 'connectionEstablished':
        this.onConnectionEstablished(message);
        break;

      case 'subscribed':
        console.log(`Successfully subscribed: ${message.topic}`);
        break;

      case 'topicData':
        this.handleTopicData(message);
        break;

      case 'error':
        console.error('Server error:', message.message);
        break;

      default:
        // Call custom message handler
        const handler = this.messageHandlers.get(message.type);
        if (handler) {
          handler(message);
        }
    }
  }

  onConnectionEstablished(message) {
    this.connectionId = message.connectionId;
    this.isConnected = true;
    this.reconnectAttempts = 0;

    console.log(`Connection established: ${this.connectionId}`);

    // Restore subscriptions
    this.subscriptions.forEach(topic => {
      this.subscribe(topic);
    });

    // Trigger connection success event
    this.onConnected();
  }

  handleDisconnect() {
    console.log('Connection disconnected');
    this.isConnected = false;
    this.connectionId = null;

    this.onDisconnected();
    this.scheduleReconnect();
  }

  scheduleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = this.reconnectDelay * this.reconnectAttempts;

      console.log(`Attempting reconnect in ${delay}ms (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

      setTimeout(() => {
        this.connect();
      }, delay);
    } else {
      console.error('Maximum reconnect attempts reached');
    }
  }

  // Send message
  sendMessage(message) {
    if (this.isConnected && this.port) {
      try {
        this.port.postMessage(message);
        return true;
      } catch (error) {
        console.error('Failed to send message:', error);
        return false;
      }
    } else {
      console.warn('Connection not established, cannot send message');
      return false;
    }
  }

  // Subscribe to topic
  subscribe(topic) {
    this.subscriptions.add(topic);

    if (this.isConnected) {
      this.sendMessage({
        type: 'subscribe',
        data: { topic: topic }
      });
    }
  }

  // Unsubscribe from topic
  unsubscribe(topic) {
    this.subscriptions.delete(topic);

    if (this.isConnected) {
      this.sendMessage({
        type: 'unsubscribe',
        data: { topic: topic }
      });
    }
  }

  // Register message handler
  onMessage(messageType, handler) {
    this.messageHandlers.set(messageType, handler);
  }

  // Handle topic data
  handleTopicData(message) {
    const topic = message.topic;
    const data = message.data;

    console.log(`Received topic data ${topic}:`, data);

    // Trigger topic-specific handling
    const event = new CustomEvent(`topic:${topic}`, {
      detail: data
    });
    document.dispatchEvent(event);
  }

  // Connection success callback (can be overridden)
  onConnected() {
    console.log('Connection success callback');
  }

  // Connection disconnected callback (can be overridden)
  onDisconnected() {
    console.log('Connection disconnected callback');
  }

  // Disconnect
  disconnect() {
    if (this.port) {
      this.port.disconnect();
    }
  }
}

// Actual usage example
class StatsDisplayClient extends LongConnectionClient {
  constructor() {
    super('statsDisplay');

    this.setupEventListeners();
  }

  onConnected() {
    super.onConnected();

    // Subscribe to statistics data after connection
    this.subscribe('stats');
    this.subscribe('notifications');
  }

  setupEventListeners() {
    // Listen for statistics data updates
    document.addEventListener('topic:stats', (event) => {
      this.updateStatsDisplay(event.detail);
    });

    // Listen for notification updates
    document.addEventListener('topic:notifications', (event) => {
      this.updateNotifications(event.detail);
    });
  }

  updateStatsDisplay(statsData) {
    const clickCountElement = document.getElementById('clickCount');
    const activeTimeElement = document.getElementById('activeTime');

    if (clickCountElement) {
      clickCountElement.textContent = statsData.clickCount;
    }

    if (activeTimeElement) {
      activeTimeElement.textContent = this.formatTime(statsData.activeTime);
    }
  }

  updateNotifications(notificationData) {
    const notificationBadge = document.getElementById('notificationBadge');

    if (notificationBadge) {
      notificationBadge.textContent = notificationData.unreadCount;
      notificationBadge.style.display = notificationData.unreadCount > 0 ? 'block' : 'none';
    }
  }

  formatTime(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    return `${hours}h ${minutes}m`;
  }
}

// Use in popup or content script
// const statsClient = new StatsDisplayClient();

8.4 Cross-extension Communication

8.4.1 Cross-extension Messaging

// Sender extension
class ExternalMessenger {
  constructor() {
    this.trustedExtensions = [
      'abcdefghijklmnopqrstuvwxyz123456', // Trusted extension ID
      'zyxwvutsrqponmlkjihgfedcba654321'
    ];
  }

  // Send message to other extension
  async sendToExtension(extensionId, message) {
    if (!this.trustedExtensions.includes(extensionId)) {
      throw new Error('Untrusted extension ID');
    }

    return new Promise((resolve, reject) => {
      chrome.runtime.sendMessage(extensionId, {
        ...message,
        timestamp: Date.now(),
        source: chrome.runtime.id
      }, (response) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
        } else {
          resolve(response);
        }
      });
    });
  }

  // Broadcast message to all trusted extensions
  async broadcastToTrustedExtensions(message) {
    const promises = this.trustedExtensions.map(extensionId =>
      this.sendToExtension(extensionId, message).catch(error => ({
        extensionId,
        error: error.message
      }))
    );

    const results = await Promise.allSettled(promises);

    return results.map((result, index) => ({
      extensionId: this.trustedExtensions[index],
      success: result.status === 'fulfilled' && !result.value.error,
      data: result.status === 'fulfilled' ? result.value : null,
      error: result.status === 'rejected' ? result.reason.message :
             (result.value && result.value.error) || null
    }));
  }
}

// Receiver extension
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
  console.log('Received external message:', message, 'from:', sender.id);

  // Verify sender
  const trustedSenders = [
    'abcdefghijklmnopqrstuvwxyz123456',
    'fedcba9876543210abcdef1234567890'
  ];

  if (!trustedSenders.includes(sender.id)) {
    sendResponse({
      success: false,
      error: 'Unauthorized extension'
    });
    return;
  }

  // Handle message
  switch (message.action) {
    case 'getData':
      handleExternalDataRequest(message, sendResponse);
      return true;

    case 'syncSettings':
      handleSettingsSync(message, sendResponse);
      return true;

    case 'notify':
      handleExternalNotification(message, sendResponse);
      return true;

    default:
      sendResponse({
        success: false,
        error: 'Unknown action'
      });
  }
});

async function handleExternalDataRequest(message, sendResponse) {
  try {
    const requestedData = await chrome.storage.sync.get(message.keys);
    sendResponse({
      success: true,
      data: requestedData
    });
  } catch (error) {
    sendResponse({
      success: false,
      error: error.message
    });
  }
}

8.5 Error Handling and Debugging

8.5.1 Message Error Handling

// Message error handler
class MessageErrorHandler {
  constructor() {
    this.errorLog = [];
    this.retryCount = new Map();
    this.maxRetries = 3;
  }

  // Send message with retry
  async sendMessageWithRetry(message, maxRetries = this.maxRetries) {
    let lastError;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        console.log(`Sending message attempt ${attempt}/${maxRetries}:`, message.action);

        const response = await this.sendMessage(message);

        if (response && response.success !== false) {
          // Reset retry count
          this.retryCount.delete(message.action);
          return response;
        } else {
          throw new Error(response?.error || 'Operation failed');
        }

      } catch (error) {
        lastError = error;
        console.warn(`Message send failed (attempt ${attempt}/${maxRetries}):`, error.message);

        this.logError(message, error, attempt);

        if (attempt < maxRetries) {
          await this.delay(1000 * attempt); // Increasing delay
        }
      }
    }

    // All attempts failed
    this.recordFailedMessage(message, lastError);
    throw new Error(`Message send finally failed: ${lastError.message}`);
  }

  async sendMessage(message) {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error('Message send timeout'));
      }, 10000);

      chrome.runtime.sendMessage(message, (response) => {
        clearTimeout(timeout);

        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
        } else {
          resolve(response);
        }
      });
    });
  }

  logError(message, error, attempt) {
    const errorEntry = {
      timestamp: Date.now(),
      message: message,
      error: error.message,
      attempt: attempt
    };

    this.errorLog.push(errorEntry);

    // Limit log size
    if (this.errorLog.length > 100) {
      this.errorLog = this.errorLog.slice(-50);
    }
  }

  recordFailedMessage(message, error) {
    const current = this.retryCount.get(message.action) || 0;
    this.retryCount.set(message.action, current + 1);

    console.error(`Message ${message.action} failed:`, error.message);
  }

  getErrorLog() {
    return [...this.errorLog];
  }

  getFailureStats() {
    const stats = {};
    for (const [action, count] of this.retryCount) {
      stats[action] = count;
    }
    return stats;
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Message debugger
class MessageDebugger {
  constructor() {
    this.messageLog = [];
    this.enabled = false;
  }

  enable() {
    this.enabled = true;
    this.setupMessageInterception();
  }

  setupMessageInterception() {
    // Intercept sent messages
    const originalSendMessage = chrome.runtime.sendMessage;
    chrome.runtime.sendMessage = (message, responseCallback) => {
      if (this.enabled) {
        this.logOutgoingMessage(message);
      }

      return originalSendMessage.call(chrome.runtime, message, (response) => {
        if (this.enabled) {
          this.logMessageResponse(message, response);
        }

        if (responseCallback) {
          responseCallback(response);
        }
      });
    };
  }

  logOutgoingMessage(message) {
    this.messageLog.push({
      type: 'outgoing',
      timestamp: Date.now(),
      message: JSON.parse(JSON.stringify(message))
    });

    console.log('📤 Sending message:', message);
  }

  logMessageResponse(originalMessage, response) {
    this.messageLog.push({
      type: 'response',
      timestamp: Date.now(),
      originalMessage: JSON.parse(JSON.stringify(originalMessage)),
      response: JSON.parse(JSON.stringify(response))
    });

    console.log('📥 Message response:', response);
  }

  getMessageLog() {
    return [...this.messageLog];
  }

  clearLog() {
    this.messageLog = [];
  }

  exportLog() {
    const logData = {
      timestamp: new Date().toISOString(),
      messages: this.messageLog
    };

    const blob = new Blob([JSON.stringify(logData, null, 2)], {
      type: 'application/json'
    });

    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `message-log-${Date.now()}.json`;
    a.click();

    URL.revokeObjectURL(url);
  }
}

// Global instances
const messageErrorHandler = new MessageErrorHandler();
const messageDebugger = new MessageDebugger();

// Enable debugging in development mode
if (process.env.NODE_ENV === 'development') {
  messageDebugger.enable();
}

warning Important Notes

  1. Message Size Limits: Chrome has message size limits, avoid sending excessively large data
  2. Async Handling: Always use return true to keep async message channel open
  3. Error Handling: Check chrome.runtime.lastError and response error status
  4. Memory Leaks: Clean up event listeners and timers promptly
  5. Security Validation: Validate message source and content legitimacy

tip Best Practices

  • Define unified message format and error handling strategy
  • Implement message retry and timeout mechanisms
  • Use TypeScript to enhance type safety
  • Add message logging and debugging tools
  • Consider using message queues for handling large volumes of messages

note Summary This chapter provides an in-depth introduction to Chrome extension messaging mechanisms, including implementations for various scenarios such as one-time messages, long-lived connections, and cross-extension communication. Mastering these communication techniques is key to building complex Chrome extensions. In the next chapter, we will learn about storage and data management to implement data persistence and synchronization.

Categories