Puppeteer Frame.childFrames() 方法详解:遍历 iframe 子 Frame 树与页面框架结构
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
Frame.childFrames()是 Puppeteer 中用于获取某个Frame(DOM 框架,通常对应页面内的<iframe>)直接子 Frame 列表的核心方法。它配合Page.mainFrame()、Frame.parentFrame()与Page.frames(),即可从页面根框架出发递归访问整棵嵌套的页面结构,进而操作 iframe 内的 DOM、脚本执行与导航。阅读本文后,你将掌握childFrames()的方法契约、典型递归用法、Chrome 与 Firefox 两套协议下的底层实现原理,以及 Frame 生命周期相关的事件处理要点。
该方法的权威 API 定义位于本仓库的 docs/api/puppeteer.frame.childframes.md,本文将以它为骨架,结合源码实现与测试用例进行纵深展开。
Frame 与 Frame 树:childFrames() 存在的上下文
在浏览器中,一个<iframe>元素会加载出独立的 DOM 世界:外层文档里的 JavaScript 无法直接访问 iframe 内部的 DOM,反之亦然。Puppeteer 用Frame抽象来代表这样一个“可独立执行 JavaScript、拥有独立 DOM”的框架。正如 Frame 抽象类定义 中的类注释所说:
Represents a DOM frame. Just like iframes, frames can be nested...
也就是说:
- Frame 可以嵌套(iframe 里再套 iframe,或页面通过
frameset/嵌套浏览上下文形成层级); - 任意时刻,页面都会以Frame 树的形态对外暴露其结构;
- 树的入口是主框架,可通过
Page.mainFrame()获取; Frame.childFrames()返回该 Frame 的直接子 Frame 数组,是遍历整棵树的“下探”手段;- 与之配套的还有
Frame.parentFrame()(获取父框架,主框架返回null)和Page.frames()(返回页面内全部 Frame 的扁平集合)。
childFrames() 方法契约
childFrames()是Frame基类声明的抽象方法,其完整签名如下:
class Frame { abstract childFrames(): Frame[]; }返回类型:Frame[]
关键语义:
| 要点 | 说明 |
|---|---|
| 返回内容 | 当前 Frame 的直接子Frame 数组(不含孙子辈),类型为Frame[] |
| 顺序 | 按浏览器内部维护的子 Frame 集合生成,不保证与 DOM 出现顺序严格一致 |
| 主框架 | 主框架的childFrames()即最顶层 iframe 们的集合 |
| 无子框架 | 返回空数组[],不会返回null |
| 空状态判断 | 直接使用frame.childFrames().length === 0判断即可 |
在 Frame.ts 中可以看到该抽象方法位于公开 API 区,CdpFrame与BidiFrame都必须实现它,从而保证同一套用户代码既能驱动 Chrome(CDP),也能驱动 Firefox(WebDriver BiDi)。
用 childFrames() 遍历整棵 Frame 树
由于子 Frame 可能继续拥有自己的子 Frame,遍历 Frame 树的标准写法是递归。Frame类的官方文档注释里就给出了这样一个可复制的完整示例——从主框架开始递归输出所有框架 URL:
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com/page-with-iframes.html'); dumpFrameTree(page.mainFrame(), ''); await browser.close(); function dumpFrameTree(frame, indent) { console.log(indent + frame.url()); for (const child of frame.childFrames()) { dumpFrameTree(child, indent + ' '); } }这段代码的运行逻辑清晰对应了 childFrames 的语义:
page.mainFrame()拿到整棵树的根;- 递归对每个 Frame 先打印
frame.url(); - 遍历
frame.childFrames()得到直接子节点,缩进后继续下钻; - 当某个 Frame 没有子框架时,
childFrames()返回空数组,循环自然终止,递归收敛。
如果你希望输出更丰富的结构(比如附带<iframe>的name/id),可以参考 Puppeteer 测试代码 test/src/utils.ts 中的dumpFrames辅助函数——它先调用frame.frameElement()拿到承载该 Frame 的元素,再通过element.evaluate(...)读取frame.name || frame.id追加到描述中,然后同样用for (const child of frame.childFrames())递归缩进输出。这为调试复杂嵌套页面提供了很好的模板。
综合遍历:mainFrame、frames() 与 childFrames() 的分工
| 入口 | 返回 | 适用场景 |
|---|---|---|
page.mainFrame() | 页面主 Frame(树根) | 从根开始自顶向下递归 |
page.frames() | 页面内全部 Frame 的数组 | 直接扁平遍历所有框架 |
frame.childFrames() | 当前 Frame 的直接子 Frame | 树形下探、判断父子的包含关系 |
frame.parentFrame() | 父 Frame(根返回null) | 自底向上回溯 |
其中page.frames()收集整棵子树时,本质就是反复调用childFrames()展开——例如在 cdp/FrameManager.ts 与 BiDi 协议实现里,都能看到对mainFrame.childFrames()(或frame.childFrames())的递归汇总。
实战:按 name 定位 iframe 并读取其内容
childFrames()最常见的落地场景是:页面里嵌套了 iframe,你需要拿到某个具名 iframe 的Frame对象,然后在其中查询 DOM。Frame类注释中同样给出了标准写法:
const frames = page.frames(); let frame = null; for (const currentFrame of frames) { const frameElement = await currentFrame.frameElement(); const name = await frameElement.evaluate(el => el.getAttribute('name')); if (name === 'myframe') { frame = currentFrame; break; } } if (frame) { const text = await frame.$eval( '.selector', element => element.textContent, ); console.log(text); } else { console.error('Frame with name "myframe" not found.'); }这段示例的两个关键 API 都与 Frame 树相关:
frameElement():返回承载该 Frame 的<iframe>元素句柄,借此拿到元素的name属性;frame.$eval(selector, fn):在该 Frame 自己的 DOM 世界里执行选择器查询并求值,体现了“每个 Frame 的 JS 互相隔离”这一特性。
如果 iframe 还有多层嵌套,比如某页面结构为 主框架 → iframeA → iframeB,那么page.frames()只能给出扁平视图,此时使用树形关系才能精确表达父子层级。仓库测试 test/src/elementhandle.test.ts 中就有page.frames()[1]!.childFrames()[1]!这样的调用,用于定位深层嵌套的子框架后执行操作——可见在需要精确控制某一层子框架时,直接对已知父 Frame 调用childFrames()比在page.frames()里逐个匹配更直接。
底层实现:CDP 与 WebDriver BiDi 双协议如何产出子 Frame
childFrames()是抽象方法,Puppeteer 依据浏览器连接协议提供了两套实现。
Chrome/Chromium(CDP):由 FrameTree 集合驱动
在 CDP 实现 cdp/Frame.ts 中,CdpFrame的做法是直接委托给 FrameManager 维护的FrameTree:
override childFrames(): CdpFrame[] { return this._frameManager._frameTree.childFrames(this._id); }FrameTree是 Puppeteer 内部维护的“frameId → 父子关系”索引(见 cdp/FrameTree.ts),其childFrames(frameId)的核心逻辑为:
childFrames(frameId: string): FrameType[] { const childIds = this.#childIds.get(frameId); if (!childIds) { return []; } return Array.from(childIds) .map(id => this.getById(id)) .filter((frame): frame is FrameType => frame !== undefined); }即:为当前 frameId 维护一个子 frameId 集合#childIds,读取时把集合中每个 id 映射回对应的 Frame 对象,并过滤掉已不存在(如已被移除)的项,最终拼装成数组返回。相应地,removeFrame()在 Frame 被移除时会同步从父 Frame 的#childIds中删除该 id(FrameTree.ts),保证childFrames()不会返回已被销毁的脏对象。这与生命周期事件FrameDetached的派发保持了一致。
Firefox(WebDriver BiDi):由 browsing context 子树生成
在 BiDi 实现 bidi/Frame.ts 中,子 Frame 来自协议侧的浏览上下文(browsing context)层级关系:
override childFrames(): BidiFrame[] { return [...this.browsingContext.children].map(child => { return this.#frames.get(child)!; }); }即把当前 browsing context 的children列表映射到 Puppeteer 缓存的BidiFrame实例集合。两套实现殊途同归:无论底层协议如何组织树形关系,面向用户的childFrames()契约始终是“直接子 Frame 数组”。这也正是 Puppeteer 能让你在 Chrome 与 Firefox 间共享同一套框架遍历代码的原因。
子树变化:childFrames() 结果何时会变
Frame 树是动态的。iframe 被动态插入、导航替换、或整页刷新时,childFrames()返回的内容都会变化。Puppeteer 在Page上派发三个与 Frame 生命周期强相关的事件(见 api/Frame.ts 类注释):
PageEvent.FrameAttached:新 Frame 被加入树,此后它才会出现在父 Frame 的childFrames()中;PageEvent.FrameNavigated:Frame 内发生了导航,URL 等状态随之更新;PageEvent.FrameDetached:Frame 被移除(如 iframe 被删除、页面跳转),需在此事件里清理对子 Frame 的引用。
一个容易踩的坑是:保持一个过期的Frame引用继续调用其方法。Puppeteer 的throwIfDetached装饰器(见 api/Frame.ts)会在 Frame 已 detached 时抛出Attempted to use detached Frame '${frame._id}'.之类的错误。因此在事件回调或异步流程中缓存 Frame 时,应通过FrameDetached事件及时清理引用,而不是长期信任旧对象。
Puppeteer 内部也在多处依赖“导航后子树是否就绪”。例如 cdp/LifecycleWatcher.ts 会遍历frame.childFrames()判断加载完成状态;这提醒我们:childFrames()不只是查询 API,它还参与着导航生命周期判定这样的内部机制。
测试证据:childFrames() 的语义如何被验证
仓库的端到端测试可以佐证上面所有语义。以 OOPIF(out-of-process iframe,独立进程 iframe)相关测试 test/src/oopif.test.ts 为例:
- 第 39 行
expect(page.mainFrame().childFrames()).toHaveLength(2):验证主框架存在两个直接子 Frame; - 第 327 行通过
oopIframe.childFrames()[0]继续访问嵌套子框架; - 第 341 行
expect(oopIframe.childFrames()).toHaveLength(0):验证无子 Frame 时返回空数组。
这类断言直接锁定了childFrames()返回数组、元素为Frame、以及“只返回直接子节点”的行为契约,可作为你编写自己遍历代码时的参照基准。
使用要点与建议
把childFrames()用好,实践中请注意以下几点:
- 直接子节点 ≠ 全部后代。需要全量框架时优先
page.frames(),需要树形结构时才递归childFrames(),避免自己重复实现扁平化逻辑。 - 主框架没有父节点。
mainFrame().parentFrame()返回null,而mainFrame().childFrames()返回最外层 iframe 集合,边界情况要分开处理。 - iframe 元素与 Frame 对象是两种抽象。
frame.frameElement()拿到的是页面 DOM 里的元素句柄(可读取name/id、做样式/位置判断),frame.childFrames()拿到的是可执行 JS、可导航的子 Frame——二者配合才能完成“先定位、再深入”的完整操作。 - 在事件回调中及时更新引用。结合
FrameAttached/FrameDetached维护自己的 Frame 缓存,避免因旧引用调用被 detach 的 Frame 而抛错。
小结
Frame.childFrames()虽然只是一句abstract childFrames(): Frame[],却是 Puppeteer 操作 iframe 与嵌套页面结构的基石:Page.mainFrame()让你站到树的根部,childFrames()让你在任意节点向下延伸,parentFrame()与page.frames()分别提供回溯与扁平视图。在 Chrome 侧它由FrameTree的子节点索引即时组装,在 Firefox 侧它由 browsing context 的 children 关系映射而来,语义始终一致。配合 Frame 类文档 与仓库中 api/Frame.ts、cdp/FrameTree.ts 等实现文件,你可以放心地在自己的爬虫、自动化测试或页面监控脚本里用递归方式遍历整棵 Frame 树,安全访问任意一层 iframe 内部的世界。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考