Chrome DevTools Protocol 进阶:DOM 域
前言
在浏览器开发和调试中,文档对象模型(DOM)是前端开发者接触最多的内容之一。DOM 代表页面的结构,允许开发者动态地操作 HTML 元素、属性和样式。Chrome DevTools Protocol(CDP)通过 DOM
域,为开发者提供了对页面结构的编程接口,使得操作页面的 DOM 元素、监听事件、修改节点等都可以通过编程自动化实现。
本文将详细介绍 CDP 中的 DOM
域,涵盖如何使用它获取和修改页面的元素,操作节点树,并提供一些常用的例子帮助开发者理解和应用该功能。
DOM 域简介
DOM
域是 CDP 中与页面 DOM 相关的命令和事件的集合,开发者可以通过它来查询、操作和监听页面的 DOM 树。主要功能包括:
- 获取整个页面的 DOM 树或某一部分的节点。
- 查找、修改、删除、添加 DOM 节点。
- 监听 DOM 结构的变化。
- 设置和获取节点的属性、样式、内容等。
DOM 域常用命令
1. 获取 DOM 树
DOM.getDocument
命令用于获取整个页面的 DOM 树,返回的节点对象可以作为后续操作的起点。我们可以根据节点 ID 对树中的元素进行进一步操作。
{
"id": 1,
"method": "DOM.getDocument"
}
返回的结构类似于以下 JSON:
{
"id": 1,
"result": {
"root": {
"nodeId": 1,
"backendNodeId": 2,
"nodeType": 9,
"nodeName": "#document",
"childNodeCount": 2,
"children": [
{
"nodeId": 3,
"nodeName": "html",
"nodeType": 1,
"attributes": [],
"childNodeCount": 3,
"children": [
{
"nodeId": 4,
"nodeName": "head",
"nodeType": 1,
"attributes": [],
"childNodeCount": 1,
"children": [ ... ]
},
{
"nodeId": 5,
"nodeName": "body",
"nodeType": 1,
"attributes": [],
"childNodeCount": 2,
"children": [ ... ]
}
]
}
]
}
}
}
通过此命令,我们获取到整个页面的 DOM 树,可以进一步操作其中的每个节点。
2. 查找节点
DOM.querySelector
和 DOM.querySelectorAll
用于通过 CSS 选择器查找页面中的元素。这两个命令功能与 JavaScript 中的 document.querySelector
和 document.querySelectorAll
类似。
DOM.querySelector
:返回第一个匹配选择器的节点。DOM.querySelectorAll
:返回所有匹配选择器的节点。
{
"id": 2,
"method": "DOM.querySelector",
"params": {
"nodeId": 1, // 文档根节点
"selector": "#my-element"
}
}
返回结果:
{
"id": 2,
"result": {
"nodeId": 6
}
}
获取到目标节点后,可以通过 nodeId
进一步操作该节点。
3. 获取节点属性
DOM.getAttributes
命令用于获取某个节点的所有属性。
{
"id": 3,
"method": "DOM.getAttributes",
"params": {
"nodeId": 6
}
}
返回的结果:
{
"id": 3,
"result": {
"attributes": [
"class", "my-class",
"id", "my-element",
"style", "color: red;"
]
}
}
4. 修改节点属性
DOM.setAttributeValue
命令用于修改节点的某个属性值。
{
"id": 4,
"method": "DOM.setAttributeValue",
"params": {
"nodeId": 6,
"name": "class",
"value": "new-class"
}
}
通过此命令,元素的 class
属性将被修改为 "new-class"
。
5. 移除节点
DOM.removeNode
命令用于移除页面中的某个节点。通过指定节点的 nodeId
,我们可以删除该节点及其子节点。
{
"id": 5,
"method": "DOM.removeNode",
"params": {
"nodeId": 6
}
}
这将会从页面中移除节点 ID 为 6
的元素。
事件监听
除了操作 DOM 节点外,DOM
域还可以监听 DOM 树的变化。DOM.setChildNodes
、DOM.childNodeInserted
和 DOM.childNodeRemoved
是用于监听节点插入、移除和更新的事件。同其他域一样,需要发送 DOM.enable
来开启 监听代理。
以下列举了常见的事件:
DOM.documentUpdated
- 描述: 当整个 DOM 文档被重新加载或更新时触发。
- 用途: 可以用于检测页面的完全重新加载或重绘。
DOM.childNodeInserted
- 描述: 当 DOM 树中插入了一个子节点时触发。
- 用途: 监听元素被动态插入 DOM 的情况。
- 参数:
parentNodeId
: 父节点的 ID。node
: 被插入的节点对象。
DOM.childNodeRemoved
- 描述: 当 DOM 树中移除了一个子节点时触发。
- 用途: 监听元素被动态移除的情况。
- 参数:
parentNodeId
: 父节点的 ID。nodeId
: 被移除的节点 ID。
DOM.setChildNodes
- 描述: 当子节点列表被重置时触发。
- 用途: 监听节点的子元素变化(如批量操作或节点的子节点被重置)。
- 参数:
parentId
: 父节点 ID。nodes
: 新的子节点列表。
测试
准备一个HTML文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM 操作示例</title>
<style>
#container {
border: 1px solid #ccc;
padding: 20px;
margin: 20px 0;
}
.node {
margin: 5px 0;
padding: 10px;
border: 1px solid #333;
}
</style>
</head>
<body>
<h1>DOM 操作示例</h1>
<div id="controls">
<button onclick="addNode()">新增节点</button>
<button onclick="removeNode()">删除节点</button>
<button onclick="modifyNodeAttribute()">修改节点属性</button>
<button onclick="modifyTextContent()">修改文本内容</button>
</div>
<div id="container">
<div id="node1" class="node" data-info="节点1">这是节点1</div>
<div id="node2" class="node" data-info="节点2">这是节点2</div>
</div>
<script>
let nodeCounter = 2;
// 新增节点
function addNode() {
nodeCounter++;
const container = document.getElementById('container');
const newNode = document.createElement('div');
newNode.id = `node${nodeCounter}`;
newNode.className = 'node';
newNode.setAttribute('data-info', `节点${nodeCounter}`);
newNode.textContent = `这是节点${nodeCounter}`;
container.appendChild(newNode);
}
// 删除最后一个节点
function removeNode() {
const container = document.getElementById('container');
const lastNode = container.lastElementChild;
if (lastNode) {
container.removeChild(lastNode);
} else {
alert('没有更多节点可以删除!');
}
}
// 修改节点的属性
function modifyNodeAttribute() {
const node = document.getElementById('node1');
if (node) {
node.setAttribute('data-info', '已修改属性');
node.style.border = '2px dashed red';
alert('节点1的属性已修改');
}
}
// 修改节点的文本内容
function modifyTextContent() {
const node = document.getElementById('node2');
if (node) {
node.textContent = '节点2的文本内容已修改';
alert('节点2的文本内容已修改');
}
}
</script>
</body>
</html>
保存文件,并在浏览器中打开
编写监听代码
import asyncio
import websockets
import json
async def listen_to_dom_changes(cdp_url):
async with websockets.connect(cdp_url) as websocket:
# 1. 启用 DOM domain
await websocket.send(json.dumps({
'id': 1,
'method': 'DOM.enable'
}))
await websocket.send(json.dumps({
'id':2,
"method": "DOM.getDocument",
"params":{
"depth": -1,
"pierce": True
}
}))
print("开始监听 DOM 变化...")
# 持续监听来自浏览器的事件
while True:
message = await websocket.recv()
print(message)
cdp_url = "ws://localhost:9222/devtools/page/1F1A646103FECD9BDC9C29868F0E31D1"
asyncio.get_event_loop().run_until_complete(listen_to_dom_changes(cdp_url))
以上代码可以监听DOM Tree 的变化。
运行结果
当我们在页面上点击DOM 操作的按钮之后,可以看到相应的日志输出。
开始监听 DOM 变化...
{"id":2,"result":{"root":{"nodeId":1,"backendNodeId":248,"nodeType":9,"nodeName":"#document","localName":"","nodeValue":"","childNodeCount":2,"children":[{"nodeId":2,"parentId":1,"backendNodeId":270,"nodeType":10,"nodeName":"html","localName":"","nodeValue":"","publicId":"","systemId":""},{"nodeId":3,"parentId":1,"backendNodeId":249,"nodeType":1,"nodeName":"HTML","localName":"html","nodeValue":"","childNodeCount":2,"children":[{"nodeId":4,"parentId":3,"backendNodeId":271,"nodeType":1,"nodeName":"HEAD","localName":"head","nodeValue":"","childNodeCount":4,"children":[{"nodeId":5,"parentId":4,"backendNodeId":272,"nodeType":1,"nodeName":"META","localName":"meta","nodeValue":"","childNodeCount":0,"children":[],"attributes":["charset","UTF-8"]},{"nodeId":6,"parentId":4,"backendNodeId":273,"nodeType":1,"nodeName":"META","localName":"meta","nodeValue":"","childNodeCount":0,"children":[],"attributes":["name","viewport","content","width=device-width, initial-scale=1.0"]},{"nodeId":7,"parentId":4,"backendNodeId":274,"nodeType":1,"nodeName":"TITLE","localName":"title","nodeValue":"","childNodeCount":1,"children":[{"nodeId":8,"parentId":7,"backendNodeId":275,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"DOM \u64cd\u4f5c\u793a\u4f8b"}],"attributes":[]},{"nodeId":9,"parentId":4,"backendNodeId":276,"nodeType":1,"nodeName":"STYLE","localName":"style","nodeValue":"","childNodeCount":1,"children":[{"nodeId":10,"parentId":9,"backendNodeId":277,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\n #container {\n border: 1px solid #ccc;\n padding: 20px;\n margin: 20px 0;\n }\n .node {\n margin: 5px 0;\n padding: 10px;\n border: 1px solid #333;\n }\n "}],"attributes":[]}],"attributes":[]},{"nodeId":11,"parentId":3,"backendNodeId":250,"nodeType":1,"nodeName":"BODY","localName":"body","nodeValue":"","childNodeCount":4,"children":[{"nodeId":12,"parentId":11,"backendNodeId":251,"nodeType":1,"nodeName":"H1","localName":"h1","nodeValue":"","childNodeCount":1,"children":[{"nodeId":13,"parentId":12,"backendNodeId":256,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"DOM \u64cd\u4f5c\u793a\u4f8b"}],"attributes":[]},{"nodeId":14,"parentId":11,"backendNodeId":252,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":4,"children":[{"nodeId":15,"parentId":14,"backendNodeId":257,"nodeType":1,"nodeName":"BUTTON","localName":"button","nodeValue":"","childNodeCount":1,"children":[{"nodeId":16,"parentId":15,"backendNodeId":258,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u65b0\u589e\u8282\u70b9"}],"attributes":["onclick","addNode()"]},{"nodeId":17,"parentId":14,"backendNodeId":260,"nodeType":1,"nodeName":"BUTTON","localName":"button","nodeValue":"","childNodeCount":1,"children":[{"nodeId":18,"parentId":17,"backendNodeId":261,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u5220\u9664\u8282\u70b9"}],"attributes":["onclick","removeNode()"]},{"nodeId":19,"parentId":14,"backendNodeId":263,"nodeType":1,"nodeName":"BUTTON","localName":"button","nodeValue":"","childNodeCount":1,"children":[{"nodeId":20,"parentId":19,"backendNodeId":264,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u4fee\u6539\u8282\u70b9\u5c5e\u6027"}],"attributes":["onclick","modifyNodeAttribute()"]},{"nodeId":21,"parentId":14,"backendNodeId":266,"nodeType":1,"nodeName":"BUTTON","localName":"button","nodeValue":"","childNodeCount":1,"children":[{"nodeId":22,"parentId":21,"backendNodeId":267,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u4fee\u6539\u6587\u672c\u5185\u5bb9"}],"attributes":["onclick","modifyTextContent()"]}],"attributes":["id","controls"]},{"nodeId":23,"parentId":11,"backendNodeId":253,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":2,"children":[{"nodeId":24,"parentId":23,"backendNodeId":254,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":1,"children":[{"nodeId":25,"parentId":24,"backendNodeId":268,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u8fd9\u662f\u8282\u70b91"}],"attributes":["id","node1","class","node","data-info","\u8282\u70b91"]},{"nodeId":26,"parentId":23,"backendNodeId":255,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":1,"children":[{"nodeId":27,"parentId":26,"backendNodeId":269,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u8fd9\u662f\u8282\u70b92"}],"attributes":["id","node2","class","node","data-info","\u8282\u70b92"]}],"attributes":["id","container"]},{"nodeId":28,"parentId":11,"backendNodeId":278,"nodeType":1,"nodeName":"SCRIPT","localName":"script","nodeValue":"","childNodeCount":1,"children":[{"nodeId":29,"parentId":28,"backendNodeId":279,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\n let nodeCounter = 2;\n\n // \u65b0\u589e\u8282\u70b9\n function addNode() {\n nodeCounter++;\n const container = document.getElementById('container');\n const newNode = document.createElement('div');\n newNode.id = `node${nodeCounter}`;\n newNode.className = 'node';\n newNode.setAttribute('data-info', `\u8282\u70b9${nodeCounter}`);\n newNode.textContent = `\u8fd9\u662f\u8282\u70b9${nodeCounter}`;\n container.appendChild(newNode);\n }\n\n // \u5220\u9664\u6700\u540e\u4e00\u4e2a\u8282\u70b9\n function removeNode() {\n const container = document.getElementById('container');\n const lastNode = container.lastElementChild;\n if (lastNode) {\n container.removeChild(lastNode);\n } else {\n alert('\u6ca1\u6709\u66f4\u591a\u8282\u70b9\u53ef\u4ee5\u5220\u9664\uff01');\n }\n }\n\n // \u4fee\u6539\u8282\u70b9\u7684\u5c5e\u6027\n function modifyNodeAttribute() {\n const node = document.getElementById('node1');\n if (node) {\n node.setAttribute('data-info', '\u5df2\u4fee\u6539\u5c5e\u6027');\n node.style.border = '2px dashed red';\n alert('\u8282\u70b91\u7684\u5c5e\u6027\u5df2\u4fee\u6539');\n }\n }\n\n // \u4fee\u6539\u8282\u70b9\u7684\u6587\u672c\u5185\u5bb9\n function modifyTextContent() {\n const node = document.getElementById('node2');\n if (node) {\n node.textContent = '\u8282\u70b92\u7684\u6587\u672c\u5185\u5bb9\u5df2\u4fee\u6539';\n alert('\u8282\u70b92\u7684\u6587\u672c\u5185\u5bb9\u5df2\u4fee\u6539');\n }\n }\n "}],"attributes":[]}],"attributes":[]}],"attributes":["lang","en"],"frameId":"74BC54E7B3604D7CAB95F8312A2989F7"}],"documentURL":"file:///E:/chromium/chromium122-patch/other/dom.html","baseURL":"file:///E:/chromium/chromium122-patch/other/dom.html","xmlVersion":"","compatibilityMode":"NoQuirksMode"}}}
{"method":"DOM.childNodeInserted","params":{"parentNodeId":23,"previousNodeId":26,"node":{"nodeId":30,"backendNodeId":280,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":1,"children":[{"nodeId":31,"parentId":30,"backendNodeId":281,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u8fd9\u662f\u8282\u70b93"}],"attributes":["id","node3","class","node","data-info","\u8282\u70b93"]}}}
{"method":"DOM.childNodeInserted","params":{"parentNodeId":23,"previousNodeId":30,"node":{"nodeId":32,"backendNodeId":282,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":1,"children":[{"nodeId":33,"parentId":32,"backendNodeId":283,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u8fd9\u662f\u8282\u70b94"}],"attributes":["id","node4","class","node","data-info","\u8282\u70b94"]}}}
{"method":"DOM.childNodeInserted","params":{"parentNodeId":23,"previousNodeId":32,"node":{"nodeId":34,"backendNodeId":284,"nodeType":1,"nodeName":"DIV","localName":"div","nodeValue":"","childNodeCount":1,"children":[{"nodeId":35,"parentId":34,"backendNodeId":285,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u8fd9\u662f\u8282\u70b95"}],"attributes":["id","node5","class","node","data-info","\u8282\u70b95"]}}}
{"method":"DOM.childNodeRemoved","params":{"parentNodeId":23,"nodeId":34}}
{"method":"DOM.childNodeRemoved","params":{"parentNodeId":23,"nodeId":32}}
{"method":"DOM.attributeModified","params":{"nodeId":24,"name":"data-info","value":"\u5df2\u4fee\u6539\u5c5e\u6027"}}
{"method":"DOM.inlineStyleInvalidated","params":{"nodeIds":[24]}}
{"method":"DOM.childNodeRemoved","params":{"parentNodeId":26,"nodeId":27}}
{"method":"DOM.childNodeInserted","params":{"parentNodeId":26,"previousNodeId":0,"node":{"nodeId":36,"backendNodeId":286,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"\u8282\u70b92\u7684\u6587\u672c\u5185\u5bb9\u5df2\u4fee\u6539"}}}
总结
Chrome DevTools Protocol 的 DOM
域提供了强大的 API,允许开发者以编程的方式操作页面的 DOM 结构。通过这一功能,开发者可以自动化复杂的页面。