当前位置: 首页 > article >正文

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.querySelectorDOM.querySelectorAll 用于通过 CSS 选择器查找页面中的元素。这两个命令功能与 JavaScript 中的 document.querySelectordocument.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.setChildNodesDOM.childNodeInsertedDOM.childNodeRemoved 是用于监听节点插入、移除和更新的事件。同其他域一样,需要发送 DOM.enable 来开启 监听代理。

以下列举了常见的事件:

  1. DOM.documentUpdated
  • 描述: 当整个 DOM 文档被重新加载或更新时触发。
  • 用途: 可以用于检测页面的完全重新加载或重绘。

  1. DOM.childNodeInserted
  • 描述: 当 DOM 树中插入了一个子节点时触发。
  • 用途: 监听元素被动态插入 DOM 的情况。
  • 参数:
    • parentNodeId: 父节点的 ID。
    • node: 被插入的节点对象。

  1. DOM.childNodeRemoved
  • 描述: 当 DOM 树中移除了一个子节点时触发。
  • 用途: 监听元素被动态移除的情况。
  • 参数:
    • parentNodeId: 父节点的 ID。
    • nodeId: 被移除的节点 ID。

  1. 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 结构。通过这一功能,开发者可以自动化复杂的页面。


http://www.kler.cn/a/407159.html

相关文章:

  • 微软发布Win11 24H2系统11月可选更新KB5046740!
  • C语言的文件函数
  • 【1.4 Getting Started--->Support Matrix】
  • 飞凌嵌入式T113-i开发板RISC-V核的实时应用方案
  • 《Object类》
  • java 并发编程 (2)Thread 类和 Runnable 接口详解
  • 开放性实验——网络安全渗透测试
  • Flutter实现气泡提示框学习
  • 设计模式-创建型-抽象工厂模式
  • Android kotlin之配置kapt编译器插件
  • 微信小程序数据绑定与事件绑定详解:从入门到精通
  • Unity UI射线检测 道具拖拽
  • 网络安全与加密
  • Spring Boot 整合 Prometheus 实现资源监控
  • 全面提升系统安全:禁用不必要服务、更新安全补丁、配置防火墙规则的实战指南
  • 鸿蒙开发-音视频
  • AI赋能 Python编程之2. 从构思到优化:用AI快速实现Python项目
  • 【多线程-第一天-多线程的执行原理-多线程的优缺点-主线程 Objective-C语言】
  • Arcpy 多线程批量重采样脚本
  • 11 —— 打包模式的应用
  • 一站式学习:害虫识别与分类图像分割
  • 汽车加油行驶问题-动态规划算法(已在洛谷AC)
  • 埃文科技携手河南企业代表团亮相第十九届广州中博会
  • 移门缓冲支架:为好梦加分
  • 【青牛科技】电流模式PWM控制器系列--D4870
  • HTML通过JavaScript获取访问连接,IP和端口