Chapter 05 Background Scripts
Chapter 5: Background Scripts
Learning Objectives
- Understand the concept and working mechanism of Background Scripts
- Master Service Worker applications in Chrome extensions
- Learn event listening and lifecycle management
- 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
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
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
- Performance Monitoring: Regularly monitor background script performance
- Resource Management: Clean up unnecessary data and listeners promptly
- Error Recovery: Implement graceful error handling and recovery mechanisms
- 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.