// ==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);
})();