Greasy Fork

移动端开发者工具

移动浏览器开发者调试工具,优化网络监控

目前为 2024-12-23 提交的版本。查看 最新版本

// ==UserScript==
// @name         移动端开发者工具
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  移动浏览器开发者调试工具,优化网络监控
// @author       Your name
// @match        *://*/*
// @grant        none
// ==/UserScript==
(function() {
    'use strict';
    
    // 样式定义
    const styles = `
        .devtools-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            height: 50%;
            background: #fff;
            z-index: 10000;
            border-bottom: 1px solid #ccc;
            display: none;
            flex-direction: column;
            font-family: Arial, sans-serif;
            transition: transform 0.3s ease;
            transform: translateY(-100%);
        }
        .devtools-container.expanded {
            transform: translateY(0);
        }
        .devtools-header {
            display: flex;
            padding: 5px;
            border-bottom: 1px solid #ccc;
            background: #f5f5f5;
        }
        .devtools-tab {
            padding: 5px 10px;
            margin-right: 5px;
            cursor: pointer;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-size: 14px;
        }
        .devtools-tab.active {
            background: #fff;
            border-bottom: none;
        }
        .devtools-content {
            flex: 1;
            overflow: auto;
            padding: 10px;
        }
        .devtools-panel {
            display: none;
            height: 100%;
        }
        .devtools-panel.active {
            display: block;
        }
        .toggle-devtools {
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 10001;
            padding: 5px 10px;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 14px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        .expand-collapse {
            position: absolute;
            bottom: -20px;
            left: 50%;
            transform: translateX(-50%);
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 0 0 5px 5px;
            padding: 2px 10px;
            cursor: pointer;
            z-index: 10001;
        }
        .element-highlight {
            position: absolute;
            background: rgba(137, 196, 244, 0.3);
            border: 1px solid #89C4F4;
            pointer-events: none;
            z-index: 9999;
        }
        .console-input {
            width: 100%;
            padding: 5px;
            border: 1px solid #ccc;
        }
        .console-output {
            margin: 5px 0;
            padding: 5px;
            background: #f5f5f5;
            border-radius: 3px;
        }
        .network-item {
            padding: 10px;
            border-bottom: 1px solid #eee;
            cursor: pointer;
        }
        .network-item:hover {
            background-color: #f0f0f0;
        }
        .network-details {
            display: none;
            padding: 10px;
            background: #f9f9f9;
            margin-top: 5px;
            border-radius: 4px;
        }
        .network-item.expanded .network-details {
            display: block;
        }
        .network-summary {
            display: flex;
            justify-content: space-between;
        }
        .network-summary > div {
            margin-right: 10px;
        }
        .network-details pre {
            white-space: pre-wrap;
            word-wrap: break-word;
            background: #fff;
            padding: 8px;
            border-radius: 4px;
            border: 1px solid #eee;
            margin: 5px 0;
            max-height: 300px;
            overflow-y: auto;
        }
    `;
    // 添加样式
    function addStyle(css) {
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }
    // 创建开发者工具UI
    function createDevToolsUI() {
        const container = document.createElement('div');
        container.className = 'devtools-container';
        container.innerHTML = `
            <div class="devtools-header">
                <div class="devtools-tab" data-panel="elements">元素</div>
                <div class="devtools-tab" data-panel="console">控制台</div>
                <div class="devtools-tab active" data-panel="network">网络</div>
                <div class="devtools-tab" data-panel="styles">样式</div>
            </div>
            <div class="devtools-content">
                <div class="devtools-panel elements-panel"></div>
                <div class="devtools-panel console-panel">
                    <div class="console-output-container"></div>
                    <input type="text" class="console-input" placeholder="输入JavaScript代码">
                </div>
                <div class="devtools-panel network-panel active"></div>
                <div class="devtools-panel styles-panel"></div>
            </div>
            <button class="expand-collapse">收起</button>
        `;
        const toggleButton = document.createElement('button');
        toggleButton.className = 'toggle-devtools';
        toggleButton.textContent = '开发工具';
        
        document.body.appendChild(container);
        document.body.appendChild(toggleButton);
        
        return {container, toggleButton};
    }
    // 元素检查器功能
    function initElementInspector(elementsPanel) {
        let highlight = document.createElement('div');
        highlight.className = 'element-highlight';
        document.body.appendChild(highlight);
        function updateHighlight(element) {
            const rect = element.getBoundingClientRect();
            highlight.style.top = rect.top + window.scrollY + 'px';
            highlight.style.left = rect.left + window.scrollX + 'px';
            highlight.style.width = rect.width + 'px';
            highlight.style.height = rect.height + 'px';
        }
        function showElementInfo(element) {
            elementsPanel.innerHTML = `
                <h3>选中的元素:</h3>
                <pre>${element.outerHTML}</pre>
                <h3>计算样式:</h3>
                <pre>${JSON.stringify(window.getComputedStyle(element), null, 2)}</pre>
            `;
        }
        document.addEventListener('mousemove', (e) => {
            if (!elementsPanel.classList.contains('active')) return;
            const element = document.elementFromPoint(e.clientX, e.clientY);
            if (element && !element.closest('.devtools-container')) {
                updateHighlight(element);
                highlight.style.display = 'block';
            }
        });
        document.addEventListener('click', (e) => {
            if (!elementsPanel.classList.contains('active')) return;
            const element = document.elementFromPoint(e.clientX, e.clientY);
            if (element && !element.closest('.devtools-container')) {
                e.preventDefault();
                showElementInfo(element);
            }
        });
    }
    // 控制台功能
    function initConsole(consolePanel) {
        const input = consolePanel.querySelector('.console-input');
        const outputContainer = consolePanel.querySelector('.console-output-container');
        function log(message, type = 'log') {
            const output = document.createElement('div');
            output.className = `console-output ${type}`;
            output.textContent = message;
            outputContainer.appendChild(output);
            outputContainer.scrollTop = outputContainer.scrollHeight;
        }
        const originalConsole = {
            log: console.log,
            error: console.error,
            warn: console.warn
        };
        console.log = function(...args) {
            log(args.join(' '));
            originalConsole.log.apply(console, args);
        };
        console.error = function(...args) {
            log(args.join(' '), 'error');
            originalConsole.error.apply(console, args);
        };
        console.warn = function(...args) {
            log(args.join(' '), 'warn');
            originalConsole.warn.apply(console, args);
        };
        input.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                try {
                    const result = eval(input.value);
                    log(`> ${input.value}`);
                    log(result);
                } catch (error) {
                    log(error.message, 'error');
                }
                input.value = '';
            }
        });
    }
    // 网络请求监控

function initNetworkMonitor(networkPanel) {
    const originalFetch = window.fetch;
    const originalXHR = window.XMLHttpRequest.prototype.open;
    function formatHeaders(headers) {
        if (headers instanceof Headers) {
            return Array.from(headers.entries())
                .map(([key, value]) => `${key}: ${value}`)
                .join('\n');
        } else if (typeof headers === 'string') {
            // 处理XMLHttpRequest的getAllResponseHeaders()返回的字符串
            return headers.split('\r\n').filter(line => line.trim()).join('\n');
        } else if (headers && typeof headers === 'object') {
            return Object.entries(headers)
                .map(([key, value]) => `${key}: ${value}`)
                .join('\n');
        }
        return 'No headers available';
    }
    function formatBody(body, contentType) {
        if (contentType && contentType.includes('application/json')) {
            try {
                return JSON.stringify(JSON.parse(body), null, 2);
            } catch (e) {
                return body;
            }
        }
        return body;
    }
    function createNetworkLogItem(requestInfo, responseInfo) {
        const item = document.createElement('div');
        item.className = 'network-item';
        item.innerHTML = `
            <div class="network-summary">
                <div>${requestInfo.method}</div>
                <div>${requestInfo.url}</div>
                <div>${responseInfo.status}</div>
                <div>${responseInfo.duration.toFixed(2)} ms</div>
            </div>
            <div class="network-details">
                <h4>General</h4>
                <pre>Request URL: ${requestInfo.url}
Request Method: ${requestInfo.method}
Status Code: ${responseInfo.status}
Remote Address: ${location.hostname}
Referrer Policy: ${document.referrer}</pre>
                
                <h4>Response Headers</h4>
                <pre>${formatHeaders(responseInfo.headers)}</pre>
                
                <h4>Request Headers</h4>
                <pre>${formatHeaders(requestInfo.headers)}</pre>
                
                <h4>Response Body</h4>
                <pre>${formatBody(responseInfo.body, responseInfo.headers['content-type'] || responseInfo.headers.get('content-type'))}</pre>
            </div>
        `;
        item.querySelector('.network-summary').addEventListener('click', () => {
            item.classList.toggle('expanded');
        });
        return item;
    }
        window.fetch = async function(...args) {
            const startTime = performance.now();
            const request = args[0] instanceof Request ? args[0] : new Request(...args);
            
            try {
                const response = await originalFetch.apply(this, args);
                const endTime = performance.now();
                
                const clonedResponse = response.clone();
                const responseBody = await clonedResponse.text();
                const requestInfo = {
                    url: request.url,
                    method: request.method,
                    headers: request.headers,
                };
                const responseInfo = {
                    status: response.status + ' ' + response.statusText,
                    headers: response.headers,
                    body: responseBody,
                    duration: endTime - startTime
                };
                const logItem = createNetworkLogItem(requestInfo, responseInfo);
                networkPanel.appendChild(logItem);
                return response;
            } catch (error) {
                console.error('Fetch error:', error);
                throw error;
            }
        };
        window.XMLHttpRequest.prototype.open = function(...args) {
            const startTime = performance.now();
            const [method, url] = args;
            this.addEventListener('load', () => {
                const endTime = performance.now();
                const requestInfo = {
                    url: url,
                    method: method,
                    headers: this.getAllResponseHeaders(),
                };
                const responseInfo = {
                    status: this.status + ' ' + this.statusText,
                    headers: this.getAllResponseHeaders(),
                    body: this.responseText,
                    duration: endTime - startTime
                };
                const logItem = createNetworkLogItem(requestInfo, responseInfo);
                networkPanel.appendChild(logItem);
            });
            return originalXHR.apply(this, args);
        };
    }
    // 样式查看/修改功能
    function initStylesPanel(stylesPanel) {
        function updateStyles() {
            const styleSheets = Array.from(document.styleSheets);
            let html = '<h3>页面样式:</h3>';
            
            styleSheets.forEach((sheet, index) => {
                try {
                    const rules = Array.from(sheet.cssRules);
                    html += `<details>
                        <summary>样式表 ${index + 1}</summary>
                        <pre>${rules.map(rule => rule.cssText).join('\n')}</pre>
                    </details>`;
                } catch (e) {
                    html += `<p>无法访问样式表 ${index + 1} (CORS限制)</p>`;
                }
            });
            stylesPanel.innerHTML = html;
        }
        updateStyles();
    }
    // 初始化开发者工具
    function initDevTools() {
        addStyle(styles);
        const {container, toggleButton} = createDevToolsUI();
        const elementsPanel = container.querySelector('.elements-panel');
        const consolePanel = container.querySelector('.console-panel');
        const networkPanel = container.querySelector('.network-panel');
        const stylesPanel = container.querySelector('.styles-panel');
        const expandCollapseBtn = container.querySelector('.expand-collapse');
        initElementInspector(elementsPanel);
        initConsole(consolePanel);
        initNetworkMonitor(networkPanel);
        initStylesPanel(stylesPanel);
        // 标签切换
        container.querySelector('.devtools-header').addEventListener('click', (e) => {
            if (e.target.classList.contains('devtools-tab')) {
                const panelName = e.target.dataset.panel;
                container.querySelectorAll('.devtools-tab').forEach(tab => tab.classList.remove('active'));
                container.querySelectorAll('.devtools-panel').forEach(panel => panel.classList.remove('active'));
                e.target.classList.add('active');
                container.querySelector(`.${panelName}-panel`).classList.add('active');
            }
        });
       // 展开/收起面板
        expandCollapseBtn.addEventListener('click', () => {
            container.classList.toggle('expanded');
            expandCollapseBtn.textContent = container.classList.contains('expanded') ? '收起' : '展开';
        });
        // 显示/隐藏开发者工具
        toggleButton.addEventListener('click', () => {
            container.style.display = container.style.display === 'none' ? 'flex' : 'none';
            if (container.style.display === 'flex') {
                container.classList.add('expanded');
                expandCollapseBtn.textContent = '收起';
            }
        });
    }
    // 页面加载完成后初始化
    window.addEventListener('load', initDevTools);
})();