移动端开发者工具(优化版)

为移动浏览器提供类似桌面版的开发者工具,优化性能和响应性,适应深浅色模式

// ==UserScript==
// @name         移动端开发者工具(优化版)
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  为移动浏览器提供类似桌面版的开发者工具,优化性能和响应性,适应深浅色模式
// @match        *://*/*
// @grant        none
// ==/UserScript==
(function() {
    'use strict';
    // 样式定义
    const styles = `
        #mobile-devtools {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 50%;
            background-color: var(--devtools-bg-color, #fff);
            color: var(--devtools-text-color, #000);
            border-bottom: 1px solid var(--devtools-border-color, #ccc);
            z-index: 9999;
            font-family: Arial, sans-serif;
            font-size: 14px;
            display: none;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        #mobile-devtools-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 5px;
            background-color: var(--devtools-header-bg-color, #f0f0f0);
            border-bottom: 1px solid var(--devtools-border-color, #ccc);
        }
        #mobile-devtools-tabs {
            display: flex;
        }
        .mobile-devtools-tab {
            padding: 5px 10px;
            cursor: pointer;
            border-right: 1px solid var(--devtools-border-color, #ccc);
            color: var(--devtools-tab-text-color, #333);
        }
        .mobile-devtools-tab.active {
            background-color: var(--devtools-active-tab-bg-color, #fff);
            color: var(--devtools-active-tab-text-color, #000);
        }
        #mobile-devtools-content {
            height: calc(100% - 30px);
            overflow-y: auto;
            -webkit-overflow-scrolling: touch;
        }
        .mobile-devtools-panel {
            display: none;
            padding: 10px;
        }
        .mobile-devtools-panel.active {
            display: block;
        }
        #mobile-devtools-toggle {
            position: fixed;
            top: 10px;
            right: 10px;
            width: 50px;
            height: 50px;
            background-color: var(--devtools-toggle-bg-color, #007bff);
            color: var(--devtools-toggle-text-color, #fff);
            border-radius: 50%;
            text-align: center;
            line-height: 50px;
            font-size: 16px;
            cursor: pointer;
            z-index: 10000;
            touch-action: none;
            user-select: none;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        #mobile-devtools-arrow {
            position: absolute;
            bottom: -15px;
            left: 50%;
            transform: translateX(-50%);
            width: 0;
            height: 0;
            border-left: 15px solid transparent;
            border-right: 15px solid transparent;
            border-top: 15px solid var(--devtools-toggle-bg-color, #007bff);
            cursor: pointer;
        }
        #network-requests {
            width: 100%;
            table-layout: fixed;
            border-collapse: collapse;
        }
        #network-requests th, #network-requests td {
            border: 1px solid var(--devtools-border-color, #ccc);
            padding: 5px;
            text-align: left;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            color: var(--devtools-text-color, #000);
        }
        #network-requests tr {
            cursor: pointer;
        }
        #network-requests tr:hover {
            background-color: var(--devtools-hover-bg-color, #f5f5f5);
        }
        #network-requests tr.active {
            background-color: var(--devtools-active-row-bg-color, #e6f3ff);
        }
        .network-detail-row td {
            padding: 0 !important;
        }
        .network-detail {
            padding: 10px;
            overflow-x: hidden;
            overflow-y: auto;
            max-height: 300px;
        }
        .network-detail p {
            word-wrap: break-word;
            word-break: break-all;
            margin: 5px 0;
        }
        .network-detail pre, .network-detail .response-body {
            white-space: pre-wrap;
            word-wrap: break-word;
            word-break: break-all;
            background-color: var(--devtools-code-bg-color, #f0f0f0);
            color: var(--devtools-code-text-color, #000);
            padding: 10px;
            border: 1px solid var(--devtools-border-color, #ccc);
            margin: 5px 0;
            max-height: 200px;
            overflow-y: auto;
        }
        @media (prefers-color-scheme: dark) {
            :root {
                --devtools-bg-color: #1e1e1e;
                --devtools-text-color: #ffffff;
                --devtools-border-color: #444;
                --devtools-header-bg-color: #2d2d2d;
                --devtools-tab-text-color: #ccc;
                --devtools-active-tab-bg-color: #3c3c3c;
                --devtools-active-tab-text-color: #fff;
                --devtools-toggle-bg-color: #0056b3;
                --devtools-toggle-text-color: #fff;
                --devtools-hover-bg-color: #2a2a2a;
                --devtools-active-row-bg-color: #264f78;
                --devtools-code-bg-color: #2d2d2d;
                --devtools-code-text-color: #d4d4d4;
            }
        }
    `;
    // 创建样式元素
    const styleElement = document.createElement('style');
    styleElement.textContent = styles;
    document.head.appendChild(styleElement);
    // HTML结构
    const devToolsHTML = `
        <div id="mobile-devtools">
            <div id="mobile-devtools-header">
                <div id="mobile-devtools-tabs">
                    <div class="mobile-devtools-tab active" data-tab="elements">元素</div>
                    <div class="mobile-devtools-tab" data-tab="console">控制台</div>
                    <div class="mobile-devtools-tab" data-tab="network">网络</div>
                    <div class="mobile-devtools-tab" data-tab="resources">资源</div>
                    <div class="mobile-devtools-tab" data-tab="storage">存储</div>
                </div>
            </div>
            <div id="mobile-devtools-content">
                <div class="mobile-devtools-panel active" id="elements-panel">
                    <div id="element-tree"></div>
                </div>
                <div class="mobile-devtools-panel" id="console-panel">
                    <div id="console-output"></div>
                    <input type="text" id="console-input" placeholder="输入JavaScript代码">
                </div>
                <div class="mobile-devtools-panel" id="network-panel">
                    <table id="network-requests">
                        <thead>
                            <tr>
                                <th style="width: 5%;">ID</th>
                                <th style="width: 30%;">名称</th>
                                <th style="width: 10%;">方法</th>
                                <th style="width: 10%;">状态</th>
                                <th style="width: 15%;">类型</th>
                                <th style="width: 15%;">大小</th>
                                <th style="width: 15%;">时间</th>
                            </tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                </div>
                <div class="mobile-devtools-panel" id="resources-panel">
                    <h3>资源列表</h3>
                    <ul id="resource-list"></ul>
                </div>
                <div class="mobile-devtools-panel" id="storage-panel">
                    <h3>本地存储</h3>
                    <div id="local-storage"></div>
                    <h3>会话存储</h3>
                    <div id="session-storage"></div>
                    <h3>Cookies</h3>
                    <div id="cookies"></div>
                </div>
            </div>
            <div id="mobile-devtools-arrow"></div>
        </div>
        <div id="mobile-devtools-toggle">开发</div>
    `;
    // 将HTML插入到页面中
    document.body.insertAdjacentHTML('beforeend', devToolsHTML);
    // 获取必要的DOM元素
    const devTools = document.getElementById('mobile-devtools');
    const devToolsToggle = document.getElementById('mobile-devtools-toggle');
    const devToolsArrow = document.getElementById('mobile-devtools-arrow');
    const tabs = document.querySelectorAll('.mobile-devtools-tab');
    const panels = document.querySelectorAll('.mobile-devtools-panel');
    // 跟踪最新的网络请求行
    let latestNetworkRow = null;
    // 优化1: 使用事件委托来处理标签切换
    document.getElementById('mobile-devtools-tabs').addEventListener('click', handleTabClick);
    // 优化2: 使用防抖函数来优化频繁触发的函数
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }
    // 优化3: 使用 requestAnimationFrame 来优化滚动和动画
    function smoothScroll(element, to, duration) {
        const start = element.scrollTop;
        const change = to - start;
        const startTime = performance.now();
        function animateScroll(currentTime) {
            const elapsedTime = currentTime - startTime;
            const progress = Math.min(elapsedTime / duration, 1);
            element.scrollTop = start + change * easeInOutQuad(progress);
            if (progress < 1) {
                requestAnimationFrame(animateScroll);
            }
        }
        requestAnimationFrame(animateScroll);
    }
    function easeInOutQuad(t) {
        return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    }
    // 优化4: 优化触摸事件处理
    let touchStartX, touchStartY, touchStartTime;
let isDragging = false;
const CLICK_DELAY = 300; // 毫秒
const MOVE_THRESHOLD = 10; // 像素
devToolsToggle.addEventListener('touchstart', handleTouchStart, { passive: true });
devToolsToggle.addEventListener('touchmove', handleTouchMove, { passive: false });
devToolsToggle.addEventListener('touchend', handleTouchEnd);
function handleTouchStart(e) {
    touchStartX = e.touches[0].clientX;
    touchStartY = e.touches[0].clientY;
    touchStartTime = Date.now();
    isDragging = false;
}
function handleTouchMove(e) {
    if (!touchStartX || !touchStartY) return;
    const touchEndX = e.touches[0].clientX;
    const touchEndY = e.touches[0].clientY;
    const deltaX = touchEndX - touchStartX;
    const deltaY = touchEndY - touchStartY;
    if (Math.abs(deltaX) > MOVE_THRESHOLD || Math.abs(deltaY) > MOVE_THRESHOLD) {
        isDragging = true;
        // 移动逻辑
        const newLeft = devToolsToggle.offsetLeft + deltaX;
        const newTop = devToolsToggle.offsetTop + deltaY;
        const maxX = window.innerWidth - devToolsToggle.offsetWidth;
        const maxY = window.innerHeight - devToolsToggle.offsetHeight;
        devToolsToggle.style.left = `${Math.max(0, Math.min(newLeft, maxX))}px`;
        devToolsToggle.style.top = `${Math.max(0, Math.min(newTop, maxY))}px`;
        touchStartX = touchEndX;
        touchStartY = touchEndY;
    }
    e.preventDefault(); // 防止页面滚动
}
function handleTouchEnd(e) {
    const touchEndTime = Date.now();
    const touchDuration = touchEndTime - touchStartTime;
    if (!isDragging && touchDuration < CLICK_DELAY) {
        toggleDevTools(e);
    }
    touchStartX = null;
    touchStartY = null;
    isDragging = false;
}
// 显示/隐藏开发者工具
function toggleDevTools(e) {
    e.preventDefault();
    e.stopPropagation();
    if (devTools.style.display === 'none' || devTools.style.display === '') {
        devTools.style.display = 'block';
        devToolsToggle.textContent = '关闭';
    } else {
        devTools.style.display = 'none';
        devToolsToggle.textContent = '开发';
    }
}
    // 收起/展开开发者工具面板
    devToolsArrow.addEventListener('click', toggleDevToolsPanel);
    devToolsArrow.addEventListener('touchend', toggleDevToolsPanel);
    function toggleDevToolsPanel(e) {
        e.preventDefault();
        e.stopPropagation();
        if (devTools.style.height === '50%' || devTools.style.height === '') {
            devTools.style.height = '30px';
            devToolsArrow.style.transform = 'translateX(-50%) rotate(180deg)';
        } else {
            devTools.style.height = '50%';
            devToolsArrow.style.transform = 'translateX(-50%)';
        }
    }
    // 标签切换功能
    function handleTabClick(e) {
        e.preventDefault();
        e.stopPropagation();
        
        const clickedTab = e.target;
        
        tabs.forEach(t => t.classList.remove('active'));
        panels.forEach(p => p.classList.remove('active'));
        
        clickedTab.classList.add('active');
        const panelId = `${clickedTab.dataset.tab}-panel`;
        const panel = document.getElementById(panelId);
        if (panel) {
            panel.classList.add('active');
            
            // 如果切换到网络面板,滚动到最新的请求
            if (panelId === 'network-panel') {
                setTimeout(scrollToLatestRequest, 0);
            }
        }
    }
    // 滚动到最新的请求
    function scrollToLatestRequest() {
        if (latestNetworkRow) {
            latestNetworkRow.scrollIntoView({ behavior: 'smooth', block: 'end' });
        }
    }
    // 确保面板内容可以滚动
    const mobileDevToolsContent = document.getElementById('mobile-devtools-content');
    mobileDevToolsContent.style.overflowY = 'auto';
    mobileDevToolsContent.style.webkitOverflowScrolling = 'touch';
    // 防止面板内容的滚动传播到整个页面
    mobileDevToolsContent.addEventListener('touchmove', function(e) {
        e.stopPropagation();
    }, { passive: false });
    // 元素检查功能
    function inspectElement(element, level = 0) {
        const elementInfo = document.createElement('div');
        elementInfo.style.marginLeft = `${level * 20}px`;
        elementInfo.textContent = `<${element.tagName.toLowerCase()}${element.id ? ` id="${element.id}"` : ''}${element.className ? ` class="${element.className}"` : ''}>`;
        document.getElementById('element-tree').appendChild(elementInfo);
        Array.from(element.children).forEach(child => {
            inspectElement(child, level + 1);
        });
    }
    inspectElement(document.body);
    // 控制台功能
    const consoleOutput = document.getElementById('console-output');
    const consoleInput = document.getElementById('console-input');
    consoleInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            try {
                const result = eval(consoleInput.value);
                consoleOutput.innerHTML += `<div>> ${consoleInput.value}</div><div>${result}</div>`;
            } catch (error) {
                consoleOutput.innerHTML += `<div>> ${consoleInput.value}</div><div style="color: red;">${error}</div>`;
            }
            consoleInput.value = '';
        }
    });
    // 重写console.log方法
    const originalConsoleLog = console.log;
    console.log = function(...args) {
        consoleOutput.innerHTML += `<div>${args.join(' ')}</div>`;
        originalConsoleLog.apply(console, args);
    };
    // 网络请求监控
    const originalFetch = window.fetch;
    const originalXHR = window.XMLHttpRequest.prototype.open;
    let requestId = 0;
    // 拦截 fetch 请求
    window.fetch = function(...args) {
        const id = requestId++;
        const startTime = Date.now();
        console.log(`Fetch request ${id} started:`, args);
        return originalFetch.apply(this, args).then(response => {
            const endTime = Date.now();
            const duration = endTime - startTime;
            const clone = response.clone();
            
            clone.text().then(body => {
                console.log(`Fetch request ${id} completed:`, response);
                addNetworkRequest(id, args[0], args[1]?.method || 'GET', response.status, response.headers.get('content-type'), body.length, duration, body, response.headers);
            }).catch(error => {
                console.error(`Error cloning response for fetch request ${id}:`, error);
                addNetworkRequest(id, args[0], args[1]?.method || 'GET', response.status, response.headers.get('content-type'), 'Unknown', duration, 'Unable to read response body', response.headers);
            });
            
            return response;
        }).catch(error => {
            console.error(`Fetch request ${id} failed:`, error);
            addNetworkRequest(id, args[0], args[1]?.method || 'GET', 'Failed', 'N/A', 'N/A', Date.now() - startTime, error.message, new Headers());
            throw error;
        });
    };
    // 拦截 XMLHttpRequest
    window.XMLHttpRequest.prototype.open = function(...args) {
        const id = requestId++;
        const startTime = Date.now();
        console.log(`XHR request ${id} started:`, args);
        this.addEventListener('load', function() {
            const endTime = Date.now();
            const duration = endTime - startTime;
            console.log(`XHR request ${id} completed:`, this);
            addNetworkRequest(id, args[1], args[0], this.status, this.getResponseHeader('Content-Type'), this.responseText.length, duration, this.responseText, this.getAllResponseHeaders());
        });
        this.addEventListener('error', function() {
            console.error(`XHR request ${id} failed:`, this);
            addNetworkRequest(id, args[1], args[0], 'Failed', 'N/A', 'N/A', Date.now() - startTime, 'Request failed', this.getAllResponseHeaders());
        });
        return originalXHR.apply(this, args);
    };
    function addNetworkRequest(id, url, method, status, type, size, time, body, headers) {
        const networkRequests = document.querySelector('#network-requests tbody');
        const row = document.createElement('tr');
        row.innerHTML = `
            <td style="width: 5%;">${id}</td>
            <td style="width: 30%;">${truncateString(url, 30)}</td>
            <td style="width: 10%;">${method}</td>
            <td style="width: 10%;">${status}</td>
            <td style="width: 15%;">${type || 'unknown'}</td>
            <td style="width: 15%;">${typeof size === 'number' ? formatSize(size) : size}</td>
            <td style="width: 15%;">${time} ms</td>
        `;
        row.addEventListener('click', () => toggleNetworkDetail(row, id, url, method, status, type, size, time, body, headers));
        networkRequests.appendChild(row);
        
        // 更新最新的网络请求行
        latestNetworkRow = row;
    }
    function toggleNetworkDetail(row, id, url, method, status, type, size, time, body, headers) {
        console.log('Request details:', { id, url, method, status, type, size, time, body, headers });
        
        // 检查是否已存在详情行
        let detailRow = row.nextElementSibling;
        if (detailRow && detailRow.classList.contains('network-detail-row')) {
            // 如果详情行已存在,则切换其显示状态
            if (detailRow.style.display === 'none') {
                detailRow.style.display = 'table-row';
                row.classList.add('active');
            } else {
                detailRow.style.display = 'none';
                row.classList.remove('active');
            }
        } else {
            // 如果详情行不存在,则创建新的详情行
            detailRow = document.createElement('tr');
            detailRow.classList.add('network-detail-row');
            detailRow.innerHTML = `
                <td colspan="7">
                    <div class="network-detail">
                        <h3>请求详情</h3>
                        <p><strong>ID:</strong> ${id}</p>
                        <p><strong>URL:</strong> ${url}</p>
                        <p><strong>方法:</strong> ${method}</p>
                        <p><strong>状态:</strong> ${status}</p>
                        <p><strong>类型:</strong> ${type || 'unknown'}</p>
                        <p><strong>大小:</strong> ${typeof size === 'number' ? formatSize(size) : size}</p>
                        <p><strong>时间:</strong> ${time} ms</p>
                        <h4>响应头:</h4>
                        <pre>${formatHeaders(headers)}</pre>
                        <h4>响应体:</h4>
                        <div class="response-body">${formatBody(body, type)}</div>
                    </div>
                </td>
            `;
            row.parentNode.insertBefore(detailRow, row.nextSibling);
            row.classList.add('active');
        }
    }
    function truncateString(str, maxLength) {
        if (str.length <= maxLength) return str;
        return str.substr(0, maxLength - 3) + '...';
    }
    function formatSize(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
        if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
        return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
    }
    function formatHeaders(headers) {
        if (typeof headers === 'string') {
            return headers;
        }
        return Array.from(headers).map(([key, value]) => `${key}: ${value}`).join('\n');
    }
    function formatBody(body, type) {
        if (typeof body !== 'string') {
            return 'Unable to display response body (not a string)';
        }
        if (body.length > 100000) {
            return `<div>响应体过大,仅显示前 100000 个字符:</div>${escapeHTML(body.substr(0, 100000))}...`;
        }
        if (type && type.includes('json')) {
            try {
                const jsonObj = JSON.parse(body);
                return `<pre>${escapeHTML(JSON.stringify(jsonObj, null, 2))}</pre>`;
            } catch (e) {
                // 如果解析失败,按普通文本处理
            }
        }
        return escapeHTML(body);
    }
    function escapeHTML(str) {
        if (typeof str !== 'string') {
            return 'Unable to escape HTML (not a string)';
        }
        return str
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }
    // 资源列表
    function updateResourceList() {
        const resourceList = document.getElementById('resource-list');
        resourceList.innerHTML = '';
        performance.getEntriesByType('resource').forEach(resource => {
            const li = document.createElement('li');
            li.textContent = `${resource.name} (${resource.initiatorType})`;
            resourceList.appendChild(li);
        });
    }
    updateResourceList();
    // 存储信息
    function updateStorageInfo() {
        const localStorage = document.getElementById('local-storage');
        const sessionStorage = document.getElementById('session-storage');
        const cookies = document.getElementById('cookies');
        localStorage.innerHTML = '';
        for (let i = 0; i < window.localStorage.length; i++) {
            const key = window.localStorage.key(i);
            localStorage.innerHTML += `<div>${key}: ${window.localStorage.getItem(key)}</div>`;
        }
        sessionStorage.innerHTML = '';
        for (let i = 0; i < window.sessionStorage.length; i++) {
            const key = window.sessionStorage.key(i);
            sessionStorage.innerHTML += `<div>${key}: ${window.sessionStorage.getItem(key)}</div>`;
        }
        cookies.innerHTML = document.cookie.split(';').map(cookie => `<div>${cookie.trim()}</div>`).join('');
    }
    updateStorageInfo();
    // 添加深色模式检测和切换功能
    function updateColorScheme() {
        if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
            document.documentElement.setAttribute('data-theme', 'dark');
        } else {
            document.documentElement.setAttribute('data-theme', 'light');
        }
    }
    // 初始化时调用一次
    updateColorScheme();
    // 监听系统颜色方案变化
    window.matchMedia('(prefers-color-scheme: dark)').addListener(updateColorScheme);
})();