油猴脚本工程化实战:打造带开关与日志面板的Fetch拦截工具
每次调试网页请求时,你是否厌倦了反复修改油猴脚本代码、刷新页面的繁琐流程?当脚本意外拦截了不该处理的请求时,是否希望能一键关闭拦截功能而不必注释代码?本文将带你从工程化角度,为油猴脚本添加可视化控制界面和日志系统,让开发体验提升一个档次。
1. 拦截功能的核心架构设计
在开始编写代码前,我们需要明确几个关键设计原则。首先,拦截逻辑必须保持非侵入性,确保在不启用拦截时网页功能完全正常。其次,状态管理要足够轻量,避免影响页面性能。最后,所有功能模块应当解耦,便于单独调试和维护。
1.1 可插拔的Fetch拦截器
传统的覆盖fetch方法的方式存在明显缺陷——一旦替换就无法恢复。我们可以采用代理模式实现动态拦截:
const createInterceptor = () => { const originalFetch = window.fetch; let isActive = true; return { enable: () => isActive = true, disable: () => isActive = false, fetch: (...args) => { if (!isActive) return originalFetch(...args); console.log('[Intercepted]', args[0]); // 在这里添加你的拦截逻辑 return originalFetch(...args).then(response => { // 响应处理逻辑 return response; }); } }; };这种实现方式有三大优势:
- 拦截开关通过
isActive变量控制,无需重新加载页面 - 原始fetch方法被安全地保留,随时可以恢复
- 拦截逻辑集中在一处,便于维护
1.2 拦截器的生命周期管理
为确保脚本稳定运行,需要考虑几种特殊场景:
- 页面AJAX密集:添加请求队列避免阻塞
- 脚本多次加载:防止重复初始化
- 跨域请求:正确处理CORS头
建议采用单例模式管理拦截器实例:
const getInterceptor = (() => { let instance; return () => { if (!instance) { instance = createInterceptor(); window.fetch = instance.fetch; } return instance; }; })();2. 构建可视化控制面板
纯代码的开关不够直观,我们直接在页面上添加控制UI。油猴脚本操作DOM需要特别注意样式隔离,避免影响原页面。
2.1 创建浮动控制栏
使用Shadow DOM实现样式隔离的控制面板:
const createControlPanel = () => { const panel = document.createElement('div'); const shadow = panel.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` .control-bar { position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: rgba(0,0,0,0.7); color: white; padding: 10px; border-radius: 5px; font-family: Arial; } .toggle-btn { cursor: pointer; padding: 5px 10px; background: #4CAF50; border-radius: 3px; } `; const html = ` <div class="control-bar"> <span>拦截器状态:</span> <span id="status">已启用</span> <button class="toggle-btn" id="toggle">禁用</button> </div> `; shadow.appendChild(style); shadow.innerHTML += html; document.body.appendChild(panel); return shadow; };2.2 实现状态同步
将UI控件与拦截器状态绑定:
const initControls = () => { const shadow = createControlPanel(); const interceptor = getInterceptor(); const statusEl = shadow.getElementById('status'); const toggleBtn = shadow.getElementById('toggle'); const updateUI = () => { const isActive = interceptor.isActive; statusEl.textContent = isActive ? '已启用' : '已禁用'; toggleBtn.textContent = isActive ? '禁用' : '启用'; toggleBtn.style.background = isActive ? '#f44336' : '#4CAF50'; }; toggleBtn.addEventListener('click', () => { if (interceptor.isActive) { interceptor.disable(); } else { interceptor.enable(); } updateUI(); }); updateUI(); };3. 请求日志系统实现
完整的调试工具离不开日志功能,我们需要实时展示拦截的请求详情。
3.1 日志存储设计
采用环形缓冲区避免内存无限增长:
class RequestLogger { constructor(maxEntries = 200) { this.buffer = new Array(maxEntries); this.index = 0; this.maxEntries = maxEntries; this.count = 0; } add(entry) { this.buffer[this.index] = { timestamp: new Date(), ...entry }; this.index = (this.index + 1) % this.maxEntries; this.count = Math.min(this.count + 1, this.maxEntries); } getAll() { if (this.count < this.maxEntries) { return this.buffer.slice(0, this.index); } return [ ...this.buffer.slice(this.index), ...this.buffer.slice(0, this.index) ]; } }3.2 日志面板实现
扩展控制面板,添加可折叠的日志区域:
const enhancePanelWithLogs = (shadow) => { const logger = new RequestLogger(); const style = document.createElement('style'); style.textContent += ` .log-container { max-height: 300px; overflow-y: auto; margin-top: 10px; border-top: 1px solid #444; padding-top: 10px; } .log-entry { font-family: monospace; font-size: 12px; margin-bottom: 5px; padding: 3px; background: rgba(255,255,255,0.1); } .log-url { color: #64B5F6; } .log-method { color: #81C784; } `; shadow.appendChild(style); const logContainer = document.createElement('div'); logContainer.className = 'log-container'; shadow.querySelector('.control-bar').appendChild(logContainer); return { log: (entry) => { logger.add(entry); renderLogs(); }, toggle: () => { logContainer.style.display = logContainer.style.display === 'none' ? 'block' : 'none'; } }; function renderLogs() { logContainer.innerHTML = logger.getAll().map(entry => ` <div class="log-entry"> [${entry.timestamp.toLocaleTimeString()}] <span class="log-method">${entry.method}</span> <span class="log-url">${entry.url}</span> </div> `).join(''); } };4. 完整集成与优化
将各个模块组合起来,形成完整的解决方案。
4.1 主程序入口
(function() { 'use strict'; // 初始化拦截器 const interceptor = getInterceptor(); // 创建控制界面 const shadow = createControlPanel(); const { log } = enhancePanelWithLogs(shadow); // 绑定拦截器日志 const originalFetch = interceptor.fetch; interceptor.fetch = function(...args) { const [input, init] = args; const url = typeof input === 'string' ? input : input.url; const method = (init?.method || 'GET').toUpperCase(); log({ method, url }); return originalFetch.apply(this, args).then(response => { log({ method, url, status: response.status, type: response.type }); return response; }); }; // 初始化UI控制 initControls(); })();4.2 性能优化技巧
- 节流日志渲染:高频请求时避免频繁DOM操作
- Web Worker处理:复杂响应处理可移交给Worker
- 本地存储配置:使用GM_setValue保存用户偏好
// 示例:使用requestAnimationFrame节流 let pendingLogs = []; let isRendering = false; const efficientLog = (entry) => { pendingLogs.push(entry); if (!isRendering) { isRendering = true; requestAnimationFrame(() => { processLogs(); isRendering = false; }); } }; const processLogs = () => { if (pendingLogs.length > 0) { log(pendingLogs); pendingLogs = []; } };5. 高级功能扩展
基础功能完成后,可以考虑添加更多实用特性。
5.1 请求过滤规则
在控制面板中添加规则编辑器:
const addRuleEngine = (interceptor) => { const rules = []; const matchRule = (url) => { return rules.some(rule => { try { return new RegExp(rule.pattern).test(url); } catch { return url.includes(rule.pattern); } }); }; interceptor.fetch = function(...args) { const [input] = args; const url = typeof input === 'string' ? input : input.url; if (rules.length > 0 && !matchRule(url)) { return originalFetch(...args); } // ...原有拦截逻辑 }; return { addRule: (pattern) => rules.push({ pattern }), removeRule: (index) => rules.splice(index, 1), getRules: () => [...rules] }; };5.2 响应修改沙箱
安全地修改响应数据:
const createModifier = (response) => { const modifiers = []; const modifiedResponse = response.clone(); const originalJson = modifiedResponse.json.bind(modifiedResponse); modifiedResponse.json = async function() { let data = await originalJson(); for (const modifier of modifiers) { try { data = modifier(data); } catch (e) { console.error('Modifier error:', e); } } return data; }; return { response: modifiedResponse, addModifier: (fn) => modifiers.push(fn) }; }; // 使用示例 interceptor.fetch = function(...args) { return originalFetch(...args).then(response => { if (response.ok && response.headers.get('content-type')?.includes('json')) { const { response: modifiedResponse } = createModifier(response); modifiedResponse.addModifier(data => { data.injected = true; return data; }); return modifiedResponse; } return response; }); };6. 实际应用案例
让我们看几个实际场景中的应用示例。
6.1 API调试助手
在开发过程中,经常需要检查API请求和响应。配置以下规则:
ruleEngine.addRule('/api/');然后在控制台查看所有API请求的详细日志,包括:
- 请求URL和参数
- 响应状态码
- 响应时间
- 数据大小
6.2 数据Mocking
当后端API尚未完成时,可以拦截特定请求返回模拟数据:
interceptor.fetch = function(...args) { const [input] = args; const url = typeof input === 'string' ? input : input.url; if (url.includes('/mock/')) { return Promise.resolve(new Response( JSON.stringify({ mocked: true }), { status: 200, headers: { 'Content-Type': 'application/json' } } )); } return originalFetch(...args); };6.3 性能监控
记录请求耗时,统计页面API性能:
const perfMonitor = { stats: {}, record: (url, duration) => { if (!perfMonitor.stats[url]) { perfMonitor.stats[url] = { count: 0, total: 0, max: 0, min: Infinity }; } const stat = perfMonitor.stats[url]; stat.count++; stat.total += duration; stat.max = Math.max(stat.max, duration); stat.min = Math.min(stat.min, duration); } }; interceptor.fetch = function(...args) { const start = performance.now(); return originalFetch(...args).then(response => { const end = performance.now(); perfMonitor.record(response.url, end - start); return response; }); };7. 调试技巧与问题排查
即使设计完善的工具也会遇到问题,这里分享几个实用技巧。
7.1 常见问题解决
拦截不生效:
- 检查
@run-at是否为document-start - 确认没有其他脚本覆盖了fetch
- 尝试在控制台手动执行拦截代码
- 检查
样式冲突:
- 确保使用Shadow DOM
- 为所有class添加特定前缀
- 避免使用!important
性能下降:
- 检查是否有无限循环的日志
- 限制日志缓冲区大小
- 复杂操作放入Web Worker
7.2 调试控制台命令
在浏览器控制台暴露工具接口:
window.__debugInterceptor = { getStatus: () => interceptor.isActive, toggle: () => { if (interceptor.isActive) interceptor.disable(); else interceptor.enable(); }, clearLogs: () => logger.clear() };这样可以直接在控制台执行__debugInterceptor.toggle()来切换状态。
8. 发布与分享
完成开发后,你可能想分享这个工具给其他开发者。
8.1 脚本元信息配置
完整的油猴脚本头部信息示例:
// ==UserScript== // @name Advanced Fetch Interceptor // @namespace http://your-site.com // @version 1.0 // @description Professional fetch interceptor with UI controls // @author YourName // @match *://*/* // @run-at document-start // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // ==/UserScript==8.2 用户配置持久化
使用GM函数保存用户偏好:
// 保存状态 GM_setValue('interceptorEnabled', true); // 读取状态 const savedState = GM_getValue('interceptorEnabled', true); interceptor[savedState ? 'enable' : 'disable']();8.3 代码压缩与混淆
发布前建议使用工具处理代码:
- Terser 代码压缩
- webpack 打包模块
- Babel 转译新语法
但注意保留必要的注释和文档,方便他人理解。