跳到主要内容

浏览器扩展与脚本开发教程

基于 Chrome Manifest V3 与 Tampermonkey 油猴脚本的实战开发指南

目录


一、快速入门

1.1 Chrome 扩展 vs 油猴脚本

对比维度Chrome 扩展油猴脚本
安装方式Chrome 商店或开发者模式Tampermonkey 管理器
权限管理严格的权限声明灵活的 @grant 声明
复杂度高,支持完整应用中等,适合快速开发
持久化Service Worker 后台运行页面注入,生命周期短
适用场景完整功能型工具网页增强、自动化
开发门槛需要理解扩展架构类似普通 JS 脚本
调试多页面调试单页面调试
更新机制自动更新手动或自动更新
跨浏览器需要适配 Firefox/EdgeTampermonkey 跨平台

选择建议:

  • 需要后台服务、复杂功能 → Chrome 扩展
  • 快速网页增强、自动化 → 油猴脚本
  • 需要发布到应用商店 → Chrome 扩展
  • 个人使用或小范围分享 → 油猴脚本

1.2 开发环境搭建

Chrome 扩展开发

  1. 启用开发者模式

    • 访问 chrome://extensions/
    • 开启右上角"开发者模式"开关
  2. 安装必要的开发工具

    • Chrome DevTools(内置)
    • React DevTools(如使用 React)
    • Redux DevTools(如使用 Redux)
  3. 推荐编辑器插件

    • VS Code:Chrome Extension Tools
    • WebStorm:内置支持

油猴脚本开发

  1. 安装 Tampermonkey

    • Chrome Web Store 搜索"Tampermonkey"
    • 点击"添加到 Chrome"安装
  2. 访问脚本库

  3. 推荐工具

    • VS Code + Tampermonkey 插件
    • Chrome DevTools 调试

1.3 第一个 Chrome 扩展

创建最小化的 Hello World 扩展:

manifest.json

{
"manifest_version": 3,
"name": "我的第一个扩展",
"version": "1.0",
"description": "Hello World 扩展",
"action": {
"default_popup": "popup.html",
"default_title": "点击我"
},
"permissions": [],
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}

popup.html

<!DOCTYPE html>
<html>
<head>
<style>
body {
width: 300px;
padding: 15px;
font-family: Arial, sans-serif;
}
button {
width: 100%;
padding: 10px;
margin-top: 10px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #357ae8;
}
</style>
</head>
<body>
<h2>Hello World!</h2>
<p>这是我的第一个 Chrome 扩展</p>
<button id="testBtn">测试按钮</button>
<script src="popup.js"></script>
</body>
</html>

popup.js

document.getElementById('testBtn').addEventListener('click', () => {
alert('扩展运行正常!');
});

安装步骤:

  1. 创建文件夹 my-extension
  2. 放入上述文件和图标
  3. 访问 chrome://extensions/
  4. 点击"加载已解压的扩展程序"
  5. 选择文件夹完成安装

1.4 第一个油猴脚本

创建一个简单的油猴脚本:

// ==UserScript==
// @name 网页标题修改器
// @namespace https://example.com
// @version 1.0
// @description 修改网页标题并添加自定义样式
// @author YourName
// @match *://*/*
// @grant GM_addStyle
// @grant GM_notification
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';

// 等待页面加载完成
window.addEventListener('load', () => {
// 修改标题
const originalTitle = document.title;
document.title = `${originalTitle}`;

// 注入样式
GM_addStyle(`
body {
margin: 0;
}
.gm-custom-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px;
text-align: center;
z-index: 9999;
font-weight: bold;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
`);

// 创建横幅
const banner = document.createElement('div');
banner.className = 'gm-custom-banner';
banner.textContent = '本页面已由油猴脚本增强';
document.body.insertBefore(banner, document.body.firstChild);

// 发送通知
GM_notification({
title: '脚本运行成功',
text: '网页标题已修改',
timeout: 3000
});
});
})();

安装步骤:

  1. 打开 Tampermonkey 管理面板
  2. 点击"+"添加新脚本
  3. 粘贴代码并保存
  4. 刷新目标网页查看效果

二、Chrome 扩展核心

2.1 Manifest V3 深度解析

完整 manifest.json 配置示例

{
"manifest_version": 3,
"name": "高级扩展示例",
"version": "2.0.0",
"description": "一个功能完整的 Chrome 扩展示例",
"author": "Your Name",
"homepage_url": "https://example.com",

"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",
"48": "icons/icon48.png"
},
"default_title": "点击打开扩展",
"theme_icons": [
{
"light": "icons/icon16-light.png",
"dark": "icons/icon16-dark.png",
"size": 16
}
]
},

"background": {
"service_worker": "background/service-worker.js",
"type": "module"
},

"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"js": ["content/main.js"],
"css": ["content/styles.css"],
"run_at": "document_idle",
"all_frames": false
}
],

"options_page": "options/options.html",
"options_ui": {
"page": "options/options.html",
"open_in_tab": false
},

"permissions": [
"storage",
"tabs",
"notifications",
"contextMenus",
"declarativeNetRequest",
"webNavigation"
],

"host_permissions": [
"https://api.example.com/*",
"https://*.google.com/*"
],

"web_accessible_resources": [{
"resources": ["assets/*", "icons/*"],
"matches": ["https://*.example.com/*"]
}]
}

关键配置详解

manifest_version

  • V2(已弃用) → V3(当前标准)
  • V3 主要变化:
    • Service Worker 替代 Background Pages
    • Declarative Net Request 替代 Web Request Blocking
    • 更严格的 CSP 策略
    • 远程代码注入限制

background.service_worker

  • 长期运行的后台脚本
  • 不能访问 DOM
  • 支持模块化(type: "module")
  • 支持事件监听(chrome.runtime.onInstalled)

content_scripts

  • 注入到目标页面
  • 可访问页面 DOM
  • 与页面脚本隔离
  • 支持 run_at 时机:
    • document_start - 在 CSS 加载后
    • document_end - 在 DOM 加载后
    • document_idle - 最佳时机(默认)

permissions vs host_permissions

  • permissions:通用权限(storage, tabs, notifications)
  • host_permissions:网站访问权限
  • V3 分离两者以增强安全性

2.2 核心组件详解

Service Worker(后台脚本)

生命周期管理

// background/service-worker.js

// 安装事件
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('扩展首次安装');
// 初始化设置
chrome.storage.local.set({
settings: { theme: 'light', notifications: true }
});
// 创建右键菜单
createContextMenu();
} else if (details.reason === 'update') {
console.log('扩展更新,从版本', details.previousVersion);
// 执行数据迁移
migrateData(details.previousVersion);
}
});

// 启动事件
chrome.runtime.onStartup.addListener(() => {
console.log('浏览器启动');
// 恢复定时任务
scheduleTasks();
});

// 消息监听
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getData') {
fetchAndSendData().then(sendResponse);
return true; // 保持消息通道打开
}
});

// 错误处理
self.addEventListener('error', (event) => {
console.error('Service Worker 错误:', event.error);
});

// 自增版本更新缓存
const CACHE_NAME = 'my-cache-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/icons/icon48.png',
'/assets/config.json'
]);
})
);
});

Popup(弹出面板)

最佳实践

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
:root {
--primary-color: #4285f4;
--text-color: #333;
--bg-color: #f5f5f5;
}

body {
width: 320px;
min-height: 400px;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
}

.header {
background: var(--primary-color);
color: white;
padding: 15px;
text-align: center;
}

.content {
padding: 15px;
}

.stats-card {
background: white;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.btn {
width: 100%;
padding: 10px;
margin-top: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}

.btn-primary {
background: var(--primary-color);
color: white;
}

.btn-primary:hover {
background: #357ae8;
}

.loading {
text-align: center;
padding: 20px;
}
</style>
</head>
<body>
<div class="header">
<h3>我的扩展</h3>
</div>
<div class="content">
<div id="stats">
<!-- 统计数据将动态加载 -->
<div class="loading">加载中...</div>
</div>
<button id="refreshBtn" class="btn btn-primary">刷新数据</button>
<button id="settingsBtn" class="btn">打开设置</button>
</div>
<script src="popup.js"></script>
</body>
</html>
// popup.js
document.addEventListener('DOMContentLoaded', () => {
loadStats();
setupEventListeners();
});

async function loadStats() {
try {
const stats = await chrome.storage.local.get(['usageStats']);
renderStats(stats.usageStats || {});
} catch (error) {
showError('加载数据失败');
}
}

function renderStats(stats) {
const statsContainer = document.getElementById('stats');
statsContainer.innerHTML = `
<div class="stats-card">
<h4>使用统计</h4>
<p>页面访问: ${stats.pageViews || 0}</p>
<p>功能使用: ${stats.featureUsage || 0}</p>
</div>
`;
}

function setupEventListeners() {
document.getElementById('refreshBtn').addEventListener('click', async () => {
const btn = document.getElementById('refreshBtn');
btn.textContent = '刷新中...';
btn.disabled = true;

try {
await loadStats();
btn.textContent = '刷新数据';
btn.disabled = false;
} catch (error) {
showError('刷新失败');
}
});

document.getElementById('settingsBtn').addEventListener('click', () => {
chrome.tabs.create({ url: chrome.runtime.getURL('options/options.html') });
});
}

function showError(message) {
const statsContainer = document.getElementById('stats');
statsContainer.innerHTML = `
<div class="stats-card" style="color: red;">
${message}
</div>
`;
}

Options(设置页面)

<!-- options.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}

.form-group {
margin-bottom: 20px;
}

label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}

input[type="text"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}

.toggle-switch {
position: relative;
width: 50px;
height: 24px;
}

.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}

.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 24px;
}

.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}

input:checked + .slider {
background-color: #4285f4;
}

input:checked + .slider:before {
transform: translateX(26px);
}

.btn-save {
background: #4285f4;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}

.status-message {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
display: none;
}

.status-success {
background: #d4edda;
color: #155724;
}

.status-error {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<h1>扩展设置</h1>

<div class="form-group">
<label>启用通知</label>
<label class="toggle-switch">
<input type="checkbox" id="notificationsEnabled">
<span class="slider"></span>
</label>
</div>

<div class="form-group">
<label for="theme">主题</label>
<select id="theme">
<option value="light">浅色</option>
<option value="dark">深色</option>
<option value="auto">跟随系统</option>
</select>
</div>

<div class="form-group">
<label for="refreshInterval">刷新间隔(秒)</label>
<input type="number" id="refreshInterval" min="5" max="300" value="30">
</div>

<div class="form-group">
<label for="customMessage">自定义消息</label>
<textarea id="customMessage" rows="3" placeholder="输入自定义消息..."></textarea>
</div>

<button id="saveBtn" class="btn-save">保存设置</button>
<div id="statusMessage" class="status-message"></div>

<script src="options.js"></script>
</body>
</html>
// options.js
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
document.getElementById('saveBtn').addEventListener('click', saveSettings);
});

async function loadSettings() {
const settings = await chrome.storage.local.get('settings');
const data = settings.settings || {
notificationsEnabled: true,
theme: 'light',
refreshInterval: 30,
customMessage: ''
};

document.getElementById('notificationsEnabled').checked = data.notificationsEnabled;
document.getElementById('theme').value = data.theme;
document.getElementById('refreshInterval').value = data.refreshInterval;
document.getElementById('customMessage').value = data.customMessage;
}

async function saveSettings() {
const settings = {
notificationsEnabled: document.getElementById('notificationsEnabled').checked,
theme: document.getElementById('theme').value,
refreshInterval: parseInt(document.getElementById('refreshInterval').value),
customMessage: document.getElementById('customMessage').value
};

try {
await chrome.storage.local.set({ settings });
showStatus('设置已保存', 'success');

// 通知背景脚本
chrome.runtime.sendMessage({ action: 'settingsChanged', settings });
} catch (error) {
showStatus('保存失败', 'error');
}
}

function showStatus(message, type) {
const statusEl = document.getElementById('statusMessage');
statusEl.textContent = message;
statusEl.className = `status-message status-${type}`;
statusEl.style.display = 'block';

setTimeout(() => {
statusEl.style.display = 'none';
}, 3000);
}

2.3 消息传递机制

Chrome 扩展支持三种消息传递方式:

1. 一次性消息(一次请求-响应)

Content Script → Background

// content.js
chrome.runtime.sendMessage(
{ action: 'getData', params: { id: 123 } },
(response) => {
console.log('收到响应:', response);
}
);

// background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getData') {
// 异步处理
fetchData(request.params.id).then(data => {
sendResponse({ success: true, data });
});
return true; // 保持消息通道打开
}
});

2. 长期连接(端口通信)

建立连接

// content.js
const port = chrome.runtime.connect({ name: 'my-port' });

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

port.onMessage.addListener((message) => {
console.log('收到消息:', message);
});

port.onDisconnect.addListener(() => {
console.log('连接已断开');
});

// background.js
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((message) => {
if (message.type === 'init') {
port.postMessage({ status: 'connected', timestamp: Date.now() });
}
});
});

3. Tab 间通信

Content Script → Popup

// popup.js
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { action: 'getPageData' }, (response) => {
console.log('页面数据:', response);
});
});

// content.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getPageData') {
sendResponse({
title: document.title,
url: window.location.href,
content: document.body.innerText.substring(0, 100)
});
}
});

跨 Tab 通信

// 发送消息到所有 Tab
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, { action: 'broadcast', data: 'Hello' });
});
});

2.4 存储管理

Chrome 提供 storage API,支持同步和本地存储。

Storage API 基础用法

// 存储数据
chrome.storage.local.set({
key1: 'value1',
key2: { nested: 'data' },
preferences: { theme: 'dark' }
});

// 读取数据
chrome.storage.local.get(['key1', 'key2'], (result) => {
console.log('key1:', result.key1);
console.log('key2:', result.key2);
});

// 读取所有数据
chrome.storage.local.get(null, (data) => {
console.log('所有数据:', data);
});

// 删除数据
chrome.storage.local.remove(['key1']);

// 清空所有数据
chrome.storage.local.clear();

// 监听数据变化
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' && changes.key1) {
console.log('key1 变化:', changes.key1.newValue);
}
});

同步存储(跨设备同步)

// 同步存储(需要在 manifest.json 中声明 "permissions": ["storage"])
chrome.storage.sync.set({
settings: { theme: 'light' }
});

chrome.storage.sync.get(['settings'], (result) => {
console.log('同步设置:', result.settings);
});

// 注意限制:
// - 总大小: 102,400 字节(每个项目)
// - 项目数量: 512 个
// - 字符串长度: 每个项目 2,180 字符

封装存储工具类

// utils/storage.js
class StorageManager {
constructor(namespace = 'app') {
this.namespace = namespace;
this.storage = chrome.storage.local;
}

async get(key, defaultValue = null) {
const data = await this._get(key);
return data !== undefined ? data : defaultValue;
}

async set(key, value) {
const data = {};
data[`${this.namespace}_${key}`] = value;
return this.storage.set(data);
}

async remove(key) {
return this.storage.remove(`${this.namespace}_${key}`);
}

async clear() {
const data = await this._get(null);
const keys = Object.keys(data)
.filter(k => k.startsWith(this.namespace))
.map(k => k.split('_').slice(1).join('_'));
return this.storage.remove(keys);
}

onChange(key, callback) {
const listener = (changes, namespace) => {
const fullKey = `${this.namespace}_${key}`;
if (namespace === 'local' && changes[fullKey]) {
callback(changes[fullKey].oldValue, changes[fullKey].newValue);
}
};
chrome.storage.onChanged.addListener(listener);
return () => chrome.storage.onChanged.removeListener(listener);
}

_get(keys) {
return new Promise((resolve) => {
if (keys === null) {
this.storage.get(null, resolve);
} else if (Array.isArray(keys)) {
const fullKeys = keys.map(k => `${this.namespace}_${k}`);
this.storage.get(fullKeys, (data) => {
const result = {};
keys.forEach(k => {
result[k] = data[`${this.namespace}_${k}`];
});
resolve(result);
});
} else {
this.storage.get(`${this.namespace}_${keys}`, (data) => {
resolve(data[`${this.namespace}_${keys}`]);
});
}
});
}
}

// 使用示例
const storage = new StorageManager('myExtension');
await storage.set('user', { name: 'John', age: 30 });
const user = await storage.get('user');
storage.onChange('user', (oldVal, newVal) => {
console.log('用户数据变化:', oldVal, '->', newVal);
});

三、Content Script 注入

3.1 动态注入策略

程序化注入

// background.js - 根据条件动态注入
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url.includes('example.com')) {
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['content/dynamic.js']
});
}
});

// 或注入代码字符串
chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
console.log('动态注入的代码');
document.body.style.border = '2px solid red';
}
});

条件注入

// 根据用户设置决定是否注入
chrome.storage.local.get(['enabledSites'], ({ enabledSites = [] }) => {
if (enabledSites.includes(window.location.hostname)) {
initContentScript();
}
});

function initContentScript() {
// 注入逻辑
addCustomStyles();
createToolbar();
}

多个 Content Script 协作

// content1.js - 基础功能
window.contentBridge = {
version: '1.0',
ready: false,
modules: {}
};

// content2.js - 扩展功能
window.contentBridge.ready = true;
window.contentBridge.modules.advanced = {
init() {
console.log('高级模块初始化');
}
};

// 检查依赖
function waitForDependency() {
return new Promise((resolve) => {
const check = () => {
if (window.contentBridge?.ready) {
resolve();
} else {
setTimeout(check, 50);
}
};
check();
});
}

waitForDependency().then(() => {
window.contentBridge.modules.advanced.init();
});

3.2 DOM 操作与事件监听

安全的 DOM 操作

// 等待元素出现
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}

const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});

observer.observe(document.body, {
childList: true,
subtree: true
});

setTimeout(() => {
observer.disconnect();
reject(new Error(`元素 ${selector} 未找到`));
}, timeout);
});
}

// 使用示例
waitForElement('#main-content').then(element => {
console.log('元素已加载:', element);
element.style.border = '2px solid green';
});

事件委托模式

// 使用事件委托避免内存泄漏
document.addEventListener('click', (e) => {
// 处理动态添加的元素
if (e.target.matches('.dynamic-button')) {
handleButtonClick(e.target);
} else if (e.target.closest('.dynamic-container')) {
handleContainerClick(e.target.closest('.dynamic-container'));
}
});

function handleButtonClick(button) {
console.log('按钮被点击:', button);
}

function handleContainerClick(container) {
console.log('容器被点击:', container);
}

MutationObserver 监听 DOM 变化

// 监听特定元素的变化
const targetNode = document.querySelector('#watch-container');
const config = {
attributes: true,
childList: true,
subtree: true
};

const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
console.log('子节点变化:', mutation.addedNodes);
} else if (mutation.type === 'attributes') {
console.log('属性变化:', mutation.attributeName);
}
});
});

observer.observe(targetNode, config);

// 停止观察
// observer.disconnect();

3.3 CSS 注入与样式修改

动态注入样式

// 方法 1: 使用 GM_addStyle(油猴脚本)
GM_addStyle(`
.custom-toolbar {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9999;
}
`);

// 方法 2: 创建 style 标签
function injectCSS(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
return style;
}

injectCSS(`
.highlight {
background: yellow;
font-weight: bold;
}
`);

// 方法 3: 修改元素样式
const element = document.querySelector('#target');
element.style.setProperty('background', '#4285f4', 'important');

响应式样式注入

// 根据页面主题注入不同样式
function getTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}

function injectThemeBasedStyles() {
const theme = getTheme();
const styles = {
light: `
.custom-container {
background: white;
color: #333;
}
`,
dark: `
.custom-container {
background: #1a1a1a;
color: #f0f0f0;
}
`
};

injectCSS(styles[theme]);
}

// 监听主题变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', injectThemeBasedStyles);

injectThemeBasedStyles();

3.4 页面通信与 API 调用

调用页面原生 API

// 安全地调用页面变量
function safeCallPageAPI() {
try {
if (typeof window.pageAPI !== 'undefined') {
const result = window.pageAPI.getData();
console.log('页面 API 返回:', result);
}
} catch (error) {
console.error('调用页面 API 失败:', error);
}
}

// 调用页面函数
function callPageFunction(functionName, ...args) {
if (typeof window[functionName] === 'function') {
return window[functionName](...args);
}
throw new Error(`函数 ${functionName} 不存在`);
}

Hook 页面函数

// 拦截页面函数调用
function hookFunction(obj, functionName, hook) {
const original = obj[functionName];
obj[functionName] = function(...args) {
const result = original.apply(this, args);
hook.apply(this, [result, ...args]);
return result;
};
}

// 使用示例
hookFunction(window, 'fetch', (response, url, options) => {
console.log('拦截 fetch 请求:', url);
return response;
});

hookFunction(console, 'log', (result, ...args) => {
console.log('拦截 console.log:', args);
return result;
});

劫持 XMLHttpRequest

// 劫持 XHR 请求
const originalXHR = window.XMLHttpRequest;

window.XMLHttpRequest = function() {
const xhr = new originalXHR();

const originalOpen = xhr.open;
const originalSend = xhr.send;

xhr.open = function(method, url) {
this._method = method;
this._url = url;
return originalOpen.apply(this, arguments);
};

xhr.send = function(data) {
console.log('XHR 请求:', this._method, this._url, data);

const originalOnLoad = this.onload;
this.onload = function() {
console.log('XHR 响应:', this.responseText);
if (originalOnLoad) originalOnLoad.apply(this, arguments);
};

return originalSend.apply(this, arguments);
};

return xhr;
};

四、权限与安全

4.1 权限系统详解

常用权限列表

权限说明使用场景
storage访问存储 API保存用户设置
tabs访问标签页信息获取当前页面 URL
notifications显示通知提醒用户
contextMenus右键菜单添加自定义菜单项
activeTab活动标签页访问临时访问当前页
scripting脚本注入动态注入 JS/CSS
declarativeNetRequest网络请求拦截广告拦截
webNavigation浏览历史监控页面导航
bookmarks书签管理书签操作
history浏览历史历史记录管理

host_permissions 详解

{
"host_permissions": [
"https://*/*", // 所有 HTTPS 网站
"http://*.example.com/*", // example.com 子域
"https://example.com/*", // 特定网站
"file:///*", // 本地文件
"<all_urls>" // 所有网站(谨慎使用)
]
}

最小权限原则

// 只在需要时请求权限
async function requestPermissionIfNeeded() {
const hasPermission = await chrome.permissions.contains({
origins: ['https://example.com/*']
});

if (!hasPermission) {
const granted = await chrome.permissions.request({
origins: ['https://example.com/*']
});

if (granted) {
console.log('权限已授予');
} else {
console.log('权限被拒绝');
}
}
}

// 在 popup 中请求
document.getElementById('requestPermissionBtn').addEventListener('click', () => {
chrome.permissions.request({
permissions: ['notifications'],
origins: ['https://example.com/*']
}, (granted) => {
if (granted) {
alert('权限已授予');
}
});
});

4.2 Content Security Policy

默认 CSP 策略

{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}

自定义 CSP 策略

{
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'none';"
}
}

避免的安全陷阱

// ❌ 危险:直接执行字符串
eval(userInput);

// ❌ 危险:使用 innerHTML 插入未转义内容
element.innerHTML = userInput;

// ✅ 安全:使用 textContent
element.textContent = userInput;

// ✅ 安全:使用 DOM API 创建元素
const div = document.createElement('div');
div.textContent = userInput;
element.appendChild(div);

4.3 安全最佳实践

输入验证

// 验证 URL
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}

// 验证并清理 HTML
function sanitizeHTML(html) {
const temp = document.createElement('div');
temp.textContent = html;
return temp.innerHTML;
}

// 使用 DOMPurify 清理 HTML
import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirtyHTML);

安全的第三方库加载

// ❌ 危险:从 CDN 加载脚本
{
"content_scripts": [{
"js": ["https://cdn.example.com/library.js"]
}]
}

// ✅ 安全:本地加载或使用 @require(油猴)
// ==UserScript==
// @require https://hanphone.top/npm/jquery@3.6.0/dist/jquery.min.js
// ==/UserScript==

// 或将库复制到扩展中
{
"web_accessible_resources": [{
"resources": ["lib/jquery.min.js"],
"matches": ["https://*/*"]
}]
}

防止 XSS 攻击

// 转义 HTML 特殊字符
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}

// 使用 setAttribute 而不是直接设置 HTML
const link = document.createElement('a');
link.setAttribute('href', sanitizeUrl(userUrl));
link.textContent = sanitizeHTML(userText);

五、高级功能

5.1 网络请求拦截

Declarative Net Request API

// manifest.json
{
"permissions": ["declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
"host_permissions": ["<all_urls>"],
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
}
}

rules.json

[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "*://example.com/ads/*",
"resourceTypes": ["script", "image"]
}
},
{
"id": 2,
"priority": 1,
"action": {
"type": "redirect",
"redirect": { "regexSubstitution": "https://cdn.example.com\\1" }
},
"condition": {
"regexFilter": "^https://example.com/(.*)$",
"resourceTypes": ["image"]
}
}
]

动态更新规则

// background.js

// 移除旧规则
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1, 2, 3]
});

// 添加新规则
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [
{
id: 4,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: '*://ads.example.com/*',
resourceTypes: ['script']
}
}
]
});

// 获取所有规则
chrome.declarativeNetRequest.getDynamicRules((rules) => {
console.log('当前规则:', rules);
});

Web Request API(V2)

chrome.webRequest.onBeforeRequest.addListener(
(details) => {
console.log('请求拦截:', details.url);
if (details.url.includes('ads')) {
return { cancel: true }; // 取消请求
}
},
{ urls: ['<all_urls>'] },
['blocking']
);

// 修改请求头
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
details.requestHeaders.push({
name: 'X-Custom-Header',
value: 'CustomValue'
});
return { requestHeaders: details.requestHeaders };
},
{ urls: ['<all_urls>'] },
['blocking', 'requestHeaders']
);

// 监听响应
chrome.webRequest.onCompleted.addListener(
(details) => {
console.log('请求完成:', details.url, details.statusCode);
},
{ urls: ['<all_urls>'] }
);

5.2 右键菜单

创建菜单

// background.js

chrome.runtime.onInstalled.addListener(() => {
// 创建主菜单项
chrome.contextMenus.create({
id: 'mainMenu',
title: '我的扩展',
contexts: ['all']
});

// 创建子菜单项
chrome.contextMenus.create({
id: 'copyText',
parentId: 'mainMenu',
title: '复制选中文本',
contexts: ['selection']
});

chrome.contextMenus.create({
id: 'openInNewTab',
parentId: 'mainMenu',
title: '在新标签页打开链接',
contexts: ['link']
});

chrome.contextMenus.create({
id: 'saveImage',
parentId: 'mainMenu',
title: '保存图片',
contexts: ['image']
});

// 分隔符
chrome.contextMenus.create({
id: 'separator1',
parentId: 'mainMenu',
type: 'separator'
});

chrome.contextMenus.create({
id: 'settings',
parentId: 'mainMenu',
title: '设置',
contexts: ['all']
});
});

// 处理点击事件
chrome.contextMenus.onClicked.addListener((info, tab) => {
switch (info.menuItemId) {
case 'copyText':
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (selectedText) => {
navigator.clipboard.writeText(selectedText);
alert('文本已复制');
},
args: [info.selectionText]
});
break;

case 'openInNewTab':
chrome.tabs.create({ url: info.linkUrl });
break;

case 'saveImage':
chrome.downloads.download({ url: info.srcUrl });
break;

case 'settings':
chrome.tabs.create({
url: chrome.runtime.getURL('options/options.html')
});
break;
}
});

5.3 通知系统

基础通知

chrome.notifications.create('notification-id', {
type: 'basic',
iconUrl: 'icons/icon48.png',
title: '通知标题',
message: '这是通知内容',
priority: 2
}, (notificationId) => {
console.log('通知已创建:', notificationId);
});

// 按钮通知
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: '操作确认',
message: '是否继续?',
buttons: [
{ title: '确定' },
{ title: '取消' }
]
});

// 进度通知
chrome.notifications.create('progress-notification', {
type: 'progress',
iconUrl: 'icons/icon48.png',
title: '下载进度',
message: '正在下载...',
progress: 45 // 0-100
});

// 监听点击事件
chrome.notifications.onClicked.addListener((notificationId) => {
console.log('通知被点击:', notificationId);
chrome.notifications.clear(notificationId);
});

// 监听按钮点击
chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
console.log('按钮被点击:', notificationId, buttonIndex);
if (buttonIndex === 0) {
// 确定按钮
executeAction();
}
});

通知封装工具

class NotificationManager {
static create(options) {
const defaultOptions = {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
priority: 2,
requireInteraction: false
};

chrome.notifications.create({
...defaultOptions,
...options
});
}

static showSuccess(title, message) {
this.create({
type: 'basic',
title: `${title}`,
message
});
}

static showError(title, message) {
this.create({
type: 'basic',
title: `${title}`,
message,
priority: 2
});
}

static showProgress(title, message, progress) {
this.create({
type: 'progress',
title,
message,
progress
});
}
}

// 使用示例
NotificationManager.showSuccess('操作成功', '数据已保存');
NotificationManager.showError('操作失败', '网络连接错误');
NotificationManager.showProgress('下载中', '文件下载进度...', 50);

5.4 开发者工具集成

DevTools Panel

// manifest.json
{
"devtools_page": "devtools.html"
}

devtools.html

<!DOCTYPE html>
<html>
<head>
<script src="devtools.js"></script>
</head>
</html>

devtools.js

chrome.devtools.panels.create(
'我的面板',
'icons/icon16.png',
'panel/panel.html',
(panel) => {
panel.onShown.addListener(() => {
console.log('面板显示');
});

panel.onHidden.addListener(() => {
console.log('面板隐藏');
});
}
);

panel/panel.html

<!DOCTYPE html>
<html>
<head>
<style>
body {
padding: 10px;
font-family: monospace;
}
.info {
padding: 10px;
background: #f0f0f0;
border-left: 3px solid #4285f4;
margin-bottom: 10px;
}
</style>
</head>
<body>
<h3>扩展调试面板</h3>
<div id="info"></div>
<script src="panel.js"></script>
</body>
</html>

panel/panel.js

chrome.devtools.inspectedWindow.eval(
'document.title',
(result, isException) => {
if (!isException) {
document.getElementById('info').innerHTML = `
<div class="info">页面标题: ${result}</div>
<div class="info">页面 URL: ${location.href}</div>
`;
}
}
);

Network 监听

// 监听网络请求
chrome.devtools.network.onRequestFinished.addListener((request) => {
if (request.request.url.includes('api')) {
request.getContent((content) => {
console.log('API 响应:', content);
});
}
});

六、油猴脚本开发

6.1 GM_API 全解

常用 GM API

API说明需要声明
GM_addStyle(css)注入 CSS@grant GM_addStyle
GM_notification(options)显示通知@grant GM_notification
GM_xmlhttpRequest(options)跨域请求@grant GM_xmlhttpRequest
GM_setValue(name, value)存储数据@grant GM_setValue
GM_getValue(name, default)读取数据@grant GM_getValue
GM_deleteValue(name)删除数据@grant GM_deleteValue
GM_openInTab(url, options)打开新标签@grant GM_openInTab
GM_download(url, filename)下载文件@grant GM_download
GM_registerMenuCommand(name, callback)注册菜单@grant GM_registerMenuCommand
GM_unregisterMenuCommand(id)注销菜单@grant GM_unregisterMenuCommand
GM_setClipboard(text)复制到剪贴板@grant GM_setClipboard

使用示例

// ==UserScript==
// @name API 示例脚本
// @namespace https://example.com
// @version 1.0
// @match *://*/*
// @grant GM_addStyle
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_openInTab
// @grant GM_download
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @require https://hanphone.top/npm/sweetalert2@11
// ==/UserScript==

// 存储数据
GM_setValue('lastVisit', Date.now());

// 读取数据
const lastVisit = GM_getValue('lastVisit', '首次访问');
console.log('上次访问:', new Date(lastVisit));

// 删除数据
GM_deleteValue('lastVisit');

// 跨域请求
GM_xmlhttpRequest({
method: 'GET',
url: 'https://api.example.com/data',
onload: (response) => {
console.log('响应数据:', response.responseText);
const data = JSON.parse(response.responseText);
processData(data);
},
onerror: (err) => {
console.error('请求失败:', err);
}
});

// 下载文件
GM_download({
url: 'https://example.com/file.pdf',
name: 'downloaded.pdf',
onload: () => {
GM_notification({
title: '下载完成',
text: '文件已保存'
});
}
});

// 注册菜单
const menuId = GM_registerMenuCommand('执行功能', () => {
alert('菜单被点击');
});

// 复制到剪贴板
GM_setClipboard('复制的文本内容');

// 使用外部库
Swal.fire({
title: '欢迎使用',
text: '这是一个油猴脚本示例',
icon: 'success'
});

6.2 实战案例:网页优化

网页元素精简

// ==UserScript==
// @name 网页精简工具
// @namespace https://example.com
// @version 1.0
// @match *://*.bilibili.com/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';

// 配置要隐藏的元素
const config = {
hideAds: true,
hideRecommendations: true,
hideComments: false
};

// 注入隐藏样式
function injectStyles() {
let styles = '';

if (config.hideAds) {
styles += `
.ad-report,
.ad-mask,
[class*="ad-"],
[class*="ad "],
[class*=" advertisement"] {
display: none !important;
}
`;
}

if (config.hideRecommendations) {
styles += `
.recommend-list,
.rcmd-list {
display: none !important;
}
`;
}

if (config.hideComments) {
styles += `
.comments-container,
#comment {
display: none !important;
}
`;
}

GM_addStyle(styles);
}

// 移除悬浮元素
function removeFloatingElements() {
const elements = document.querySelectorAll('[style*="position: fixed"]');
elements.forEach(el => {
if (!el.id.includes('gm-') && !el.textContent.includes('油猴')) {
el.remove();
}
});
}

// 创建控制面板
function createControlPanel() {
const panel = document.createElement('div');
panel.innerHTML = `
<div id="gm-control-panel" style="
position: fixed;
top: 10px;
right: 10px;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9999;
font-family: Arial, sans-serif;
">
<h3 style="margin: 0 0 10px 0;">控制面板</h3>
<label>
<input type="checkbox" id="gm-hide-ads" ${config.hideAds ? 'checked' : ''}>
隐藏广告
</label><br>
<label>
<input type="checkbox" id="gm-hide-rec" ${config.hideRecommendations ? 'checked' : ''}>
隐藏推荐
</label><br>
<label>
<input type="checkbox" id="gm-hide-cmt" ${config.hideComments ? 'checked' : ''}>
隐藏评论
</label>
</div>
`;

document.body.appendChild(panel);

// 添加事件监听
document.getElementById('gm-hide-ads').addEventListener('change', (e) => {
config.hideAds = e.target.checked;
GM_setValue('config', config);
location.reload();
});

document.getElementById('gm-hide-rec').addEventListener('change', (e) => {
config.hideRecommendations = e.target.checked;
GM_setValue('config', config);
location.reload();
});

document.getElementById('gm-hide-cmt').addEventListener('change', (e) => {
config.hideComments = e.target.checked;
GM_setValue('config', config);
location.reload();
});
}

// 初始化
function init() {
// 加载保存的配置
const savedConfig = GM_getValue('config', config);
Object.assign(config, savedConfig);

// 注入样式
injectStyles();

// 移除浮动元素
removeFloatingElements();

// 创建控制面板
setTimeout(createControlPanel, 1000);
}

// 注册菜单
GM_registerMenuCommand('打开控制面板', () => {
const panel = document.getElementById('gm-control-panel');
if (panel) {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
});

// 运行
init();
})();

6.3 数据抓取与存储

批量数据抓取

// ==UserScript==
// @name 数据抓取工具
// @namespace https://example.com
// @version 1.0
// @match https://example.com/list
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_download
// ==/UserScript==

(function() {
'use strict';

// 数据存储
const storage = {
data: [],

save() {
GM_setValue('scrapedData', this.data);
},

load() {
this.data = GM_getValue('scrapedData', []);
return this.data;
},

clear() {
this.data = [];
GM_setValue('scrapedData', this.data);
},

add(item) {
this.data.push(item);
this.save();
},

export(format = 'json') {
if (format === 'json') {
return JSON.stringify(this.data, null, 2);
} else if (format === 'csv') {
const headers = Object.keys(this.data[0]).join(',');
const rows = this.data.map(item =>
Object.values(item).map(v =>
typeof v === 'string' ? `"${v}"` : v
).join(',')
);
return [headers, ...rows].join('\n');
}
}
};

// 数据抓取
function scrapeData() {
const items = document.querySelectorAll('.item');

items.forEach(item => {
const data = {
title: item.querySelector('.title')?.textContent.trim(),
url: item.querySelector('a')?.href,
price: item.querySelector('.price')?.textContent,
rating: item.querySelector('.rating')?.getAttribute('data-rating')
};

if (data.title) {
storage.add(data);
}
});
}

// 翻页抓取
async function scrapeAllPages() {
let currentPage = 1;
const totalPages = parseInt(document.querySelector('.total-pages').textContent);

storage.clear();

while (currentPage <= totalPages) {
console.log(`正在抓取第 ${currentPage} 页...`);

scrapeData();

// 查找下一页按钮
const nextBtn = document.querySelector('.next-page');
if (nextBtn && currentPage < totalPages) {
nextBtn.click();
await new Promise(resolve => setTimeout(resolve, 2000));
}

currentPage++;
}

// 导出数据
const jsonData = storage.export('json');
downloadFile('data.json', jsonData);
}

// 下载文件
function downloadFile(filename, content) {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);

GM_download({
url: url,
name: filename,
saveAs: true,
onload: () => {
URL.revokeObjectURL(url);
alert('数据导出成功!');
}
});
}

// 创建工具栏
function createToolbar() {
const toolbar = document.createElement('div');
toolbar.innerHTML = `
<div id="gm-toolbar" style="
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9999;
">
<h3>数据抓取工具</h3>
<p>已抓取: ${storage.load().length} 条</p>
<button id="gm-scrape">抓取当前页</button><br><br>
<button id="gm-scrape-all">抓取所有页</button><br><br>
<button id="gm-export-json">导出 JSON</button><br><br>
<button id="gm-export-csv">导出 CSV</button><br><br>
<button id="gm-clear">清空数据</button>
</div>
`;

document.body.appendChild(toolbar);

// 事件监听
document.getElementById('gm-scrape').addEventListener('click', () => {
scrapeData();
updateCount();
});

document.getElementById('gm-scrape-all').addEventListener('click', scrapeAllPages);

document.getElementById('gm-export-json').addEventListener('click', () => {
downloadFile('data.json', storage.export('json'));
});

document.getElementById('gm-export-csv').addEventListener('click', () => {
downloadFile('data.csv', storage.export('csv'));
});

document.getElementById('gm-clear').addEventListener('click', () => {
storage.clear();
updateCount();
});
}

function updateCount() {
const countEl = document.querySelector('#gm-toolbar p');
if (countEl) {
countEl.textContent = `已抓取: ${storage.load().length}`;
}
}

// 初始化
createToolbar();
})();

6.4 自动化脚本

表单自动填充

// ==UserScript==
// @name 表单自动填充
// @namespace https://example.com
// @version 1.0
// @match *://example.com/form
// @grant GM_registerMenuCommand
// ==/UserScript==

(function() {
'use strict';

// 表单数据配置
const formData = {
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
address: '北京市朝阳区'
};

// 自动填充表单
function fillForm(data) {
Object.keys(data).forEach(key => {
const element = document.querySelector(`[name="${key}"], [id="${key}"]`);
if (element) {
element.value = data[key];
// 触发 input 事件
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}

// 自动提交
function submitForm() {
const submitBtn = document.querySelector('button[type="submit"], input[type="submit"]');
if (submitBtn) {
submitBtn.click();
}
}

// 完整流程
function autoFillAndSubmit() {
fillForm(formData);
setTimeout(submitForm, 1000);
}

// 注册菜单
GM_registerMenuCommand('自动填充', () => fillForm(formData));
GM_registerMenuCommand('填充并提交', () => autoFillAndSubmit());
})();

定时任务

// ==UserScript==
// @name 定时任务脚本
// @namespace https://example.com
// @version 1.0
// @match *://example.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// ==/UserScript==

(function() {
'use strict';

// 任务配置
const tasks = [
{
id: 'checkIn',
name: '每日签到',
interval: 24 * 60 * 60 * 1000, // 24小时
action: () => {
const checkInBtn = document.querySelector('.check-in-btn');
if (checkInBtn) {
checkInBtn.click();
GM_notification({
title: '签到成功',
text: '每日签到已完成'
});
}
}
},
{
id: 'collectReward',
name: '领取奖励',
interval: 1 * 60 * 60 * 1000, // 1小时
action: () => {
const rewardBtn = document.querySelector('.reward-btn');
if (rewardBtn && !rewardBtn.disabled) {
rewardBtn.click();
GM_notification({
title: '奖励已领取',
text: '奖励已领取到账户'
});
}
}
}
];

// 检查并执行任务
function checkAndExecuteTasks() {
tasks.forEach(task => {
const lastRun = GM_getValue(`${task.id}_lastRun`, 0);
const now = Date.now();

if (now - lastRun >= task.interval) {
console.log(`执行任务: ${task.name}`);
task.action();
GM_setValue(`${task.id}_lastRun`, now);
}
});
}

// 初始化
function init() {
// 首次运行时记录时间
tasks.forEach(task => {
if (!GM_getValue(`${task.id}_lastRun`)) {
GM_setValue(`${task.id}_lastRun`, Date.now());
}
});

// 立即检查一次
checkAndExecuteTasks();

// 定时检查
setInterval(checkAndExecuteTasks, 60000); // 每分钟检查一次
}

init();
})();

七、调试与测试

7.1 扩展调试技巧

Service Worker 调试

  1. 打开 Service Worker 控制台

    • 访问 chrome://extensions/
    • 找到扩展,点击"Service Worker"链接
    • 打开开发者工具
  2. 查看日志

    console.log('普通日志');
    console.warn('警告信息');
    console.error('错误信息');
    console.table({ name: 'John', age: 30 });
  3. 断点调试

    debugger; // 断点
    const data = processData();
  1. 右键点击扩展图标

    • 选择"检查"
    • 自动打开 DevTools
  2. 在 Popup 中调试

    <script>
    console.log('Popup 已加载');
    debugger;
    </script>

Content Script 调试

  1. 打开页面 DevTools

    • F12 或右键"检查"
    • 在 Console 中输入 window.postMessage
  2. 从扩展发送消息

    // background.js
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    chrome.tabs.sendMessage(tabs[0].id, { action: 'debug' });
    });

7.2 脚本调试技巧

油猴脚本调试

  1. 在 Tampermonkey 中查看

    • 点击油猴图标
    • 选择"管理面板"
    • 点击脚本编辑器的"Console"按钮
  2. 使用 GM_notification 调试

    GM_notification({
    title: '调试信息',
    text: '脚本已运行到此位置',
    timeout: 2000
    });
  3. 添加调试日志

    const DEBUG = true;

    function debugLog(...args) {
    if (DEBUG) {
    console.log('[Debug]', ...args);
    }
    }

    debugLog('当前步骤:', step);

7.3 性能优化

减少重排和重绘

// ❌ 低效:频繁操作 DOM
for (let i = 0; i < 1000; i++) {
document.body.appendChild(createElement(i));
}

// ✅ 高效:使用文档片段
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
fragment.appendChild(createElement(i));
}
document.body.appendChild(fragment);

防抖和节流

// 防抖
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}

// 节流
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}

// 使用示例
const handleScroll = throttle(() => {
console.log('滚动事件');
}, 200);

window.addEventListener('scroll', handleScroll);

懒加载

// 图片懒加载
function lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]');

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});

images.forEach(img => observer.observe(img));
}

lazyLoadImages();

7.4 错误处理

全局错误捕获

// Chrome 扩展
self.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
// 发送错误日志
sendErrorLog(event.error);
});

self.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误:', event.reason);
});

// 油猴脚本
window.addEventListener('error', (event) => {
console.error('页面错误:', event.error);
});

window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误:', event.reason);
});

异步错误处理

// async/await 错误处理
async function fetchData() {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}

// Promise 错误处理
fetchData()
.then(data => processData(data))
.catch(error => handleError(error));

用户友好的错误提示

function showError(message, error) {
console.error(message, error);

// 显示用户友好的错误
const errorDiv = document.createElement('div');
errorDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f44336;
color: white;
padding: 15px;
border-radius: 4px;
z-index: 9999;
max-width: 300px;
`;
errorDiv.textContent = message;
document.body.appendChild(errorDiv);

setTimeout(() => errorDiv.remove(), 5000);
}

八、实战项目

8.1 项目一:网页广告拦截器

项目结构

adblocker/
├── manifest.json
├── background.js
├── popup.html
├── popup.js
├── rules.json
└── icons/
├── 16.png
├── 48.png
└── 128.png

核心代码

manifest.json

{
"manifest_version": 3,
"name": "广告拦截器",
"version": "1.0",
"description": "简单高效的网页广告拦截器",
"permissions": [
"storage",
"declarativeNetRequest"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/16.png",
"48": "icons/48.png"
}
},
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
}
}

background.js

chrome.runtime.onInstalled.addListener(async () => {
// 加载拦截规则
const response = await fetch(chrome.runtime.getURL('rules.json'));
const rules = await response.json();

// 动态添加规则
chrome.declarativeNetRequest.updateDynamicRules({
addRules: rules
});

console.log('广告拦截器已安装');
});

// 更新规则
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'updateRules') {
updateRules(request.rules);
}
});

popup.js

document.addEventListener('DOMContentLoaded', () => {
loadStats();
document.getElementById('refreshBtn').addEventListener('click', updateRules);
});

async function loadStats() {
const stats = await chrome.storage.local.get(['blockedCount']);
document.getElementById('blockedCount').textContent =
stats.blockedCount || 0;
}

async function updateRules() {
const response = await fetch(chrome.runtime.getURL('rules.json'));
const rules = await response.json();

await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: rules.map(r => r.id),
addRules: rules
});

alert('规则已更新');
}

8.2 项目二:网页笔记工具

功能特点

  • 高亮网页文本
  • 添加笔记标注
  • 导出为 Markdown
  • 云端同步

核心代码

// content.js

class WebNote {
constructor() {
this.notes = [];
this.currentSelection = null;
this.init();
}

init() {
this.loadNotes();
this.renderNotes();
this.setupEventListeners();
}

setupEventListeners() {
document.addEventListener('mouseup', () => this.handleSelection());
}

handleSelection() {
const selection = window.getSelection();
if (selection.toString().trim()) {
this.currentSelection = selection;
this.showNoteDialog();
}
}

showNoteDialog() {
const dialog = document.createElement('div');
dialog.innerHTML = `
<div style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9999;
">
<h3>添加笔记</h3>
<textarea id="note-content" rows="5" cols="40" placeholder="输入笔记内容..."></textarea><br><br>
<button id="save-note">保存</button>
<button id="cancel-note">取消</button>
</div>
`;
document.body.appendChild(dialog);

document.getElementById('save-note').addEventListener('click', () => {
const content = document.getElementById('note-content').value;
if (content) {
this.addNote(content);
}
dialog.remove();
});

document.getElementById('cancel-note').addEventListener('click', () => {
dialog.remove();
});
}

addNote(content) {
const range = this.currentSelection.getRangeAt(0);
const span = document.createElement('span');
span.style.background = '#ffeb3b';
span.className = 'web-note-highlight';
span.dataset.noteId = Date.now();

range.surroundContents(span);

const note = {
id: Date.now(),
url: window.location.href,
text: this.currentSelection.toString(),
content: content,
timestamp: new Date().toISOString()
};

this.notes.push(note);
this.saveNotes();
}

async loadNotes() {
const data = await chrome.storage.local.get(['webNotes']);
this.notes = data.webNotes || [];
}

async saveNotes() {
await chrome.storage.local.set({ webNotes: this.notes });
}

renderNotes() {
// 恢复高亮
this.notes.forEach(note => {
// 根据存储的数据恢复高亮
});
}

exportMarkdown() {
let markdown = `# ${document.title}\n\n`;

this.notes.forEach(note => {
markdown += `> ${note.text}\n\n`;
markdown += `**笔记:** ${note.content}\n\n`;
markdown += `---\n\n`;
});

return markdown;
}
}

const webNote = new WebNote();

8.3 项目三:视频下载助手

功能特点

  • 检测网页视频
  • 一键下载视频
  • 支持多格式

核心代码

// ==UserScript==
// @name 视频下载助手
// @namespace https://example.com
// @version 1.0
// @match *://*/*
// @grant GM_download
// @grant GM_notification
// @grant GM_registerMenuCommand
// ==/UserScript==

(function() {
'use strict';

// 检测视频
function detectVideos() {
const videos = document.querySelectorAll('video');
const videoList = [];

videos.forEach((video, index) => {
const src = video.src || video.querySelector('source')?.src;
if (src) {
videoList.push({
id: index,
src: src,
duration: video.duration,
type: video.querySelector('source')?.type || 'video/mp4'
});
}
});

return videoList;
}

// 创建下载按钮
function createDownloadButton(video) {
const button = document.createElement('button');
button.textContent = '下载视频';
button.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background: #4285f4;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
z-index: 9999;
`;

button.addEventListener('click', () => {
downloadVideo(video.src, `video_${Date.now()}.mp4`);
});

return button;
}

// 下载视频
function downloadVideo(url, filename) {
GM_download({
url: url,
name: filename,
onload: () => {
GM_notification({
title: '下载完成',
text: `${filename} 下载成功`,
timeout: 3000
});
},
onerror: (error) => {
GM_notification({
title: '下载失败',
text: '请检查视频链接是否有效',
timeout: 3000
});
}
});
}

// 初始化
function init() {
const videos = detectVideos();

videos.forEach(video => {
const videoElement = document.querySelector(`video[src="${video.src}"]`);
if (videoElement) {
const container = videoElement.parentElement;
container.style.position = 'relative';
container.appendChild(createDownloadButton(video));
}
});

GM_registerMenuCommand('刷新视频列表', () => {
location.reload();
});
}

// 监听动态加载的视频
const observer = new MutationObserver(() => {
const newVideos = detectVideos();
// 处理新视频
});

observer.observe(document.body, { childList: true, subtree: true });

init();
})();

8.4 项目四:网站助手油猴脚本

功能特点

  • 网页元素优化
  • 快捷操作菜单
  • 自定义样式

核心代码

// ==UserScript==
// @name 网站助手
// @namespace https://example.com
// @version 1.0
// @match *://*.example.com/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';

// 配置
const config = {
removeAds: true,
optimizeLayout: true,
darkMode: false
};

// 加载配置
const savedConfig = GM_getValue('config', config);
Object.assign(config, savedConfig);

// 注入样式
GM_addStyle(`
/* 移除广告 */
.ad, .advertisement, [class*="ad-"] {
display: none !important;
}

/* 优化布局 */
body {
max-width: 1200px;
margin: 0 auto;
}

/* 深色模式 */
body.dark-mode {
background: #1a1a1a;
color: #f0f0f0;
}
`);

// 初始化功能
function init() {
removeAds();
optimizeLayout();
addToolbar();
}

// 移除广告
function removeAds() {
if (config.removeAds) {
const ads = document.querySelectorAll('.ad, .advertisement, [class*="ad-"]');
ads.forEach(ad => ad.remove());
}
}

// 优化布局
function optimizeLayout() {
if (config.optimizeLayout) {
// 自定义布局优化
}
}

// 添加工具栏
function addToolbar() {
const toolbar = document.createElement('div');
toolbar.id = 'site-assistant-toolbar';
toolbar.innerHTML = `
<button id="sa-remove-ads">移除广告</button>
<button id="sa-dark-mode">深色模式</button>
<button id="sa-settings">设置</button>
`;

document.body.appendChild(toolbar);

GM_addStyle(`
#site-assistant-toolbar {
position: fixed;
bottom: 20px;
right: 20px;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9999;
}
#site-assistant-toolbar button {
margin: 0 5px;
padding: 8px 12px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
`);

// 事件监听
document.getElementById('sa-remove-ads').addEventListener('click', () => {
removeAds();
});

document.getElementById('sa-dark-mode').addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
config.darkMode = !config.darkMode;
GM_setValue('config', config);
});

document.getElementById('sa-settings').addEventListener('click', () => {
showSettings();
});
}

// 显示设置
function showSettings() {
// 显示设置对话框
}

// 注册菜单
GM_registerMenuCommand('刷新页面', () => location.reload());
GM_registerMenuCommand('清除缓存', () => {
GM_setValue('config', config);
alert('配置已重置');
});

// 运行
init();
})();

九、发布与维护

9.1 Chrome 商店发布流程

1. 准备工作

  • 开发者账号(5美元注册费)
  • 扩展包文件
  • 图标和截图
  • 隐私政策(如涉及)

2. 打包扩展

# 创建 zip 包
zip -r my-extension.zip . -x "*.DS_Store" "*.git*" "node_modules/*"

# 或使用 Chrome 扩展打包工具

3. 填写信息

  • 扩展名称和描述
  • 类别
  • 语言
  • 隐私政策链接
  • 权限说明

4. 提交审核

  • 审核时间:1-3天
  • 拒绝原因:权限过多、违反政策等

9.2 版本管理与更新

版本号规则

{
"version": "1.2.3"
}
  • 主版本号(1):重大更新
  • 次版本号(2):新功能
  • 修订号(3):Bug 修复

自动更新

// background.js
chrome.runtime.onUpdateAvailable.addListener((details) => {
console.log('新版本可用:', details.version);

// 通知用户
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: '扩展更新',
message: '新版本已可用,点击立即更新'
});
});

9.3 用户反馈与迭代

收集反馈

// popup.js
document.getElementById('feedbackBtn').addEventListener('click', () => {
chrome.tabs.create({
url: 'mailto:developer@example.com?subject=扩展反馈'
});
});

数据分析

// background.js
function trackEvent(event, data) {
// 发送到分析服务
fetch('https://analytics.example.com/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, data })
});
}

chrome.runtime.onInstalled.addListener(() => {
trackEvent('install', { version: chrome.runtime.getManifest().version });
});

十、工具对比

Chrome 扩展 vs 油猴脚本 完整对比

特性Chrome 扩展油猴脚本
开发难度中等
功能强度中等
持久化Service Worker页面级别
权限管理严格灵活
跨域请求声明权限GM_xmlhttpRequest
存储storage APIGM_setValue
发布Chrome 商店GreasyFork 等
更新自动更新手动/自动
调试多组件调试单页面调试
社区Chrome Dev 社区GreasyFork 社区
适用场景完整应用快速增强

选择建议

选择 Chrome 扩展,如果:

  • 需要后台服务
  • 需要复杂 UI
  • 需要发布到商店
  • 需要多种权限组合
  • 需要长期维护

选择油猴脚本,如果:

  • 快速实现功能
  • 个人使用
  • 小范围分享
  • 不需要后台服务
  • 学习扩展开发

总结

本教程涵盖了浏览器扩展和油猴脚本开发的全部核心内容:

  1. 快速入门 - 从零开始创建扩展和脚本
  2. 核心机制 - 深入理解 Manifest V3 和组件协作
  3. 高级功能 - 网络拦截、消息传递、存储管理
  4. 实战项目 - 4个完整项目案例
  5. 工程实践 - 调试、测试、优化、发布

核心要点:

  • 遵循最小权限原则
  • 重视安全性和用户体验
  • 使用最佳实践和设计模式
  • 持续迭代和优化

希望这份教程能帮助你掌握浏览器扩展和脚本开发!