油猴脚本进阶:给你的fetch请求拦截脚本加上“开关”和“日志面板”
2026/4/17 10:45:31 网站建设 项目流程

油猴脚本工程化实战:打造带开关与日志面板的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 常见问题解决

  • 拦截不生效

    1. 检查@run-at是否为document-start
    2. 确认没有其他脚本覆盖了fetch
    3. 尝试在控制台手动执行拦截代码
  • 样式冲突

    1. 确保使用Shadow DOM
    2. 为所有class添加特定前缀
    3. 避免使用!important
  • 性能下降

    1. 检查是否有无限循环的日志
    2. 限制日志缓冲区大小
    3. 复杂操作放入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 转译新语法

但注意保留必要的注释和文档,方便他人理解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询