关于Browser use控制浏览器,核心代码之DOM树的构建以及DOM元素渲染

前言

🌟本期将深入讲解浏览器控制的核心代码实现

💡更多技术分享欢迎访问我的博客:GGBondlctrl-CSDN博客

👍您的支持是我持续创作的最大动力

🚀让我们直接进入正题

 

目录

📚️1.前言

📚️2.DOM元素渲染

2.1元素高亮前提工作

2.2高亮容器设置

 2.3设置颜色

2.4设置偏移量

2.5创建高亮覆盖层

2.6总结

📚️3.智能交互检测

3.1初始工作

3.2定义可交互与不可交互

3.3判断元素是否可交互(核心)

3.4检查ARIA角色

3.5检查是否可编辑

 3.6监听事件

📚️4.总结

 

📚️1.前言

本期底层源码来自github开源项目,项目地址:

https://github.com/browser-use/browser-use

大家可以自己去看看,实验实验,具体步骤小编放置到上一期中咯,小编对于javaScript不是很精通,只是大致讲解一下每个代码的具体的作用,有不对的地方欢迎大家指正🚀🚀🚀~~~~

📚️2.DOM元素渲染

具体实现的方法:

function highlightElement()

 如下图所示:

这里包含了具体的文件路径,找不到可以在这里看看;

2.1元素高亮前提工作

代码:

function highlightElement(element, index, parentIframe = null) {
    pushTiming('highlighting');
    
    if (!element) return index;

    // Store overlays and the single label for updating
    const overlays = [];
    let label = null;
    let labelWidth = 20;
    let labelHeight = 16;
    let cleanupFn = null;

传递参数(元素,元素坐标,:可选参数,默认值 null,表示元素所在的 iframe(当前代码未使用)

接下来元素是否存在?存在就继续往下走,反之return

接下来就是覆盖层的高亮标签设置;

2.2高亮容器设置

代码如下所示:

try {
  // Create or get highlight container
  //这一段就是设置高亮容器,放置我们的高亮效果
  let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
  if (!container) {
    container = document.createElement("div");
    container.id = HIGHLIGHT_CONTAINER_ID;
    container.style.position = "fixed";
    container.style.pointerEvents = "none";
    container.style.top = "0";
    container.style.left = "0";
    container.style.width = "100%";
    container.style.height = "100%";
    container.style.zIndex = "2147483640";
    container.style.backgroundColor = 'transparent';
    document.body.appendChild(container);
  }

设置我们的容器,来创建高亮的效果

第一步:通过id获取已经存在的高亮容器

第二步:如果容器为空,那么就会进行创建新的容器

第三步:设置具体的内容,创建div元素作为容器

第四步:设置容器id

第五步:设置容器的样式,并添加容器到我们的页面中

 2.3设置颜色

代码如下所示:

//根据位置的不同返回我们元素框对应的颜色
const colors = [
  "#FF0000",
  "#00FF00",
  "#0000FF",
  "#FFA500",
  "#800080",
  "#008080",
  "#FF69B4",
  "#4B0082",
  "#FF4500",
  "#2E8B57",
  "#DC143C",
  "#4682B4",
];
const colorIndex = index % colors.length;
const baseColor = colors[colorIndex];
const backgroundColor = baseColor + "1A"; // 10% opacity version of the color

设置颜色,获取对应颜色的下标位置,进行颜色的选取

2.4设置偏移量

代码如下所示:

// Get iframe offset if necessary
let iframeOffset = { x: 0, y: 0 };
if (parentIframe) {
  const iframeRect = parentIframe.getBoundingClientRect(); // Keep getBoundingClientRect for iframe offset
  iframeOffset.x = iframeRect.left;
  iframeOffset.y = iframeRect.top;
}
设置偏移量
当元素位于 iframe 内部时,它的坐标是​ ​相对于 iframe 的左上角​​计算的。但是:
  1. 我们的高亮容器是直接放在主文档中的
  2. 高亮位置需要基于​​整个页面的坐标系

因此需要将 iframe 内部的坐标转换为全局坐标 

2.5创建高亮覆盖层

代码如下所示:

 overlay.style.position = "fixed";
  overlay.style.border = `2px solid ${baseColor}`; // 使用基础颜色作为边框
  overlay.style.backgroundColor = backgroundColor; // 设置半透明背景色
  overlay.style.pointerEvents = "none"; // 禁止鼠标事件穿透
  overlay.style.boxSizing = "border-box"; // 确保边框不增加额外尺寸

负责为元素的每个可见矩形区域创建高亮覆盖层

紧接着设置高亮层的位置以及尺寸

overlay.style.top = `${top}px`;
overlay.style.left = `${left}px`;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;

然后,接下来就是添加到我们的文档片段:

 // 6. 将高亮层添加到文档片段
  fragment.appendChild(overlay);

然后针对创建的矩形,创建序号标签:

label = document.createElement("div");
label.className = "playwright-highlight-label";
label.style.position = "fixed";
label.style.background = baseColor;
label.style.color = "white";
label.style.padding = "1px 4px";
label.style.borderRadius = "4px";
label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
label.textContent = index;

2.6总结

具体的思路就是:设置高亮覆盖层,高亮元素的标签,然后搞定高亮容器,设置高亮效果,如果是在iframe中,需要修改偏移量

创建高亮覆盖层,负责为元素的每个可见矩形区域创建高亮覆盖层(同样的样式,位置尺寸)

📚️3.智能交互检测

3.1初始工作

主要的核心逻辑:

部分逻辑主要在 isInteractiveElement() 函数中实现,并辅以 isElementDistinctInteraction() 和 isHeuristicallyInteractive() 进行更精细的判断。

// Cache the tagName and style lookups
const tagName = element.tagName.toLowerCase();
const style = getCachedComputedStyle(element);

缓存标签名忽略大小写,以及缓存计算样式

  • 首次调用时计算并存储样式到 WeakMap。
  • 后续调用直接返回缓存结果,避免重复计算

 目的:缓存后,同一元素的样式只需计算一次,提升性能

3.2定义可交互与不可交互

const interactiveCursors = new Set([
  'pointer',    // 链接/可点击元素
  'move',       // 可移动元素
  'text',       // 文本选择
  'grab',       // 可拖拽元素
  'grabbing',   // 正在拖拽中
  'cell',       // 表格单元格选择
  'copy',       // 复制操作
  'alias',      // 创建别名
  'all-scroll', // 可滚动内容
  'col-resize', // 列宽调整
  'context-menu', // 上下文菜单可用
  'crosshair',  // 精确选择(十字光标)
  'e-resize',   // 向东调整(右)
  'ew-resize',  // 东西双向调整(水平)
  'help',       // 帮助可用
  'n-resize',   // 向北调整(上)
  'ne-resize',  // 东北向调整(右上)
  'nesw-resize', // 东北-西南双向调整(对角)
  'ns-resize',  // 南北双向调整(垂直)
  'nw-resize',  // 西北向调整(左上)
  'nwse-resize', // 西北-东南双向调整(对角)
  'row-resize', // 行高调整
  's-resize',   // 向南调整(下)
  'se-resize',  // 东南向调整(右下)
  'sw-resize',  // 西南向调整(左下)
  'vertical-text', // 垂直文本选择
  'w-resize',   // 向西调整(左)
  'zoom-in',    // 放大
  'zoom-out'    // 缩小
]);

判断是否可以进行交互的操作;

主要检查元素的style.cursor是否属于上述的数组中,是那么说明可交互;

// 定义非交互式光标
const nonInteractiveCursors = new Set([
  'not-allowed', // 操作禁止
  'no-drop',     // 禁止拖放
  'wait',        // 处理中(如加载)
  'progress',    // 操作进行中
  'initial',     // 初始值(默认状态)
  'inherit'      // 继承父元素值
  // 注释说明:
  // 以下光标未包含在内,但可能是非交互的:
  // 'none',     // 无光标
  // 'default',  // 默认箭头
  // 'auto',     // 浏览器自动决定
]);

style.cursor 是DOM元素的CSS属性,用于​​控制鼠标悬停时的光标样式​​。它直接对应CSS的 cursor 属性

cursor:光标样式

function doesElementHaveInteractivePointer(element) {
  if (element.tagName.toLowerCase() === "html") return false;

  if (interactiveCursors.has(style.cursor)) return true;

  return false;
}

这个函数用于​​通过光标样式(cursor)快速判断元素是否具有交互性​​,是 isInteractiveElement 的辅助函数

 标签是文档根节点,本身无交互意义,直接跳过检测

并且判断元素的光标样式是否属于上述可交互式光标样式集合

let isInteractiveCursor = doesElementHaveInteractivePointer(element);

是否是可以交互的,是那么就是true

const interactiveElements = new Set([
  "a",          // 链接(超链接)
  "button",     // 按钮
  "input",      // 所有输入类型(文本、复选框、单选框等)
  "select",     // 下拉菜单
  "textarea",   // 多行文本输入框
  "details",    // 可折叠/展开的详情块
  "summary",    // 详情块的点击标题部分
  "label",      // 表单标签(通常可点击)
  "option",     // 下拉菜单选项
  "optgroup",   // 下拉菜单选项分组
  "fieldset",   // 表单字段分组(通常包含图例)
  "legend",     // 字段分组的标题
]);

 

所有​​原生支持交互​​的HTML元素标签名(小写)。这些元素默认具有交互行为(如点击、输入、展开等)

并且这里包保存使用的Set进行存储,可以保证唯一性,并且这里的查找的时间复杂度为1

const explicitDisableTags = new Set([
  'disabled',    // 标准禁用属性(禁用按钮/输入框等)
  // 'aria-disabled',      // ARIA禁用状态(已注释,未启用)
  'readonly',    // 只读属性(禁止输入但允许聚焦)
  // 'aria-readonly',     // ARIA只读状态(已注释)
  // 其他被注释掉的属性:
  // 'aria-hidden',       // 对无障碍隐藏
  // 'hidden',           // HTML全局隐藏属性
  // 'inert',            // 惰性属性(禁止交互)
  // 'tabindex="-1"',    // 从Tab键顺序移除
]);

显示禁用属性集合

定义所有​​显式禁用交互​​的HTML属性。如果元素具有这些属性,即使它是交互式元素(如 ),也应视为​​不可交互​​。避免误判

3.3判断元素是否可交互(核心)

if (interactiveElements.has(tagName)) {
  // Check for non-interactive cursor
  if (nonInteractiveCursors.has(style.cursor)) {
    return false;
  }

是否在主判断方法中是否是可交互元素,进入后交给

nonInteractiveCursors再次进行判断是否是可交互

for (const disableTag of explicitDisableTags) {  // 遍历所有禁用属性
  if (
    element.hasAttribute(disableTag) ||          // 属性存在(如 disabled)
    element.getAttribute(disableTag) === 'true' || // 属性值为'true'(如 aria-disabled="true")
    element.getAttribute(disableTag) === ''      // 属性值为空(如 disabled="")
  ) {
    return false; // 命中任意条件则判定为不可交互
  }
}

检查元素是否被显式禁用​​,如果满足禁用条件,则判定该元素​​不可交互​​。

// 检查元素的 disabled 属性,DOM属性
if (element.disabled) {
  return false; // 如果禁用,返回不可交互
}

// 检查表单元素的只读属性
if (element.readOnly) {
  return false; // 如果只读,返回不可交互
}

// 检查 inert 属性(HTML5新增的惰性属性)
if (element.inert) {
  return false; // 如果惰性,返回不可交互
}

检查元素的禁用状态​​,通过直接访问DOM元素的属性来判断其是否被禁用或只读

重重判断后,就是一个可交互的元素

3.4检查ARIA角色

// 获取元素的 role 和 aria-role 属性值
const role = element.getAttribute("role");
const ariaRole = element.getAttribute("aria-role");

// 定义交互式ARIA角色的集合
const interactiveRoles = new Set([
  'button',           // 按钮
  'menuitemradio',    // 单选菜单项
  'menuitemcheckbox', // 复选菜单项
  'radio',            // 单选按钮
  'checkbox',         // 复选框
  'tab',              // 标签页
  'switch',           // 切换开关
  'slider',           // 滑块
  'spinbutton',       // 数字调节按钮
  'combobox',         // 组合框(下拉+输入)
  'searchbox',        // 搜索框
  'textbox',          // 文本框
  'option',           // 下拉选项
  'scrollbar'         // 滚动条
]);

// 检查角色是否在交互式集合中
if (interactiveRoles.has(role) || interactiveRoles.has(ariaRole)) {
  return true; // 判定为可交互元素
}

传统检测只能识别原生交互元素

无法识别自定义组件的交互性(ARIA角色作用自定义组件,定义缺失,组件状态)

3.5检查是否可编辑

// Check for contenteditable attribute
if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
  return true;
}

作用,检查元素是否可以进行编辑操作(获取JS属性)

例如我们的富文本编辑,评论输入框都被识别为可以交互的元素

// Added enhancement to capture dropdown interactive elements
if (element.classList && (
  element.classList.contains("button") ||
  element.classList.contains('dropdown-toggle') ||
  element.getAttribute('data-index') ||
  element.getAttribute('data-toggle') === 'dropdown' ||
  element.getAttribute('aria-haspopup') === 'true'
)) {
  return true;
}

条件

说明

class="button"

按钮样式类

class="dropdown-toggle"

下拉菜单触发器

data-index

列表项索引

data-toggle="dropdown"

下拉菜单标识

aria-haspopup="true"

有弹出菜单

 3.6监听事件

try {
  if (typeof getEventListeners === 'function') {
    const listeners = getEventListeners(element);
    const mouseEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];
    for (const eventType of mouseEvents) {
      if (listeners[eventType] && listeners[eventType].length > 0) {
        return true;
      }
    }
  }
} catch (e) {
  // 回退方案:检查内联事件属性
  const commonMouseAttrs = ['onclick', 'onmousedown', 'onmouseup', 'ondblclick'];
  for (const attr of commonMouseAttrs) {
    if (element.hasAttribute(attr) || typeof element[attr] === 'function') {
      return true;
    }
  }
}
使用Chrome API getEventListeners()
  • 检查是否存在以下事件的监听器:
    • click(点击)
    • mousedown(鼠标按下)
    • mouseup(鼠标释放)
    • dblclick(双击)
当  getEventListeners  不可用或出错时(如非Chrome环境:判断是否是具有点击...的属性,或者这个属性是否是一个方法
作用:覆盖通过JavaScript动态添加的交互功能
以及跨浏览器的兼容方案

 

📚️4.总结

第一初始化开始工作:设置缓存计算方式,以及缓存名,然后设置可交互与不可交互集合(可交互光标样式,不可交互光标样式,原生支持交互,以及显示禁用和交互​​

核心:是可交互式光标样式直接返回true,

是原生支持交互 -》是不是不可交互光标样式 -》是不是显示禁用的元素 - 》 通过直接访问DOM元素的属性来判断其是否被禁用或只读;一个不通过返回false,相反都满足就是true

核心判断路程图:

 整体的思路:

 

评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值