返回 Skill 列表
extension
分类: 开发与工程无需 API Key

chrome-extension-dev-skills

Develop Chrome extensions with Manifest V3. Covers architecture, service workers (background), content scripts, popups, message passing, storage, permissions, and security best practices.

person作者: awol2005exhubModelScope

Chrome Extension Development (Manifest V3)

This skill provides comprehensive guidance for developing Chrome extensions using Manifest V3, covering architecture, best practices, and common patterns.

Core Architecture

A Chrome extension consists of multiple components that work together:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Popup UI      │────▶│  Service Worker │────▶│  Content Script │
│  (User Interface)│     │ (Background Logic)│     │ (Page Interaction)│
└─────────────────┘     └─────────────────┘     └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
   ┌──────────┐           ┌──────────┐           ┌──────────┐
   │   User   │           │  Chrome  │           │ Web Page │
   └──────────┘           │   APIs   │           │  (DOM)   │
                          └──────────┘           └──────────┘

Component Responsibilities

| Component | Context | Purpose | Lifetime | |-----------|---------|---------|----------| | Service Worker | Extension | Event handling, background logic | Event-driven, ephemeral | | Content Script | Web Page | DOM manipulation, page interaction | Tied to page lifecycle | | Popup | Extension | User interface | Only when opened | | Options Page | Extension | Settings/configuration | When opened |

Manifest V3 Structure

Basic manifest.json Template

{
  "manifest_version": 3,
  "name": "Extension Name",
  "version": "1.0.0",
  "description": "Extension description",
  "icons": {
    "16": "icons/icon16.png",
    "32": "icons/icon32.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "32": "icons/icon32.png"
    },
    "default_title": "Click to open"
  },
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content.css"],
      "run_at": "document_idle"
    }
  ],
  "permissions": [
    "storage",
    "scripting",
    "tabs",
    "activeTab"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "web_accessible_resources": [
    {
      "resources": ["*.js", "*.css", "*.png", "*.svg"],
      "matches": ["*://*/*"]
    }
  ]
}

Key Manifest V3 Changes from V2

| Aspect | Manifest V2 | Manifest V3 | |--------|-------------|-------------| | Background | Persistent background page | Ephemeral service worker | | Network | webRequest blocking | declarativeNetRequest rules | | Remote Code | Allowed with restrictions | Completely prohibited | | CSP | Configurable | Strict default | | browser_action | Separate API | Unified action API |

Service Worker (Background Script)

Basic Structure

// background.js
// Service workers are ephemeral - use event-based architecture

// Extension installed/updated
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    console.log('Extension installed');
    // Initialize storage
    chrome.storage.local.set({ installed: true, version: '1.0.0' });
  } else if (details.reason === 'update') {
    console.log('Extension updated from', details.previousVersion);
  }
});

// Handle messages from content scripts or popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'getData') {
    handleGetData(request).then(sendResponse);
    return true; // Keep channel open for async
  }
});

// Tab events
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    console.log('Tab updated:', tab.url);
  }
});

// Action (toolbar icon) click
chrome.action.onClicked.addListener(async (tab) => {
  // Inject content script on click
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: showNotification,
    args: ['Hello from service worker!']
  });
});

async function handleGetData(request) {
  // Async operations
  const data = await chrome.storage.local.get('key');
  return { success: true, data };
}

function showNotification(message) {
  alert(message);
}

Service Worker Best Practices

  1. Event-driven architecture: Service workers wake up for events and sleep after idle
  2. Keep state minimal: Use chrome.storage instead of global variables
  3. Async/await: Use modern async patterns
  4. Error handling: Always wrap async operations

Content Scripts

Static Injection (via manifest)

// content.js - Runs in web page context
// Has access to DOM but limited extension APIs

console.log('Content script loaded on:', window.location.href);

// Read page data
const pageTitle = document.title;
const pageUrl = window.location.href;

// Modify DOM
const highlight = document.createElement('div');
highlight.style.cssText = `
  position: fixed;
  top: 10px;
  right: 10px;
  background: #4CAF50;
  color: white;
  padding: 10px;
  border-radius: 4px;
  z-index: 999999;
`;
highlight.textContent = 'Extension Active';
document.body.appendChild(highlight);

// Communicate with background
chrome.runtime.sendMessage({
  action: 'pageData',
  data: { title: pageTitle, url: pageUrl }
}, (response) => {
  console.log('Background response:', response);
});

// Listen for messages from background/popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'highlightText') {
    highlightTextOnPage(request.text);
    sendResponse({ success: true });
  }
  return true;
});

function highlightTextOnPage(text) {
  // Implementation for text highlighting
  const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );
  
  const nodes = [];
  while (walker.nextNode()) {
    if (walker.currentNode.textContent.includes(text)) {
      nodes.push(walker.currentNode);
    }
  }
  
  nodes.forEach(node => {
    const span = document.createElement('span');
    span.style.backgroundColor = 'yellow';
    const regex = new RegExp(`(${text})`, 'gi');
    span.innerHTML = node.textContent.replace(regex, '<mark>$1</mark>');
    node.parentNode.replaceChild(span, node);
  });
}

Dynamic Injection (from service worker)

// background.js - Inject content script programmatically

chrome.action.onClicked.addListener(async (tab) => {
  // Inject CSS
  await chrome.scripting.insertCSS({
    target: { tabId: tab.id },
    files: ['injected.css']
  });
  
  // Inject JavaScript
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['injected.js']
  });
  
  // Or execute function directly
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: (message) => {
      console.log('Injected message:', message);
      document.body.style.border = '5px solid red';
    },
    args: ['Hello from service worker']
  });
});

Popup Development

HTML Structure

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      width: 350px;
      min-height: 400px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #f5f5f5;
    }
    
    .header {
      background: #4CAF50;
      color: white;
      padding: 16px;
      text-align: center;
    }
    
    .content {
      padding: 16px;
    }
    
    .button {
      width: 100%;
      padding: 12px;
      background: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      margin-bottom: 8px;
    }
    
    .button:hover {
      background: #1976D2;
    }
    
    .status {
      margin-top: 16px;
      padding: 12px;
      background: white;
      border-radius: 4px;
      border-left: 4px solid #4CAF50;
    }
  </style>
</head>
<body>
  <div class="header">
    <h2>My Extension</h2>
  </div>
  <div class="content">
    <button class="button" id="btn-action">Perform Action</button>
    <button class="button" id="btn-options">Open Options</button>
    <div class="status" id="status">Ready</div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Popup JavaScript

// popup.js
// Popup scripts have access to most chrome.* APIs

document.addEventListener('DOMContentLoaded', async () => {
  // Load saved data
  const result = await chrome.storage.local.get(['count', 'settings']);
  updateUI(result);
  
  // Set up event listeners
  document.getElementById('btn-action').addEventListener('click', performAction);
  document.getElementById('btn-options').addEventListener('click', openOptions);
});

async function performAction() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  
  // Send message to content script
  try {
    const response = await chrome.tabs.sendMessage(tab.id, { 
      action: 'performAction',
      data: { timestamp: Date.now() }
    });
    
    updateStatus(`Success: ${response.message}`);
    
    // Update storage
    const result = await chrome.storage.local.get('count');
    const newCount = (result.count || 0) + 1;
    await chrome.storage.local.set({ count: newCount });
    
  } catch (error) {
    updateStatus(`Error: ${error.message}`);
    // Content script might not be injected - inject it
    await injectContentScript(tab.id);
  }
}

async function injectContentScript(tabId) {
  await chrome.scripting.executeScript({
    target: { tabId },
    files: ['content.js']
  });
  updateStatus('Content script injected. Try again.');
}

function openOptions() {
  chrome.runtime.openOptionsPage();
}

function updateUI(data) {
  const statusEl = document.getElementById('status');
  if (data.count !== undefined) {
    statusEl.textContent = `Action performed ${data.count} times`;
  }
}

function updateStatus(message) {
  document.getElementById('status').textContent = message;
}

Message Passing

One-time Messages

// Sending message
chrome.runtime.sendMessage({
  action: 'getUserData',
  userId: 123
}, (response) => {
  if (chrome.runtime.lastError) {
    console.error('Error:', chrome.runtime.lastError.message);
    return;
  }
  console.log('Response:', response);
});

// Receiving message (in background or content script)
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'getUserData') {
    fetchUserData(request.userId)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    
    return true; // Keep message channel open for async
  }
});

Long-lived Connections

// For continuous communication (e.g., streaming data)

// In content script or popup
const port = chrome.runtime.connect({ name: 'my-connection' });

port.postMessage({ type: 'init', data: {} });

port.onMessage.addListener((message) => {
  console.log('Received:', message);
});

port.onDisconnect.addListener(() => {
  console.log('Connection closed');
});

// In background script
chrome.runtime.onConnect.addListener((port) => {
  if (port.name === 'my-connection') {
    port.onMessage.addListener((message) => {
      if (message.type === 'init') {
        // Handle initialization
        port.postMessage({ type: 'ready' });
      }
    });
  }
});

Storage API

// chrome.storage is the recommended way to persist data

// Local storage (device-specific)
await chrome.storage.local.set({ 
  userSettings: { theme: 'dark', notifications: true },
  lastVisit: Date.now()
});

const result = await chrome.storage.local.get(['userSettings']);
console.log(result.userSettings);

// Sync storage (synced across devices, limited quota)
await chrome.storage.sync.set({ bookmarks: [] });

// Session storage (cleared when browser closes)
await chrome.storage.session.set({ tempData: {} });

// Storage change listeners
chrome.storage.onChanged.addListener((changes, namespace) => {
  for (let key in changes) {
    console.log(`Storage key "${key}" changed:`, {
      oldValue: changes[key].oldValue,
      newValue: changes[key].newValue,
      namespace
    });
  }
});

Permissions Guide

Common Permissions

| Permission | Purpose | Use Case | |------------|---------|----------| | activeTab | Temporary access to current tab | User-initiated actions | | scripting | Inject scripts/CSS | Content script injection | | storage | Persist data | Settings, user data | | tabs | Access tab information | Tab management | | alarms | Schedule tasks | Periodic background tasks | | notifications | Show notifications | User alerts | | contextMenus | Add right-click menus | Quick actions | | clipboardWrite | Copy to clipboard | Copy functionality | | offscreen | Offscreen documents | DOM parsing in service worker |

Permission Best Practices

  1. Use activeTab over broad host permissions: Only request access when user interacts
  2. Request optional permissions: Use chrome.permissions.request() for non-essential permissions
  3. Justify each permission: Chrome Web Store requires explanation for each permission
// Request optional permission
const granted = await chrome.permissions.request({
  permissions: ['bookmarks'],
  origins: ['https://example.com/*']
});

if (granted) {
  // Use the permission
}

Security Best Practices

1. Content Security Policy (CSP)

Manifest V3 has strict CSP by default:

  • No inline scripts
  • No eval() or new Function()
  • No remote code execution
// ❌ Don't do this
const script = document.createElement('script');
script.textContent = 'alert("inline script")';
document.head.appendChild(script);

// ✅ Do this instead
const script = document.createElement('script');
script.src = chrome.runtime.getURL('external.js');
document.head.appendChild(script);

2. Secure Communication

// Validate message sender
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  // Verify sender is from expected origin
  if (!sender.url || !sender.url.startsWith('https://trusted-site.com')) {
    return;
  }
  
  // Validate request structure
  if (!request || typeof request !== 'object') {
    return;
  }
  
  // Process validated request
});

3. Sanitize User Input

// Always sanitize before injecting into DOM
function sanitizeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// Use safe APIs
chrome.scripting.executeScript({
  target: { tabId },
  func: (safeData) => {
    // safeData is JSON-serialized, safe to use
    document.body.textContent = safeData;
  },
  args: [userInput] // Automatically serialized
});

Development Workflow

Project Structure

my-extension/
├── manifest.json
├── background.js
├── content.js
├── popup.html
├── popup.js
├── popup.css
├── options.html
├── options.js
├── icons/
│   ├── icon16.png
│   ├── icon32.png
│   ├── icon48.png
│   └── icon128.png
├── _locales/
│   └── en/
│       └── messages.json
└── lib/
    └── utils.js

Loading Extension in Chrome

  1. Open chrome://extensions/
  2. Enable Developer mode (toggle top-right)
  3. Click Load unpacked
  4. Select your extension folder

Debugging

  • Service Worker: Click "service worker" link on extensions page → opens DevTools
  • Popup: Right-click extension icon → "Inspect popup"
  • Content Script: Use regular DevTools on the web page

Hot Reload for Development

// Add to background.js for development
if (process.env.NODE_ENV === 'development') {
  chrome.runtime.onMessage.addListener((request) => {
    if (request.action === 'reload') {
      chrome.runtime.reload();
    }
  });
}

Publishing Checklist

Before submitting to Chrome Web Store:

  • [ ] Icons in all required sizes (16, 32, 48, 128)
  • [ ] Screenshots (1280x800 or 640x400)
  • [ ] Detailed description with features
  • [ ] Privacy policy URL (if collecting data)
  • [ ] Justify all permissions
  • [ ] Test on clean Chrome profile
  • [ ] Verify no remote code execution
  • [ ] Check for console errors
  • [ ] Test extension update path

Common Patterns

Pattern: User-Initiated Content Script

// Only inject when user clicks extension icon
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content.js']
  });
});

Pattern: Cross-Origin Fetch in Service Worker

// Service workers can make cross-origin requests
async function fetchExternalData(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    await chrome.storage.local.set({ cachedData: data });
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    // Return cached data
    const { cachedData } = await chrome.storage.local.get('cachedData');
    return cachedData;
  }
}

Pattern: Offscreen Document (for DOM parsing)

// For operations requiring DOM in service worker
// manifest.json:
{
  "permissions": ["offscreen"]
}

// background.js:
async function createOffscreenDocument() {
  await chrome.offscreen.createDocument({
    url: 'offscreen.html',
    reasons: ['DOM_PARSER'],
    justification: 'Parse HTML content'
  });
}

Resources