LFDOM
全局变量:LFDOM · 启用方式:// @require dom · 附带:自动包含 network
DOM 与页面操作库:就绪等待、HTML 挂载、toast、Playwright 风格定位器(自动等待/重试)、语义快照(看 → 操作)、卡片聚合、表单填充、虚拟列表收集、截图与网络等待。
仅在页面环境可用:已保存/已安装应用与 liveDebug(USER_SCRIPT 隔离世界)、run --tab 临时脚本(默认 MAIN 世界)。后台应用分支(// @match background / run --target background)与不带 --tab 的 run(编辑器沙箱)没有目标网页:声明了 @require dom 时库仍会加载,但操作的是不可见、零尺寸的沙箱文档——页面选择器匹配不到元素,click / fill / hover / waitFor(visible)等依赖可见性的操作会超时并附带 [no web page in the <env> environment …] 提示,waitForResponse / waitForRequest 直接以 LF_NETWORK_NO_TAB 拒绝。请用 LFRuntime.executionEnvironment 分流,不要在这些环境交付 UI。@require dom 会在运行时自动注入 network 供 waitForResponse / waitForRequest 使用,但源码里直接调用 LFNetwork.* 仍须声明 // @require network。
方法
LFDOM.ready(callback): void
LFDOM.ready(options, callback): void
await LFDOM.ready(options?): Promise<void>文档脱离 loading(DOMContentLoaded)后执行回调;DOM 已解析时同步执行。不幂等:每次调用都会各执行一次回调。options.waitFor(CSS 选择器,或返回真值即就绪的谓词函数,可为 async)用于再等 SPA 主体渲染,options.timeout 默认 30000ms;超时不会 reject,只在 console 打印警告后照常执行回调。不传回调时返回 Promise
LFDOM.whenReady(options?): Promise<void>Promise 形式的就绪等待,等价于 await LFDOM.ready(options);options 同上,另有 pierce(默认 false)、interval(默认 50ms 起轮询)、maxInterval(退避上限 500ms)
LFDOM.mountHTML(html, parent?): Element | Element[] | Node | null把 HTML 字符串挂载为真实节点(parent 默认 document.body):单个顶层元素返回该元素,多个并列元素返回数组,纯文本返回 Text 节点,空串返回 null
LFDOM.createFloatingRoot(options?): HTMLDivElement创建固定定位的浮层根节点;同一应用同 id 复用,id 已被其他应用或页面占用时改用 <id>--lf-<应用id>
LFDOM.toast(message, duration?): HTMLDivElement弹出自动消失的提示条
LFDOM.locator(selector, options?): Locator创建定位器:查询 + 动作(click/fill/hover…)自动等待重试;selector 也可以是 snapshot 给出的 @N 引用
LFDOM.activeElement(): Locator当前聚焦元素的定位器
await LFDOM.clickAt(x, y, options?): Promise<Element>按视口坐标点击
LFDOM.mountAnchoredHTML(options): AnchoredMountState把 HTML 锚定挂载到页面元素旁,锚点出现时自动挂上;锚点被替换后自动重挂(仅限锚点的直接父容器不变,见下文 reanchorOnLost)
LFDOM.unmountAnchored(key): boolean卸载锚定挂载
LFDOM.mountFloatingHTML(options): FloatingMountState浮动跟随某个锚点元素的挂载
LFDOM.unmountFloating(key): boolean卸载浮动挂载
LFDOM.mountHoverAction(options): HoverActionState为卡片流挂载共享悬停操作,详见 mountHoverAction
LFDOM.waitForElement(selector, options?): Promise<Element | true>等待元素达到指定 state(默认 'attached',注意与 Locator.waitFor 默认 'visible' 不同),默认超时 5000ms;state 为 hidden / detached 时 resolve 为 true 而不是元素
LFDOM.fillForm(map, options?): Promise<Record<string, true>>按「CSS 选择器 / @N 引用 → 值」映射批量填表(键不支持 label 文本)
LFDOM.findInVirtualList(options): Promise<Array<Item>>滚动收集虚拟列表/瀑布流的全部条目并去重
LFDOM.waitForResponse(urlPattern, options?): Promise<NetworkRecord>等待命中 URL 模式的响应(基于 LFNetwork;响应头到达时即 resolve)
LFDOM.waitForRequest(urlPattern, options?): Promise<NetworkRecord>等待命中 URL 模式的请求
LFDOM.intercept(urlPattern, handler): () => void猴补丁当前 JS 世界的 fetch/XHR 做 mock(返回取消函数)。仅适合 run --tab(MAIN 世界)临时探测:已保存应用与 liveDebug 运行在隔离世界,看不到页面自身的请求,正式 Compile 会直接拒绝包含 LFDOM.intercept 的源码。产品代码请用 LFRuntime.network 并声明 @intercept。未取消的 handler 在 run 进程结束 / liveDebug 热重载时自动注销;最后一个 handler 注销后还原原始的 fetch/XHR
LFDOM.screenshot(target?, options?): Promise<string>把页面或元素的 DOM 序列化进 SVG foreignObject,返回 data:image/svg+xml 字符串——不是像素截图(不含外部样式表、canvas、video、跨域图片);需要真实截图请用终端 screenshot 命令
LFDOM.actAsHuman(options?): LFDOM返回一个代理对象:只有经该代理调用的顶层 click / dblclick / hover / fill / type / press 会先随机延迟;不改变 LFDOM 本身,也不影响 Locator 上的动作
await LFDOM.snapshot(options?): Promise<{ text, nodes, cards, ... }>生成带 @N 引用的语义快照文本,适合「看一眼再操作」
await LFDOM.cards(options?): Promise<CardRecord[]>把重复卡片结构(商品列表等)聚合为结构化记录
await LFDOM.layoutAudit(root?, options?): Promise<{ text, issues, count }>布局体检:重叠/溢出/对比度问题列表
await LFDOM.fireReactHandler(selector, eventName?): Promise<boolean>直接触发 React 合成事件处理器(顽固 hover 场景)
参数详解
createFloatingRoot(options?) / toast(message, duration?)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
options.id | string | userscript-lf-root | 根节点的 DOM id;同一应用相同 id 复用同一元素,该 id 已被其他应用/页面占用时改用 <id>--lf-<应用id> |
options.className | string | '' | 每次调用都会追加到根节点的 classList(复用已有元素时同样追加) |
options.zIndex | number / string | 2147483000 | 新建根节点的 z-index,留有余量给站点自身的弹层(复用已有元素时不修改) |
message(toast) | string | — | 纯文本,经 textContent 赋值,不解析 HTML |
duration(toast) | number | 2400 | 总显示毫秒数;返回元素可调 .remove() 提前关闭 |
新建的根节点默认样式为 position: fixed; top: 16px; right: 16px; max-width: 420px。被页面脚本移除时会自动补挂回 body;调用根节点自己的 .remove() 才会真正移除。
定位器 Locator
LFDOM.locator(selector, { root?, pierce? }) 创建惰性定位器:动作前自动等待元素挂载/可见/稳定/可用(默认超时 5000ms 并自动重试),默认穿透开放 Shadow DOM。selector 必须是原生 CSS 或 snapshot 的 @N 引用;:has-text() 等 Playwright 专有伪类会在构造时直接抛错——文本匹配用语义构造器:
| 构造器 | 说明 |
|---|---|
getByRole(role, { name?, exact? }) | 按可访问角色 + 可访问名匹配(button/link/textbox/checkbox 等,含隐式 HTML 角色);操作交互控件的首选方式 |
getByText(text, { exact? }) | 按可见文本匹配,返回包含文本的最内层元素;默认子串匹配(大小写不敏感、空白归一化),exact: true 精确匹配 |
getByLabel(text) | 按关联 label 匹配表单控件(解析 aria-label / for= / 包裹 label),返回控件本身 |
getByPlaceholder(text) / getByTitle(text) / getByAltText(text) / getByTestId(id) | 按 placeholder / title / img alt / data-testid 匹配 |
链式收窄:locator(child)、filter({ hasText, hasNotText, has, hasNot })、first() / last() / nth(i)、and(other) / or(other)、all()、count()。语义查询可作用在父定位器上(如 row.getByTitle('删除')),是消除重复按钮歧义的推荐方式。中文标签常互为子串(如「删除」与「批量删除」),必要时传 { exact: true } 或先用父容器限定范围。
| 动作/读取 | 说明 |
|---|---|
click({ modifiers?, position?, force? }) / dblclick / tripleClick / hover / tap / dragTo(target) | 完整的浏览器事件序列(pointer→mouse→click);modifiers: ['Control'] 支持 Ctrl+Click 多选等 |
fill(value) / type(text) / clear() | 支持 input/textarea 与 contenteditable,触发 input/change 让 React/Vue 受控组件正常更新;目标 readonly/disabled 时直接抛错,{ force: true } 强写 |
press(key) | 带真实默认行为的按键(字符插入、Backspace、方向键移动光标、Control+a 全选、Enter 提交表单、Tab 焦点移动);组合键用 Modifier+Key 语法 |
selectOption(value) | 仅支持原生 select;接受字符串(按 value 或可见文本)、{ value } / { label } / { index } 或数组;自定义下拉需像用户一样点触发器再点选项 |
check() / uncheck() / setChecked(bool) / setFiles(files) | 复选框/单选/文件输入;隐藏 input + 样式 label 的自定义控件会自动转投可见 label;setFiles 返回回执 { ok, count, names, input } 而不是元素 |
focus() / blur() / scrollIntoViewIfNeeded() | 焦点与滚动 |
waitFor({ state, timeout }) | state 可为 attached / visible(默认)/ hidden / detached / stable |
textContent() / innerText() / innerHTML() / inputValue() / getAttribute(name) / boundingBox() | 读取内容与几何信息 |
isVisible() / isHidden() / isEnabled() / isDisabled() / isChecked() / isEditable() | 状态判断 |
elementHandle() / elementHandles() / elementHandleOrNull() | 取原生元素(前者自动等待挂载,后两者立即返回) |
evaluate(fn) / evaluateAll(fn) | 对匹配元素执行自定义函数 |
allInVirtualList(options?) | 在当前 Locator 作用域内滚动收集虚拟列表,等价于 findInVirtualList({ ...options, locator: this });参数同下文 findInVirtualList,请指定 itemSelector。返回 Promise<Array<{ key, text, html, href, dataset }>> |
fireReactHandler(eventName?, options?) / dispatchReact(eventName?, options?) | 两者等价,返回 Promise<boolean>。eventName 为 React 事件名,默认 'onMouseEnter';支持 options.modifiers(如 ['Control']),内部以 force: true 跳过可操作性检查。先在当前世界调用 handler,未命中时转 MAIN world;无 handler 返回 false,元素不存在默认等待 5000ms 后超时。用于 hover() 无效时兜底 |
mountAnchored / mountFloating(options)
把按钮/面板挂到页面已有元素旁。两者共用大部分选项,内部走 LFRuntime.mountHot,liveDebug 重注入时复用元素并调用 onUpdate 而不是重复插入:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key | string | 必填 | 挂载槽标识 |
anchor | string / Element / LFLocator / () => Element | 必填 | 锚点:CSS 选择器、元素、语义定位器或返回元素的函数(anchorSelector 为别名) |
position(anchored) | 'beforebegin' / 'afterbegin' / 'beforeend' / 'afterend' | 'afterend' | 相对锚点的插入位置(参与原生布局);其他取值静默回落为 'afterend' |
placement(floating) | 'left' / 'right' / 'top' / 'bottom' / 'overlay' | 'right' | 浮层相对锚点的方位(position:fixed 跟随,不进入锚点父级子节点序列) |
offset(floating) | number | 8 | 与锚点的间距(替代 margin) |
host(floating) | Element | document.body | 浮层元素的挂载父节点;浮层强制 position: fixed、margin: 0,未设置 z-index 时为 2147483646 |
html / factory / vue | — | 三选一 | HTML 片段 / 工厂函数 / Vue 组件配置 { createApp, h?, component, props?, setup? }(响应式 props 必须同时传 h) |
onMount / onUpdate / onDispose | Function | — | 挂载 / 热更新 / 卸载回调 |
persistent | boolean | true | liveDebug 卸载时保留元素;需要严格清理时传 false 或调 unmountAnchored(key) |
reanchorOnLost | boolean | true | 锚点被框架替换后自动重新解析(仅显式传 false 关闭;string/Locator/function 锚点可用)。已知限制:只在锚点的直接父容器保持不变时生效——框架连同父容器或更高层祖先一起重渲时,锚定元素会随旧子树消失且不再重挂,浮层也不会跟随新锚点 |
anchorTimeout | number | 30000 | 等待锚点出现的超时毫秒数 |
pierce | boolean | false | 锚点查询是否穿透 Shadow DOM(与定位器默认穿透不同,锚点在 shadow 内时需显式传 true) |
suppressInsertionWarning(anchored) | boolean | false | 关闭 liveDebug 下 beforebegin / afterend 插入框架容器的提示 |
挂载是异步的:返回的 state 提供 state.ready(Promise),操作挂载出的元素前先 await state.ready 或使用 onMount 回调。选型:锚点父级是稳定容器时优先 mountAnchored;必须挨着 Vue/React 管理的按钮显示时用 mountFloating,避免破坏框架的子节点 diff。
mountHoverAction(options)
卡片流悬停操作统一使用此方法。单个持久 fixed root 通过 document 级 pointer 委托跟随当前卡片,处理延迟隐藏及 scroll / resize 重定位。
| 选项 | 说明 |
|---|---|
key | 必填;bundle 内唯一、跨热更新保持不变的槽位标识 |
cardSelector | 必填;卡片的 CSS 选择器 |
html / factory(state) => Element | 二选一,生成共享操作元素;也可改用 vue |
vue | 替代 html / factory 的 Vue 配置 { createApp, h?, component, props?, setup? } |
placement | 默认 'top-right-inside';另支持 'top-left-inside' / 'bottom-right-inside' / 'bottom-left-inside' |
offset | 相对卡片的位置偏移 |
hideDelay | 隐藏延迟 |
getIdentity(card) | 显示时捕获稳定业务标识,直接传给 onActivate,无需重新读取 hover 状态 |
onActivate({ event, card, identity, element, state }) | 点击回调 |
onShow / onHide / onMount / onDispose | 显示 / 隐藏 / 挂载 / 卸载回调 |
返回 HoverActionState,包含:
| 字段 / 方法 | 说明 |
|---|---|
root / element | 共享的 fixed root / 操作元素 |
activeCard / identity | 当前活动卡片 / 业务标识 |
visible | 是否显示 |
show(card) | 在指定卡片上显示 |
hide() | 隐藏 |
reposition() | 重新定位 |
onActivate在 root 捕获阶段处理真实 click,onMount中内部按钮的stopPropagation()/preventDefault()不影响回调,通常无需转发 click。- Vue 按钮必须使用
vue配置:复用单个实例并自动卸载,不要另写 HoverMount /createFloatingRoot/createApp状态机。 - 不要在
mouseover/mouseout中反复调用mountFloating/unmountFloating,避免按钮在mousedown与click之间被移除。
示例
// @require dom
LFDOM.mountHoverAction({
key: "product-hover-action",
cardSelector: "[data-product-id]",
html: '<button type="button">查看商品编号</button>',
getIdentity: (card) => card.dataset.productId,
onActivate: ({ identity }) => LFDOM.toast(`商品编号:${identity}`),
});snapshot(options?) / cards(options?)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
interactiveOnly | boolean | false | 只保留可交互角色(button/link/textbox 等);标题等有名字的非交互节点也会被丢弃 |
viewportOnly | boolean | false | 只保留与视口相交的节点 |
contentOnly | boolean | false | 跳过 footer/banner/navigation 等地标区域 |
groupCards | boolean | false | 把重复卡片折叠为单行并填充返回值的 cards[] |
cardMinRepeat | number | 3 | 识别为卡片组的最小重复次数(最小取 2) |
cardSelector(cards) | string | — | 启发式失效时的确定性兜底:直接指定卡片元素选择器 |
cardRoot | Element | 同 root | 只在该子树内识别卡片 |
maxNodes / maxCards | number | 4000 / 200 | 输出上限 |
maxCardActions / maxCardDescendants | number | 12 / 150 | 单张卡片收集的动作数上限 / 识别卡片时扫描的后代数上限 |
root | Element | document.documentElement | 限定遍历子树;必须是 Element(Document / ShadowRoot 会被忽略并回落默认值) |
snapshot 返回 { text, nodes, cards, count, refCount }:text 是带 @N 引用的缩进可访问性树;nodes[] 每项含 { ref, role, name, depth, url, region, states }。@N 引用可传给任何接受选择器的地方——LFDOM.locator('@42'),或顶层快捷方法如 await LFDOM.click('@42')——但只对最近一次 snapshot 有效:每次 snapshot 都会重建引用表,页面变化后须重新 snapshot,失效引用会直接抛错。cards 返回卡片数组,每项 { index, depth, region, ref, title, url, image, text, actions: [{ ref, role, name }] },适合商品列表/信息流的结构化遍历。
其他常用方法
| 方法 | 参数说明 |
|---|---|
waitForElement(selector, { timeout?, state? }) | state 默认 'attached',timeout 默认 5000ms;attached/visible/stable 时返回元素,hidden / detached 时返回 true |
fillForm(map, { root?, timeout? }) | map 的键是 CSS 选择器或 @N 引用(不支持直接写 label 文本,按 label 定位请先用 getByLabel 再逐个操作),值按控件类型自动选择 fill/selectOption/setChecked;任一字段失败即抛错,已填字段不回滚;root 可限定到 iframe 文档或 ShadowRoot |
findInVirtualList({ itemSelector, scrollContainer?, maxItems?, idleMs?, dedupeKey?, timeout? }) | 滚动收集虚拟列表条目并去重,返回 { key, text, html, href, dataset }[]。itemSelector 实际必填(省略时为 '*',会匹配到整页元素);scrollContainer 可为选择器字符串、Element 或 Locator,省略或传 'window' 时整页滚动;maxItems 默认 100,idleMs(连续无新条目即停止)默认 300、最小 50;dedupeKey 默认依次取 data-id、href、文本,也可传 (element) => string |
waitForResponse(urlPattern, { timeout?, networkOptions? }) / waitForRequest(...) | urlPattern 支持字符串(子串)/正则/谓词函数;timeout 默认 5000ms。JS 监听在调用时同步注册,但若 LFNetwork 尚未在监听,会在后台异步 start(),调试器附加完成前发出的请求会被漏掉——首次使用前先 await LFNetwork.start()(需 // @require network),再「先建 wait、后触发」:const p = LFDOM.waitForResponse(/api/); await action(); const rec = await p;。waitForResponse 在响应头到达时即 resolve(rec.finished 通常仍为 false),要读响应体请等 finished 后再 LFNetwork.getResponseBody(rec.requestId)。由它隐式启动的监听不会自动停止;已在监听时 networkOptions 被忽略 |
intercept(urlPattern, handler) | 仅 MAIN 世界临时探测(见上文)。urlPattern 支持字符串(子串)/正则/谓词;handler 收到 { type: 'fetch', url, input, init } 或 { type: 'xhr', method, url, body },返回 { body, status?, headers? } 即 mock(status 默认 200),返回假值放行;XHR mock 只设置 readyState / status / responseText |
screenshot(target?, options?) | target 为 CSS 选择器或 Element(不接受 Locator),省略时为整个文档;options 只影响等待元素出现;返回 data:image/svg+xml;charset=utf-8,... |
actAsHuman({ delay? }) | 返回代理对象;经代理调用的顶层 click/dblclick/hover/fill/type/press(selector, …) 先随机等待 delay~2×delay 毫秒(delay 默认 80) |
clickAt(x, y, { modifiers? }) | 按视口坐标命中最深元素并派发完整事件序列(穿透开放 Shadow DOM);该点无元素时抛错;仅用于 canvas 热区等真坐标场景 |
layoutAudit(root?, { maxIssues?, contrast?, overlap? }) | root 可为选择器/Element/Locator,省略或匹配不到时回落 body;maxIssues 默认 50(上限 200),contrast / overlap 默认 true。返回 { text, issues: [{ type, element, detail }], count },element 是元素的描述字符串而不是 Element;issue 类型含横向溢出、零尺寸交互元素、被遮挡、兄弟重叠、文本截断、低对比度等 |
fireReactHandler(selector, eventName?) | 直接调用 React fiber 上的合成事件处理器(默认 onMouseEnter):元素存在但无 handler 时返回 false,元素不存在时等待超时(默认 5000ms)后抛错;hover 无法展开浮层时的兜底 |
frameLocator(selector) | 进入同源 iframe:LFDOM.frameLocator('#if').getByRole(...);可链式穿多层 iframe,支持 nth(i) / first() / last();.frame() 返回 Promise<Document> |
顶层快捷方法
以下方法等价于 LFDOM.locator(selector).xxx(...),selector 同样接受 CSS 或 @N 引用,自动等待规则与默认 5000ms 超时相同:
| 方法 | 等价于 |
|---|---|
click(selector, options?) / dblclick / hover | locator(selector).click(options) 等 |
fill(selector, value, options?) / type(selector, text, options?) / press(selector, key, options?) / clear(selector, options?) | 对应的 Locator 输入动作 |
text(selector) / value(selector) / attr(selector, name) | innerText() / inputValue() / getAttribute(name) |
exists(selector) / visible(selector) / count(selector) | 立即判断是否存在(不等待)/ isVisible() / count() |
waitFor(selector, options?) / waitForHidden(selector, options?) | locator(selector).waitFor(options)(默认 state 'visible')/ state 固定为 'hidden' |
getByRole / getByText / getByLabel / getByPlaceholder / getByTitle / getByAltText / getByTestId | 全页范围的语义构造器(见「定位器 Locator」) |
waitForUrl(pattern, options?) | 轮询直到 location.href 命中(字符串子串/正则/谓词),resolve 为当前 URL |
waitForLoadState(state?) | 默认 'domcontentloaded';'networkidle' 只是固定等待 500ms,并不检测网络空闲 |
waitForFunction(fn, options?) | 轮询直到 fn() 返回真值 |
mountAnchored / mountFloating | 与 mountAnchoredHTML / mountFloatingHTML 相同 |
timeoutError(message, details?) | 构造与库内一致的超时错误对象 |
示例
// @require dom
await LFDOM.whenReady();
// Playwright 风格的定位器:自动等待、自动重试
await LFDOM.locator("#submit-button").click();
await LFDOM.locator("input[name=q]").fill("keyword");
// 语义快照:把页面结构变成带 @N 引用的文本(看 → 操作)
const snap = await LFDOM.snapshot({ interactiveOnly: true });
LFRuntime.print(snap.text);
// 等待元素出现
const row = await LFDOM.waitForElement(".order-row", { timeout: 8000 });