一个油猴页面调试面板:非前端的效率工具实践

一个油猴页面调试面板:非前端的效率工具实践

最近又在折腾几个 WordPress Page,AI 大模型变强以后,各种折腾都更顺畅了。我不是专业前端开发者,更多是靠浏览器开发者工具一点点试、改、刷新。问题也很明显:每次刷新样式就没了,改到一半忘记复制保存是常态,尤其是频繁调整 HTML 和 CSS 的时候,整个流程非常碎片化,也很容易打断思路。

2026.06.22,进一步增强。现在支持在 footer 中注入 JavaScript 脚本,而且自动刷新,调试网页更方便了!并且支持保存上一次弹窗的位置和大小,支持 Ctrl + Shift + F 一键还原~

昨夜我突发奇想,能否把临时的 CSS 持久化固定在浏览器,给 ChatGPT 聊了聊这个事情,最终形成了这个脚本。从我的角度来看目前这版已经是兼顾性能和功能的完成态了:

  1. CSS 持久化存储机制
    每个页面(基于 origin + pathname + query)都有独立的 CSS 存储空间,修改后自动写入 localStorage,刷新不丢失
  2. 页面元素点击选取 + selector 自动生成
    点击页面任意元素即可生成尽可能稳定的 CSS selector,同时尽量过滤 WordPress / 页面构建器的干扰 class
  3. DOM 高亮辅助定位
    点击元素后会自动生成高亮框,实时跟随元素位置,方便快速确认修改目标。
  4. 可拖拽、可缩放的调试面板
    面板支持拖动位置、尺寸调整,并会记住上一次的位置状态
  5. 实时 CSS 应用与防抖保存机制
    输入 CSS 后不会立即频繁写入,而是通过 debounce 方式延迟保存,避免性能浪费
  6. 轻量级“开发者工具替代层”
    它并不试图替代 DevTools,而是补足 DevTools 在“持续编辑体验”上的缺口
  7. 支持快捷键打开和关闭
    目前绑定的是 Ctrl + Shift + F 打开面板,再按一次(或者按 Esc、右上角)关闭,关闭时不消耗任何资源
一个油猴页面调试面板:非前端的效率工具实践

我知道各位大佬肯定有更好的办法、有更高的效率,但就我这个连 CSS 属性都要查的人而言,这个小脚本对我来说还是蛮有用的。以前我在页面调样式的时候,总是在“修改 → 刷新 → 丢失 → 再找 → 再改”这个循环里消耗时间,现在这个循环被打断了,变成了“实时修改 → 持久保存 → 可回溯调整”。

而且用上之后,我发现他可能在未来还会用于非页面调试的场景...比如给客户的 Demo 展示,直接浏览器改了就行,随便刷新,随便造...真是机智如我😎😎😎


// ==UserScript==
// @name          Dev Panel - CSS & JS Injector
// @namespace     https://lyoo.net
// @version       0.2
// @description   Ctrl+Shift+F 打开开发注入面板,支持 Head CSS 注入与 Footer JS 注入,关闭或刷新页面不影响已保存内容
// @match         *://*/*
// @grant         none
// ==/UserScript==

(function () {
  const STYLE_ID = 'custom-css-injector';
  const SCRIPT_ID = 'custom-js-footer-injector';
  const PANEL_ID = 'dev-toolbox-panel';

  const PAGE_KEY = location.origin + location.pathname + location.search;
  const CSS_STORAGE_KEY = 'stored_custom_css_' + PAGE_KEY;
  const JS_STORAGE_KEY = 'stored_custom_js_' + PAGE_KEY;

  const PANEL_POS_KEY = 'dev_panel_pos_v1';
  const PANEL_SIZE_KEY = 'dev_panel_size_v1';

  let panelVisible = false;
  let lockMode = false;
  let currentMode = 'css';

  let dragMoveHandler = null;
  let dragUpHandler = null;
  let saveTimeout = null;
  let clickHandler = null;
  let selectorBar = null;
  let resizeObserver = null;
  let editor = null;
  let modeToggleBtn = null;
  let titleText = null;

  // =========================
  // Head CSS 注入层
  // =========================
  let style = document.getElementById(STYLE_ID);

  if (!style) {
    style = document.createElement('style');
    style.id = STYLE_ID;
    document.head.appendChild(style);
  }

  function loadCSS() {
    return localStorage.getItem(CSS_STORAGE_KEY) || '';
  }

  function safeApplyCSS(css) {
    try {
      const testSheet = new CSSStyleSheet();
      testSheet.replaceSync(css);

      requestAnimationFrame(() => {
        style.textContent = css;
      });
    } catch (e) {
      console.warn('CSS Syntax Error:', e);
    }
  }

  function saveCSS(css) {
    localStorage.setItem(CSS_STORAGE_KEY, css);
    safeApplyCSS(css);
  }

  safeApplyCSS(loadCSS());

  // =========================
  // Footer JS 注入层
  // =========================
  function loadJS() {
    return localStorage.getItem(JS_STORAGE_KEY) || '';
  }

  function removeInjectedJS() {
    const oldScript = document.getElementById(SCRIPT_ID);
    if (oldScript) oldScript.remove();
  }

  function safeApplyJS(js) {
    removeInjectedJS();

    if (!js.trim()) return;

    try {
      new Function(js);

      requestAnimationFrame(() => {
        const script = document.createElement('script');
        script.id = SCRIPT_ID;
        script.type = 'text/javascript';

        script.textContent = `
;(() => {
${js}
})();
//# sourceURL=dev-footer-injected-${location.host}.js
        `.trim();

        const footerTarget = document.body || document.documentElement;
        footerTarget.appendChild(script);
      });
    } catch (e) {
      console.warn('JS Syntax Error:', e);
    }
  }

  function saveJS(js) {
    localStorage.setItem(JS_STORAGE_KEY, js);
    safeApplyJS(js);
  }

  safeApplyJS(loadJS());

  // =========================
  // DOM 高亮框
  // =========================
  let highlightBox = null;
  let lastTarget = null;

  function highlightElement(el) {
    if (!el) return;

    if (!highlightBox) {
      highlightBox = document.createElement('div');
      highlightBox.style.position = 'fixed';
      highlightBox.style.zIndex = 2147483647;
      highlightBox.style.pointerEvents = 'none';
      highlightBox.style.border = '2px solid #38bdf8';
      highlightBox.style.background = 'rgba(56,189,248,0.08)';
      document.body.appendChild(highlightBox);
    }

    const rect = el.getBoundingClientRect();

    highlightBox.style.left = rect.left + 'px';
    highlightBox.style.top = rect.top + 'px';
    highlightBox.style.width = rect.width + 'px';
    highlightBox.style.height = rect.height + 'px';
  }

  function clearHighlight() {
    if (highlightBox) {
      highlightBox.remove();
      highlightBox = null;
    }

    lastTarget = null;
  }

  // =========================
  // Selector 生成器
  // =========================
  function getSelector(el) {
    if (!el || el.nodeType !== 1) return '';

    if (el.id) {
      try {
        if (document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1) {
          return `#${el.id}`;
        }
      } catch (e) {}
    }

    const badPatterns = ['wpb_', 'vc_', 'container', 'row', 'wp_'];

    function cleanClasses(node) {
      return Array.from(node.classList || [])
        .filter(c => !badPatterns.some(b => c.includes(b)) && c !== PANEL_ID);
    }

    function getElementSpecificSelector(node, isBottomLayer) {
      const tagName = node.tagName.toLowerCase();

      if (tagName === 'body' || tagName === 'html') return {
        selector: tagName,
        classes: []
      };

      const classes = cleanClasses(node);
      let selector = '';

      if (classes.length > 0) {
        if (isBottomLayer) {
          selector = `.${classes.join('.')}`;
        } else {
          const sortedClasses = [...classes].sort((a, b) => a.length - b.length);
          selector = `.${sortedClasses.slice(0, 2).join('.')}`;
        }
      } else {
        selector = tagName;
      }

      if (isBottomLayer || classes.length === 0) {
        const parent = node.parentElement;

        if (parent) {
          const siblings = Array.from(parent.children).filter(child => {
            if (classes.length > 0) {
              return classes.every(c => child.classList.contains(c));
            }

            return child.tagName.toLowerCase() === tagName;
          });

          if (siblings.length > 1) {
            const index = Array.from(parent.children).indexOf(node) + 1;
            selector += `:nth-child(${index})`;
          }
        }
      }

      return { selector, classes };
    }

    let rawPathNodes = [];
    let current = el;

    while (current && current.tagName) {
      if (current.id) {
        try {
          if (document.querySelectorAll(`#${CSS.escape(current.id)}`).length === 1) {
            rawPathNodes.unshift({
              value: `#${current.id}`,
              isUnique: true
            });
            break;
          }
        } catch (e) {}
      }

      const depthFromBottom = rawPathNodes.length;
      const isBottomLayer = depthFromBottom <= 1;
      const { selector, classes } = getElementSpecificSelector(current, isBottomLayer);

      let isUnique = false;

      if (classes.length > 0) {
        const shortestClass = [...classes].sort((a, b) => a.length - b.length)[0];

        try {
          if (document.querySelectorAll(`.${CSS.escape(shortestClass)}`).length <= 2) {
            isUnique = true;
          }
        } catch (e) {}
      }

      rawPathNodes.unshift({ value: selector, isUnique });
      current = current.parentElement;
    }

    let finalPath = [];

    if (rawPathNodes.length <= 9) {
      finalPath = rawPathNodes.map(i => i.value);
    } else {
      const top = rawPathNodes.slice(0, 4);
      const bottom = rawPathNodes.slice(-5);
      const middle = rawPathNodes.slice(4, -5);

      const preserved = middle.filter(i => i.isUnique).map(i => i.value);

      finalPath = Array.from(new Set([
        ...top.map(i => i.value),
        ...preserved,
        ...bottom.map(i => i.value)
      ]));
    }

    return finalPath.join(' ');
  }

  // =========================
  // 面板工具函数
  // =========================
  function scrollToRight(el) {
    setTimeout(() => {
      el.scrollLeft = el.scrollWidth;
    }, 10);
  }

  function updateModeUI() {
    if (!editor || !modeToggleBtn || !titleText) return;

    if (currentMode === 'css') {
      titleText.innerText = 'Dev Injector Panel · CSS Head';
      modeToggleBtn.innerText = 'CSS in Head';
      modeToggleBtn.title = '当前为 CSS 注入模式,点击切换到 JS 页脚注入';
      editor.placeholder = 'Write CSS here. It will be injected into <head>.';
    } else {
      titleText.innerText = 'Dev Injector Panel · JS Footer';
      modeToggleBtn.innerText = 'JS in Footer';
      modeToggleBtn.title = '当前为 JS 注入模式,点击切换到 CSS 头部注入';
      editor.placeholder = 'Write JavaScript here. It will be injected before </body>.';
    }
  }

  function persistEditorValue() {
    if (!editor) return;

    if (currentMode === 'css') {
      saveCSS(editor.value);
    } else {
      saveJS(editor.value);
    }
  }

  function switchMode() {
    if (!editor) return;

    if (saveTimeout) {
      clearTimeout(saveTimeout);
      saveTimeout = null;
    }

    persistEditorValue();

    currentMode = currentMode === 'css' ? 'js' : 'css';
    editor.value = currentMode === 'css' ? loadCSS() : loadJS();

    updateModeUI();
    editor.focus();
  }

  // =========================
  // UI
  // =========================
  function createPanel() {
    if (document.getElementById(PANEL_ID)) return;

    panelVisible = true;
    currentMode = 'css';

    let panelStyle = document.getElementById('dev-panel-scroll-style');

    if (!panelStyle) {
      panelStyle = document.createElement('style');
      panelStyle.id = 'dev-panel-scroll-style';
      panelStyle.innerHTML = `
        #${PANEL_ID} .custom-selector-bar::-webkit-scrollbar {
          height: 4px !important;
          background: transparent !important;
        }

        #${PANEL_ID} .custom-selector-bar::-webkit-scrollbar-thumb {
          background: rgba(56,189,248,0.4) !important;
          border-radius: 2px !important;
        }

        #${PANEL_ID} .custom-selector-bar::-webkit-scrollbar-thumb:hover {
          background: rgba(56,189,248,0.7) !important;
        }

        #${PANEL_ID} .custom-selector-bar::-webkit-scrollbar-track {
          background: transparent !important;
        }
      `;
      document.head.appendChild(panelStyle);
    }

    const panel = document.createElement('div');
    panel.id = PANEL_ID;

    const pos = JSON.parse(localStorage.getItem(PANEL_POS_KEY) || '{}');
    const size = JSON.parse(localStorage.getItem(PANEL_SIZE_KEY) || '{}');

    panel.style.cssText = `
      position: fixed;
      top: ${pos.top || '70%'};
      left: ${pos.left || '4%'};
      width: ${size.width || '400px'};
      height: ${size.height || '800px'};
      min-width: 400px;
      min-height: 180px;
      background: #ffffff;
      border: 1px solid rgba(0,0,0,0.15);
      border-radius: 12px;
      box-shadow: 0 25px 70px rgba(0,0,0,0.22), 0 2px 15px rgba(0,0,0,0.08);
      z-index: 2147483647;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
      resize: both;
    `;

    const header = document.createElement('div');
    header.style.cssText = `
      height: 32px;
      background: linear-gradient(to right, #f8fafc, #f1f5f9);
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0 12px 0 16px;
      cursor: move;
      user-select: none;
      font-size: 13px;
      font-weight: 600;
      color: #1e293b;
      border-bottom: 1px solid rgba(0,0,0,0.06);
      flex-shrink: 0;
    `;

    titleText = document.createElement('span');
    titleText.innerText = 'Dev Injector Panel · CSS Head';

    const headerActions = document.createElement('div');
    headerActions.style.cssText = `
      display: flex;
      align-items: center;
      gap: 8px;
      cursor: default;
    `;

    modeToggleBtn = document.createElement('button');
    modeToggleBtn.type = 'button';
    modeToggleBtn.innerText = 'CSS in Head';
    modeToggleBtn.style.cssText = `
      height: 22px;
      padding: 0 10px;
      border: 1px solid rgba(14,165,233,0.35);
      border-radius: 999px;
      background: #e0f2fe;
      color: #0369a1;
      font-size: 12px;
      font-weight: 600;
      line-height: 20px;
      cursor: pointer;
      outline: none;
      font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
    `;

    modeToggleBtn.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      switchMode();
    };

    const closeBtn = document.createElement('span');
    closeBtn.innerText = '✕';
    closeBtn.style.cssText = `
      cursor: pointer;
      color: #64748b;
      font-weight: bold;
      font-size: 14px;
      padding: 4px;
      line-height: 1;
    `;

    closeBtn.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      destroyPanel();
    };

    headerActions.appendChild(modeToggleBtn);
    headerActions.appendChild(closeBtn);

    header.appendChild(titleText);
    header.appendChild(headerActions);

    selectorBar = document.createElement('div');
    selectorBar.className = 'custom-selector-bar';
    selectorBar.style.cssText = `
      padding: 6px 16px;
      font-family: Consolas, Menlo, Monaco, monospace;
      font-size: 14px;
      background: #0f172a;
      color: #38bdf8;
      border-bottom: 1px solid rgba(255,255,255,0.08);
      white-space: nowrap;
      overflow-x: auto;
      flex-shrink: 0;
    `;
    selectorBar.innerText = 'click element to get selector';

    selectorBar.onclick = () => {
      navigator.clipboard.writeText(selectorBar.innerText);
    };

    editor = document.createElement('textarea');
    editor.style.cssText = `
      flex: 1;
      border: none;
      outline: none;
      padding: 16px;
      font-family: Consolas, Menlo, Monaco, monospace;
      font-size: 14px;
      line-height: 1.6;
      color: #334155;
      background: #ffffff;
      resize: none;
    `;

    editor.value = loadCSS();

    editor.addEventListener('input', () => {
      const modeAtInput = currentMode;
      const valueAtInput = editor.value;

      clearTimeout(saveTimeout);

      saveTimeout = setTimeout(() => {
        if (modeAtInput === 'css') {
          saveCSS(valueAtInput);
        } else {
          saveJS(valueAtInput);
        }
      }, 300);
    });

    panel.appendChild(header);
    panel.appendChild(selectorBar);
    panel.appendChild(editor);
    document.body.appendChild(panel);

    updateModeUI();

    resizeObserver = new ResizeObserver(() => {
      localStorage.setItem(PANEL_SIZE_KEY, JSON.stringify({
        width: panel.style.width || panel.offsetWidth + 'px',
        height: panel.style.height || panel.offsetHeight + 'px'
      }));
    });

    resizeObserver.observe(panel);

    let dragging = false;
    let ox = 0;
    let oy = 0;

    header.addEventListener('mousedown', (e) => {
      if (headerActions.contains(e.target)) return;

      dragging = true;
      ox = e.clientX - panel.offsetLeft;
      oy = e.clientY - panel.offsetTop;
    });

    dragMoveHandler = function (e) {
      if (!dragging) return;

      panel.style.left = (e.clientX - ox) + 'px';
      panel.style.top = (e.clientY - oy) + 'px';
    };

    dragUpHandler = function () {
      dragging = false;

      localStorage.setItem(PANEL_POS_KEY, JSON.stringify({
        left: panel.style.left,
        top: panel.style.top
      }));

      localStorage.setItem(PANEL_SIZE_KEY, JSON.stringify({
        width: panel.offsetWidth + 'px',
        height: panel.offsetHeight + 'px'
      }));
    };

    document.addEventListener('mousemove', dragMoveHandler);
    document.addEventListener('mouseup', dragUpHandler);

    bindClick();
    scrollToRight(selectorBar);
    editor.focus();
  }

  function destroyPanel() {
    const panel = document.getElementById(PANEL_ID);

    if (panel) panel.remove();

    panelVisible = false;

    unbindClick();
    clearHighlight();

    if (dragMoveHandler) document.removeEventListener('mousemove', dragMoveHandler);
    if (dragUpHandler) document.removeEventListener('mouseup', dragUpHandler);

    dragMoveHandler = null;
    dragUpHandler = null;

    if (saveTimeout) {
      clearTimeout(saveTimeout);
      saveTimeout = null;
    }

    if (resizeObserver) {
      resizeObserver.disconnect();
      resizeObserver = null;
    }

    editor = null;
    selectorBar = null;
    modeToggleBtn = null;
    titleText = null;
  }

  function bindClick() {
    if (clickHandler) return;

    clickHandler = function (e) {
      if (!panelVisible) return;

      const panel = document.getElementById(PANEL_ID);

      if (panel && panel.contains(e.target)) return;

      if (lockMode) return;

      e.preventDefault();
      e.stopPropagation();

      const target = e.target;

      if (target === lastTarget) return;

      lastTarget = target;

      const run = () => {
        const selector = getSelector(target);

        if (!selectorBar) return;

        selectorBar.innerText = selector;
        highlightElement(target);
        scrollToRight(selectorBar);
      };

      if (window.requestIdleCallback) {
        requestIdleCallback(run);
      } else {
        setTimeout(run, 0);
      }
    };

    document.addEventListener('click', clickHandler, true);
  }

  function unbindClick() {
    if (!clickHandler) return;

    document.removeEventListener('click', clickHandler, true);
    clickHandler = null;
  }

  // =========================
  // 快捷键
  // Ctrl + Shift + F:打开 / 关闭面板
  // Ctrl + Alt + Shift + F:重置面板位置和尺寸
  // Ctrl + Shift + L:锁定 / 解锁页面元素选择
  // Esc:关闭面板
  // =========================
  document.addEventListener('keydown', (e) => {
    if (
      e.ctrlKey &&
      e.altKey &&
      e.shiftKey &&
      e.key.toLowerCase() === 'f'
    ) {
      e.preventDefault();

      localStorage.removeItem(PANEL_POS_KEY);
      localStorage.removeItem(PANEL_SIZE_KEY);

      const panel = document.getElementById(PANEL_ID);

      if (panel) {
        panel.style.left = '4%';
        panel.style.top = '20%';
        panel.style.width = '400px';
        panel.style.height = '800px';
      }

      return;
    }

    if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'f') {
      e.preventDefault();

      const panel = document.getElementById(PANEL_ID);

      if (panel) {
        destroyPanel();
      } else {
        createPanel();
      }
    }

    if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'l') {
      lockMode = !lockMode;
    }

    if (e.key === 'Escape') {
      if (panelVisible) destroyPanel();
    }
  });
})();
「一个油猴页面调试面板:非前端的效率工具实践」有 4 条评论

发表评论

请输入关键词…