Chapter 05 Background Scripts

Haiyue
19min

Chapter 5: Background Scripts

Learning Objectives

  1. Understand the concept and working mechanism of Background Scripts
  2. Master Service Worker applications in Chrome extensions
  3. Learn event listening and lifecycle management
  4. Proficiently use background scripts for data processing and API calls

5.1 Background Scripts Overview

Background Scripts are core components in Chrome extensions, running continuously in the browser background, independent of specific web pages or user interfaces.

5.1.1 Basic Concepts

🔄 正在渲染 Mermaid 图表...

tip Core Characteristics

  • Runs independently from web pages
  • Can listen to browser events
  • Handles long-running tasks
  • Manages extension state

5.1.2 Manifest Configuration

In Manifest V3, use Service Worker to replace traditional Background Pages:

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "background": {
    "service_worker": "background.js"
  },
  "permissions": [
    "storage",
    "tabs"
  ]
}

5.2 Service Worker Basics

5.2.1 Service Worker Lifecycle

🔄 正在渲染 Mermaid 图表...

5.2.2 Basic Event Listening

// background.js
// Triggered when extension is installed or updated
chrome.runtime.onInstalled.addListener((details) => {
  console.log('Extension installed:', details);

  if (details.reason === 'install') {
    // First installation
    initializeExtension();
  } else if (details.reason === 'update') {
    // Extension update
    handleUpdate();
  }
});

// Triggered when extension starts
chrome.runtime.onStartup.addListener(() => {
  console.log('Extension started');
});

function initializeExtension() {
  // Set default configuration
  chrome.storage.sync.set({
    'isEnabled': true,
    'theme': 'light'
  });
}

function handleUpdate() {
  // Handle update logic
  console.log('Extension updated');
}

5.2.3 Background Worker Simulation Example

// background_worker.js - Simulating Chrome extension background script implementation
class BackgroundWorker {
  /**
   * Class simulating Chrome extension Background Script
   */
  constructor() {
    this.isRunning = false;
    this.eventListeners = {};
    this.storage = {};
  }

  async start() {
    /**
     * Start background worker
     */
    this.isRunning = true;
    console.log(`[${new Date().toISOString()}] Background worker started`);

    // Simulate onInstalled event
    await this.triggerEvent('onInstalled', { reason: 'startup' });

    // Start event loop
    while (this.isRunning) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      await this.processEvents();
    }
  }

  addListener(eventName, callback) {
    /**
     * Add event listener
     * @param {string} eventName - Event name
     * @param {Function} callback - Callback function
     */
    if (!this.eventListeners[eventName]) {
      this.eventListeners[eventName] = [];
    }
    this.eventListeners[eventName].push(callback);
  }

  async triggerEvent(eventName, data) {
    /**
     * Trigger event
     * @param {string} eventName - Event name
     * @param {Object} data - Event data
     */
    if (this.eventListeners[eventName]) {
      for (const callback of this.eventListeners[eventName]) {
        await callback(data);
      }
    }
  }

  async processEvents() {
    /**
     * Process periodic events
     */
    // Simulate periodic checking task
    const currentTime = new Date();
    if (currentTime.getSeconds() % 30 === 0) { // Trigger every 30 seconds
      await this.triggerEvent('periodic_check', { time: currentTime });
    }
  }
}

// Usage example
async function onInstalledHandler(details) {
  console.log('Extension installed:', details);
}

async function periodicCheckHandler(details) {
  console.log('Periodic check at:', details.time);
}

// Create background worker instance
const worker = new BackgroundWorker();
worker.addListener('onInstalled', onInstalledHandler);
worker.addListener('periodic_check', periodicCheckHandler);

// Start worker (commented out actual execution)
// worker.start();

5.3 Event Listening and Handling

5.3.1 Browser Event Listening

// Tab event listening
chrome.tabs.onCreated.addListener((tab) => {
  console.log('New tab created:', tab.url);
  processNewTab(tab);
});

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    console.log('Page loaded:', tab.url);
    analyzePageContent(tab);
  }
});

chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
  console.log('Tab closed:', tabId);
  cleanupTabData(tabId);
});

// Process new tab
function processNewTab(tab) {
  // Check if URL needs special handling
  if (tab.url.includes('github.com')) {
    // Add special features for GitHub pages
    chrome.tabs.sendMessage(tab.id, {
      action: 'initGithubFeatures'
    });
  }
}

function analyzePageContent(tab) {
  // Analyze page content
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: extractPageInfo
  });
}

function extractPageInfo() {
  // Function to execute on page
  const title = document.title;
  const description = document.querySelector('meta[name="description"]')?.content;

  chrome.runtime.sendMessage({
    action: 'pageAnalyzed',
    data: { title, description, url: location.href }
  });
}

function cleanupTabData(tabId) {
  // Clean up tab-related data
  chrome.storage.local.remove([`tab_${tabId}_data`]);
}

5.3.2 Message Listening

// Listen for messages from content script or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('Message received:', message);

  switch (message.action) {
    case 'getData':
      handleGetData(message, sendResponse);
      return true; // Async response

    case 'saveData':
      handleSaveData(message.data, sendResponse);
      return true;

    case 'processUrl':
      processUrl(message.url, sendResponse);
      return true;

    default:
      sendResponse({ error: 'Unknown operation' });
  }
});

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

async function handleSaveData(data, sendResponse) {
  try {
    await chrome.storage.sync.set(data);
    sendResponse({ success: true });
  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

5.4 Scheduled Tasks and Periodic Operations

5.4.1 Using Alarms API

// Set scheduled task
chrome.alarms.create('dataSync', {
  delayInMinutes: 1,
  periodInMinutes: 30
});

chrome.alarms.create('dailyCleanup', {
  when: Date.now() + 24 * 60 * 60 * 1000 // After 24 hours
});

// Listen for alarm events
chrome.alarms.onAlarm.addListener((alarm) => {
  console.log('Alarm triggered:', alarm.name);

  switch (alarm.name) {
    case 'dataSync':
      performDataSync();
      break;

    case 'dailyCleanup':
      performDailyCleanup();
      break;
  }
});

async function performDataSync() {
  try {
    console.log('Starting data sync...');

    // Get local data
    const localData = await chrome.storage.local.get(null);

    // Sync to server
    const response = await fetch('https://api.example.com/sync', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(localData)
    });

    if (response.ok) {
      console.log('Data sync successful');
      // Update last sync time
      chrome.storage.sync.set({
        'lastSyncTime': Date.now()
      });
    }
  } catch (error) {
    console.error('Data sync failed:', error);
  }
}

function performDailyCleanup() {
  console.log('Performing daily cleanup...');

  // Clean expired data
  chrome.storage.local.get(null, (items) => {
    const keysToRemove = [];
    const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;

    for (const [key, value] of Object.entries(items)) {
      if (value.timestamp && value.timestamp < oneWeekAgo) {
        keysToRemove.push(key);
      }
    }

    if (keysToRemove.length > 0) {
      chrome.storage.local.remove(keysToRemove);
      console.log(`Cleaned ${keysToRemove.length} expired items`);
    }
  });

  // Set next cleanup
  chrome.alarms.create('dailyCleanup', {
    when: Date.now() + 24 * 60 * 60 * 1000
  });
}

5.4.2 Alarm System Simulation

// alarm_system.js - Simulating Chrome extension Alarm system
class AlarmSystem {
  /**
   * Simulate Chrome extension Alarm system
   */
  constructor() {
    this.alarms = {};
    this.running = false;
    this.listeners = [];
  }

  createAlarm(name, options = {}) {
    /**
     * Create alarm
     * @param {string} name - Alarm name
     * @param {Object} options - Configuration options
     * @param {number} options.delayMinutes - Delay in minutes
     * @param {number} options.periodMinutes - Period in minutes
     * @param {number} options.whenTimestamp - Specified execution timestamp
     */
    const now = Date.now();
    let nextRun;

    if (options.whenTimestamp) {
      nextRun = options.whenTimestamp;
    } else if (options.delayMinutes) {
      nextRun = now + (options.delayMinutes * 60 * 1000);
    } else {
      nextRun = now;
    }

    this.alarms[name] = {
      name: name,
      nextRun: nextRun,
      periodMinutes: options.periodMinutes
    };

    console.log(`Created alarm: ${name}, Next run: ${new Date(nextRun).toISOString()}`);
  }

  addListener(callback) {
    /**
     * Add alarm listener
     * @param {Function} callback - Callback function
     */
    this.listeners.push(callback);
  }

  async start() {
    /**
     * Start alarm system
     */
    this.running = true;
    console.log('Alarm system started');

    while (this.running) {
      const currentTime = Date.now();

      for (const [alarmName, alarmInfo] of Object.entries(this.alarms)) {
        if (currentTime >= alarmInfo.nextRun) {
          // Trigger alarm
          await this.triggerAlarm(alarmInfo);

          // If periodic alarm, set next execution time
          if (alarmInfo.periodMinutes) {
            alarmInfo.nextRun = currentTime + (alarmInfo.periodMinutes * 60 * 1000);
          } else {
            // One-time alarm, delete
            delete this.alarms[alarmName];
          }
        }
      }

      await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
    }
  }

  async triggerAlarm(alarmInfo) {
    /**
     * Trigger alarm event
     * @param {Object} alarmInfo - Alarm information
     */
    console.log(`Alarm triggered: ${alarmInfo.name}`);

    for (const listener of this.listeners) {
      await listener(alarmInfo);
    }
  }

  stop() {
    /**
     * Stop alarm system
     */
    this.running = false;
    console.log('Alarm system stopped');
  }
}

// Usage example
async function alarmHandler(alarmInfo) {
  const alarmName = alarmInfo.name;

  if (alarmName === 'dataSync') {
    await performDataSync();
  } else if (alarmName === 'dailyCleanup') {
    await performDailyCleanup();
  }
}

async function performDataSync() {
  console.log('Executing data sync...');
  // Simulate data sync logic
  await new Promise(resolve => setTimeout(resolve, 2000));
  console.log('Data sync complete');
}

async function performDailyCleanup() {
  console.log('Executing daily cleanup...');
  // Simulate cleanup logic
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('Cleanup complete');
}

// Create and use alarm system
const alarmSystem = new AlarmSystem();
alarmSystem.addListener(alarmHandler);

// Set alarms
alarmSystem.createAlarm('dataSync', { delayMinutes: 1, periodMinutes: 30 });
alarmSystem.createAlarm('dailyCleanup', { whenTimestamp: Date.now() + 5000 });

// Start system (commented out actual execution)
// alarmSystem.start();

5.5 Data Processing and API Calls

5.5.1 Network Request Handling

// API call management
class APIManager {
  constructor() {
    this.baseUrl = 'https://api.example.com';
    this.apiKey = null;
    this.requestQueue = [];
    this.rateLimitDelay = 1000; // 1 second
  }

  async initialize() {
    // Get API key from storage
    const result = await chrome.storage.sync.get(['apiKey']);
    this.apiKey = result.apiKey;
  }

  async makeRequest(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;

    const defaultOptions = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`
      }
    };

    const requestOptions = { ...defaultOptions, ...options };

    try {
      const response = await fetch(url, requestOptions);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }

  async fetchUserData(userId) {
    return await this.makeRequest(`/users/${userId}`);
  }

  async updateUserPreferences(userId, preferences) {
    return await this.makeRequest(`/users/${userId}/preferences`, {
      method: 'PUT',
      body: JSON.stringify(preferences)
    });
  }

  // Batch process requests
  async batchProcess(requests) {
    const results = [];

    for (const request of requests) {
      try {
        const result = await this.makeRequest(request.endpoint, request.options);
        results.push({ success: true, data: result });

        // Add delay to avoid rate limiting
        await new Promise(resolve => setTimeout(resolve, this.rateLimitDelay));
      } catch (error) {
        results.push({ success: false, error: error.message });
      }
    }

    return results;
  }
}

// Initialize API manager
const apiManager = new APIManager();
apiManager.initialize();

5.5.2 Data Caching and Optimization

// Data cache management
class DataCache {
  constructor() {
    this.cachePrefix = 'cache_';
    this.defaultTTL = 5 * 60 * 1000; // 5 minutes
  }

  async set(key, data, ttl = this.defaultTTL) {
    const cacheItem = {
      data: data,
      timestamp: Date.now(),
      ttl: ttl
    };

    await chrome.storage.local.set({
      [`${this.cachePrefix}${key}`]: cacheItem
    });
  }

  async get(key) {
    const result = await chrome.storage.local.get([`${this.cachePrefix}${key}`]);
    const cacheItem = result[`${this.cachePrefix}${key}`];

    if (!cacheItem) {
      return null;
    }

    // Check if expired
    if (Date.now() - cacheItem.timestamp > cacheItem.ttl) {
      await this.delete(key);
      return null;
    }

    return cacheItem.data;
  }

  async delete(key) {
    await chrome.storage.local.remove([`${this.cachePrefix}${key}`]);
  }

  async clear() {
    const allData = await chrome.storage.local.get(null);
    const cacheKeys = Object.keys(allData).filter(key =>
      key.startsWith(this.cachePrefix)
    );

    if (cacheKeys.length > 0) {
      await chrome.storage.local.remove(cacheKeys);
    }
  }

  // Get data with cache
  async getWithCache(key, fetchFunction, ttl) {
    let data = await this.get(key);

    if (data === null) {
      // Cache miss, execute fetch function
      data = await fetchFunction();
      await this.set(key, data, ttl);
    }

    return data;
  }
}

// Usage example
const dataCache = new DataCache();

// Cached API call
async function getCachedUserProfile(userId) {
  return await dataCache.getWithCache(
    `user_profile_${userId}`,
    () => apiManager.fetchUserData(userId),
    10 * 60 * 1000 // 10 minute cache
  );
}

warning Important Notes In Manifest V3, Service Worker has the following limitations:

  • Does not support DOM operations
  • Cannot use synchronous storage API
  • Limited lifecycle, may be terminated by browser
  • Need to use Promise-based API

5.6 Error Handling and Logging

5.6.1 Unified Error Handling

// Error handling manager
class ErrorHandler {
  constructor() {
    this.errorLog = [];
    this.maxLogSize = 100;
  }

  logError(error, context = '') {
    const errorInfo = {
      message: error.message,
      stack: error.stack,
      context: context,
      timestamp: new Date().toISOString(),
      url: location.href
    };

    this.errorLog.push(errorInfo);

    // Maintain log size limit
    if (this.errorLog.length > this.maxLogSize) {
      this.errorLog = this.errorLog.slice(-this.maxLogSize);
    }

    // Store locally
    chrome.storage.local.set({ 'errorLog': this.errorLog });

    console.error('Error caught:', errorInfo);
  }

  async getErrorLog() {
    const result = await chrome.storage.local.get(['errorLog']);
    return result.errorLog || [];
  }

  clearErrorLog() {
    this.errorLog = [];
    chrome.storage.local.remove(['errorLog']);
  }
}

const errorHandler = new ErrorHandler();

// Global error capture
self.addEventListener('error', (event) => {
  errorHandler.logError(event.error, 'Global error');
});

self.addEventListener('unhandledrejection', (event) => {
  errorHandler.logError(new Error(event.reason), 'Promise rejection');
});

tip Best Practices

  1. Performance Monitoring: Regularly monitor background script performance
  2. Resource Management: Clean up unnecessary data and listeners promptly
  3. Error Recovery: Implement graceful error handling and recovery mechanisms
  4. Debugging Tools: Use Chrome Developer Tools to debug Service Worker

note Summary This chapter introduced the core concepts and practical applications of Chrome extension Background Scripts. Mastering Service Worker lifecycle management, event listening mechanisms, scheduled task processing, and data management are the foundation for developing efficient Chrome extensions. In the next chapter, we will learn how to develop Popup dialog interfaces for user interaction.

Categories