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
- Event-driven architecture: Service workers wake up for events and sleep after idle
- Keep state minimal: Use
chrome.storageinstead of global variables - Async/await: Use modern async patterns
- 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
- Use
activeTabover broad host permissions: Only request access when user interacts - Request optional permissions: Use
chrome.permissions.request()for non-essential permissions - 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()ornew 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
- Open
chrome://extensions/ - Enable Developer mode (toggle top-right)
- Click Load unpacked
- 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'
});
}
微信扫一扫