本期底层源码来自github开源项目,项目地址:
https://github.com/browser-use/browser-use
大家可以自己去看看,实验实验,具体步骤小编放置到上一期中咯,小编对于javaScript不是很精通,只是大致讲解一下每个代码的具体的作用
具体实现的方法:
function highlightElement()
如下图所示:
这里包含了具体的文件路径,找不到可以在这里看看;
代码:
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
接下来就是覆盖层的高亮标签设置;
代码如下所示:
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 第五步:设置容器的样式,并添加容器到我们的页面中
代码如下所示:
//根据位置的不同返回我们元素框对应的颜色
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
设置颜色,获取对应颜色的下标位置,进行颜色的选取
代码如下所示:
// 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 的左上角计算的。但是:
因此需要将 iframe 内部的坐标转换为全局坐标
代码如下所示:
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;
具体的思路就是:设置高亮覆盖层,高亮元素的标签,然后搞定高亮容器,设置高亮效果,如果是在iframe中,需要修改偏移量
创建高亮覆盖层,负责为元素的每个可见矩形区域创建高亮覆盖层(同样的样式,位置尺寸)
主要的核心逻辑:
部分逻辑主要在 isInteractiveElement() 函数中实现,并辅以 isElementDistinctInteraction() 和 isHeuristicallyInteractive() 进行更精细的判断。
// Cache the tagName and style lookups
const tagName = element.tagName.toLowerCase();
const style = getCachedComputedStyle(element);
缓存标签名忽略大小写,以及缓存计算样式
目的:缓存后,同一元素的样式只需计算一次,提升性能
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属性。如果元素具有这些属性,即使它是交互式元素(如 ),也应视为不可交互。避免误判
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元素的属性来判断其是否被禁用或只读
重重判断后,就是一个可交互的元素
// 获取元素的 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角色作用自定义组件,定义缺失,组件状态)
// 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" | 有弹出菜单 |
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()
当 getEventListeners 不可用或出错时(如非Chrome环境:判断是否是具有点击...的属性,或者这个属性是否是一个方法
作用:覆盖通过JavaScript动态添加的交互功能
以及跨浏览器的兼容方案
第一初始化开始工作:设置缓存计算方式,以及缓存名,然后设置可交互与不可交互集合(可交互光标样式,不可交互光标样式,原生支持交互,以及显示禁用和交互
核心:是可交互式光标样式直接返回true,
是原生支持交互 -》是不是不可交互光标样式 -》是不是显示禁用的元素 - 》 通过直接访问DOM元素的属性来判断其是否被禁用或只读;一个不通过返回false,相反都满足就是true
核心判断路程图:
整体的思路: