实现Chrome-DevTools的Elements面板
本节实现 Chrome DevTools 最常用的面板——Elements。
Elements 面板的功能
Elements 面板可以:
- 查看 DOM 树
- 修改 DOM 节点的属性
- 查看和修改 CSS 样式
- 查看 CSS Box Model
- 查看事件监听器
图表渲染中…
实现 DOM 树查看
使用 CDP 的 DOM Domain
javascript
class ElementsPanel {
constructor(connection) {
this._connection = connection;
this._selectedNodeId = null;
}
// 获取 DOM 树
async getDocument() {
const { root } = await this._connection.send('DOM.getDocument', {
depth: -1, // 获取完整 DOM 树
pierce: true, // 穿透 Shadow DOM 和 iframe
});
return root;
}
// 渲染 DOM 树
async render() {
const root = await this.getDocument();
const container = document.getElementById('dom-tree');
container.innerHTML = '';
this._renderNode(root, container, 0);
}
_renderNode(node, container, depth) {
if (node.nodeType !== 1) return; // 只显示元素节点
const div = document.createElement('div');
div.style.paddingLeft = `${depth * 16}px`;
div.className = 'dom-node';
div.dataset.nodeId = node.nodeId;
// 展开箭头
const arrow = document.createElement('span');
arrow.className = 'arrow';
arrow.textContent = node.children?.length ? '▶' : '';
div.appendChild(arrow);
// 标签名
const tag = document.createElement('span');
tag.className = 'tag';
tag.textContent = `<${node.nodeName.toLowerCase()}`;
div.appendChild(tag);
// 属性
if (node.attributes) {
for (let i = 0; i < node.attributes.length; i += 2) {
const attr = document.createElement('span');
attr.className = 'attr';
attr.textContent = ` ${node.attributes[i]}="${node.attributes[i + 1]}"`;
div.appendChild(attr);
}
}
// 闭合标签
const closeTag = document.createElement('span');
closeTag.className = 'tag';
closeTag.textContent = '>';
div.appendChild(closeTag);
// 点击选中
div.addEventListener('click', () => this._selectNode(node));
// 双击展开/收起
arrow.addEventListener('click', (e) => {
e.stopPropagation();
this._toggleNode(node, div);
});
container.appendChild(div);
// 渲染子节点
if (node.children) {
const childContainer = document.createElement('div');
childContainer.className = 'children';
childContainer.style.display = 'none'; // 默认收起
node.children.forEach(child => {
this._renderNode(child, childContainer, depth + 1);
});
container.appendChild(childContainer);
}
}
}实现 CSS 样式查看
使用 CDP 的 CSS Domain
javascript
class StylePane {
constructor(connection) {
this._connection = connection;
}
// 获取节点的所有 CSS 规则
async getStyles(nodeId) {
// 启用 CSS Domain
await this._connection.send('CSS.enable');
// 获取匹配的 CSS 规则
const { matchedCSSRules } = await this._connection.send(
'CSS.getMatchedStylesForNode',
{ nodeId }
);
// 获取内联样式
const { inlineStyle } = await this._connection.send(
'CSS.getInlineStylesForNode',
{ nodeId }
);
// 获取计算样式
const { computedStyle } = await this._connection.send(
'CSS.getComputedStyleForNode',
{ nodeId }
);
return {
matchedRules: matchedCSSRules,
inlineStyle,
computedStyle,
};
}
// 渲染样式面板
async render(nodeId) {
const styles = await this.getStyles(nodeId);
const container = document.getElementById('style-pane');
container.innerHTML = '';
// 1. element.style(内联样式)
if (styles.inlineStyle) {
this._renderStyleSection(
container,
'element.style',
styles.inlineStyle.cssProperties
);
}
// 2. 匹配的 CSS 规则(按优先级从高到低)
for (const rule of styles.matchedRules) {
const selector = rule.rule.selectorList.selectors
.map(s => s.text)
.join(', ');
const sourceURL = rule.rule.styleSheetId ?
` (${rule.rule.origin})` : '';
this._renderStyleSection(
container,
`${selector}${sourceURL}`,
rule.rule.style.cssProperties
);
}
}
_renderStyleSection(container, title, properties) {
const section = document.createElement('div');
section.className = 'style-section';
// 标题(选择器)
const header = document.createElement('div');
header.className = 'style-header';
header.textContent = title;
section.appendChild(header);
// 属性列表
for (const prop of properties) {
if (!prop.range) continue; // 跳过 shorthand
const row = document.createElement('div');
row.className = 'style-property';
const name = document.createElement('span');
name.className = 'property-name';
name.textContent = prop.name;
const separator = document.createElement('span');
separator.textContent = ': ';
const value = document.createElement('span');
value.className = 'property-value';
value.textContent = prop.value;
row.appendChild(name);
row.appendChild(separator);
row.appendChild(value);
// 点击修改属性值
value.addEventListener('dblclick', () => {
this._editProperty(prop, value);
});
section.appendChild(row);
}
container.appendChild(section);
}
}实现 CSS 样式修改
通过 CDP 修改样式
javascript
class StylePane {
// ... 前面的代码
async _editProperty(prop, element) {
// 替换为 input
const input = document.createElement('input');
input.value = prop.value;
input.style.width = '100%';
element.replaceWith(input);
input.focus();
input.select();
const commit = async () => {
const newValue = input.value;
if (newValue !== prop.value) {
// 通过 CDP 修改样式
await this._connection.send('CSS.setPropertyValue', {
styleSheetId: prop.styleSheetId,
range: prop.range,
value: newValue,
});
}
// 恢复显示
const newValueEl = document.createElement('span');
newValueEl.className = 'property-value';
newValueEl.textContent = newValue;
input.replaceWith(newValueEl);
};
input.addEventListener('blur', commit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') commit();
if (e.key === 'Escape') {
const origValue = document.createElement('span');
origValue.className = 'property-value';
origValue.textContent = prop.value;
input.replaceWith(origValue);
}
});
}
}实现 Box Model 可视化
javascript
function renderBoxModel(computedStyle) {
const getValue = (name) => {
const prop = computedStyle.find(p => p.name === name);
return prop ? parseInt(prop.value) : 0;
};
const margin = {
top: getValue('margin-top'),
right: getValue('margin-right'),
bottom: getValue('margin-bottom'),
left: getValue('margin-left'),
};
const padding = {
top: getValue('padding-top'),
right: getValue('padding-right'),
bottom: getValue('padding-bottom'),
left: getValue('padding-left'),
};
const border = {
top: getValue('border-top-width'),
right: getValue('border-right-width'),
bottom: getValue('border-bottom-width'),
left: getValue('border-left-width'),
};
const width = getValue('width');
const height = getValue('height');
// 渲染盒模型图
return `
<div class="box-model">
<div class="margin" style="padding: ${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px">
<div class="border" style="padding: ${border.top}px ${border.right}px ${border.bottom}px ${border.left}px">
<div class="padding" style="padding: ${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px">
<div class="content" style="width: ${width}px; height: ${height}px">
${width} × ${height}
</div>
</div>
</div>
</div>
</div>
`;
}实现事件监听器查看
使用 CDP 的 DOM Domain
javascript
async function getEventListeners(connection, nodeId) {
const { listeners } = await connection.send('DOM.getEventListeners', {
objectId: await getObjectId(connection, nodeId),
});
return listeners.map(listener => ({
type: listener.type,
useCapture: listener.useCapture,
passive: listener.passive,
once: listener.once,
location: `${listener.scriptId}:${listener.lineNumber}:${listener.columnNumber}`,
}));
}Elements 面板的 CDP 调用流程
图表渲染中…