浏览器扩展与脚本开发教程
基于 Chrome Manifest V3 与 Tampermonkey 油猴脚本的实战开发指南
目录
一、快速入门
1.1 Chrome 扩展 vs 油猴脚本
| 对比维度 | Chrome 扩展 | 油猴脚本 |
|---|---|---|
| 安装方式 | Chrome 商店或开发者模式 | Tampermonkey 管理器 |
| 权限管理 | 严格的权限声明 | 灵活的 @grant 声明 |
| 复杂度 | 高,支持完整应用 | 中等,适合快速开发 |
| 持久化 | Service Worker 后台运行 | 页面注入,生命周期短 |
| 适用场景 | 完整功能型工具 | 网页增强、自动化 |
| 开发门槛 | 需要理解扩展架构 | 类似普通 JS 脚本 |
| 调试 | 多页面调试 | 单页面调试 |
| 更新机制 | 自动更新 | 手动或自动更新 |
| 跨浏览器 | 需要适配 Firefox/Edge | Tampermonkey 跨平台 |
选择建议:
- 需要后台服务、复杂功能 → Chrome 扩展
- 快速网页增强、自动化 → 油猴脚本
- 需要发布到应用商店 → Chrome 扩展
- 个人使用或小范围分享 → 油猴脚本
1.2 开发环境搭建
Chrome 扩展开发
-
启用开发者模式
- 访问
chrome://extensions/ - 开启右上角"开发者模式"开关
- 访问
-
安装必要的开发工具
- Chrome DevTools(内置)
- React DevTools(如使用 React)
- Redux DevTools(如使用 Redux)
-
推荐编辑器插件
- VS Code:Chrome Extension Tools
- WebStorm:内置支持
油猴脚本开发
-
安装 Tampermonkey
- Chrome Web Store 搜索"Tampermonkey"
- 点击"添加到 Chrome"安装
-
访问脚本库
- GreasyFork:https://greasyfork.org/zh-CN
- SleazyFork:https://sleazyfork.org/zh-CN
-
推荐工具
- 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('扩展运行正常!');
});
安装步骤:
- 创建文件夹
my-extension - 放入上述文件和图标
- 访问
chrome://extensions/ - 点击"加载已解压的扩展程序"
- 选择文件夹完成安装
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
});
});
})();
安装步骤:
- 打开 Tampermonkey 管理面板
- 点击"+"添加新脚本
- 粘贴代码并保存
- 刷新目标网页查看效果
二、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 调试
-
打开 Service Worker 控制台
- 访问
chrome://extensions/ - 找到扩展,点击"Service Worker"链接
- 打开开发者工具
- 访问
-
查看日志
console.log('普通日志');
console.warn('警告信息');
console.error('错误信息');
console.table({ name: 'John', age: 30 }); -
断点调试
debugger; // 断点
const data = processData();
Popup 调试
-
右键点击扩展图标
- 选择"检查"
- 自动打开 DevTools
-
在 Popup 中调试
<script>
console.log('Popup 已加载');
debugger;
</script>
Content Script 调试
-
打开页面 DevTools
- F12 或右键"检查"
- 在 Console 中输入
window.postMessage
-
从扩展发送消息
// background.js
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { action: 'debug' });
});
7.2 脚本调试技巧
油猴脚本调试
-
在 Tampermonkey 中查看
- 点击油猴图标
- 选择"管理面板"
- 点击脚本编辑器的"Console"按钮
-
使用 GM_notification 调试
GM_notification({
title: '调试信息',
text: '脚本已运行到此位置',
timeout: 2000
}); -
添加调试日志
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 API | GM_setValue |
| 发布 | Chrome 商店 | GreasyFork 等 |
| 更新 | 自动更新 | 手动/自动 |
| 调试 | 多组件调试 | 单页面调试 |
| 社区 | Chrome Dev 社区 | GreasyFork 社区 |
| 适用场景 | 完整应用 | 快速增强 |
选择建议
选择 Chrome 扩展,如果:
- 需要后台服务
- 需要复杂 UI
- 需要发布到商店
- 需要多种权限组合
- 需要长期维护
选择油猴脚本,如果:
- 快速实现功能
- 个人使用
- 小范围分享
- 不需要后台服务
- 学习扩展开发
总结
本教程涵盖了浏览器扩展和油猴脚本开发的全部核心内容:
- 快速入门 - 从零开始创建扩展和脚本
- 核心机制 - 深入理解 Manifest V3 和组件协作
- 高级功能 - 网络拦截、消息传递、存储管理
- 实战项目 - 4个完整项目案例
- 工程实践 - 调试、测试、优化、发布
核心要点:
- 遵循最小权限原则
- 重视安全性和用户体验
- 使用最佳实践和设计模式
- 持续迭代和优化
希望这份教程能帮助你掌握浏览器扩展和脚本开发!