Chapter 08 Messaging Mechanisms
Haiyue
38min
Chapter 8: Messaging Mechanisms
Learning Objectives
- Understand Chrome extension messaging architecture
- Master communication methods between different components
- Learn to handle asynchronous messages and errors
- 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);
}
}
Popup Script Simulation
// 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
- Message Size Limits: Chrome has message size limits, avoid sending excessively large data
- Async Handling: Always use
return trueto keep async message channel open - Error Handling: Check
chrome.runtime.lastErrorand response error status - Memory Leaks: Clean up event listeners and timers promptly
- 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.