Puppeteer Page.frames() 方法详解:获取页面上的全部 Frame 并精准操作 iframe
2026/9/8 19:10:52 网站建设 项目流程

Puppeteer Page.frames() 方法详解:获取页面上的全部 Frame 并精准操作 iframe

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

Page.frames()是 Puppeteer 中用于获取「当前页面所挂载的全部 Frame」的核心方法,返回值为Frame[]。凡涉及页面主文档(Main Frame)与各类内嵌文档(<iframe>、跨源 iframe、嵌套 iframe)的枚举、筛选与逐帧自动化操作,都从该方法出发。读完本文,你将掌握frames()的 API 契约、帧树(Frame Tree)的底层组织方式、它与mainFrame()的区别,以及基于返回Frame[]编写可靠测试与爬虫脚本的实战模式。

一、方法签名与返回值语义

根据 docs/api/puppeteer.page.frames.md 的定义,该方法在Page类上声明如下:

class Page { abstract frames(): Frame[]; }

返回值Frame[],即一个由「附加到页面上的所有 Frame」构成的数组。

1.1 何为「附加到页面上的 Frame」

在 Puppeteer 的抽象设计里,Frame指页面中的一段独立执行上下文文档,包括:

  • 主框架(Main Frame):页面最顶层的文档;
  • 子框架(Child Frame):由<iframe><frame>引入的文档,可多层嵌套;
  • 跨源/跨进程 iframe(OOPIF):即使是运行在独立渲染进程中的跨源 iframe,也仍然会出现在该数组中。

从 abstract Frame 类的源码 可以看到,Frame自身是EventEmitter<FrameEvents>的子类,每个实例携带_id_parentId_name等内部元数据,并向上关联所属Pageabstract page(): Page)。

1.2 返回数组的成员对象提供了什么

数组中每个Frame实例(统一为 Frame 抽象类型)都可以继续执行导航、求值与选择器操作,常用成员包括:

  • frame.url():获取该 Frame 当前文档的 URL(声明位置);
  • frame.parentFrame():返回父 Frame,主框架返回null(声明位置);
  • frame.childFrames():返回该 Frame 直接挂载的子 Frame 数组(声明位置),细节见 docs/api/puppeteer.frame.childframes.md;
  • frame.detached(getter):当前 Frame 是否已从页面分离(声明位置);
  • 以及frame.evaluate()frame.waitForSelector()frame.goto()frame.$(...)等一系列与Page同名但作用域限定在该 Frame 内的方法。

二、frames()mainFrame()的关系与区别

Page.frames()的返回结果是「整棵帧树的扁平集合」,其中自然包含主框架;Puppeteer 也提供独立方法page.mainFrame()直接获取主框架(见 docs/api/puppeteer.page.mainframe.md)。

两者的用途差异:

场景推荐 API
只操作页面主文档本身page.mainFrame()
需要遍历或统计全部内嵌 iframepage.frames()
明确知道 frame 的 idpage.mainFrame().frame(id)/ 内部frameManager.frame(frameId)
页面会新开子 Frame,需要等待其出现page.waitForFrame(...)(见 docs/api/puppeteer.page.waitforframe.md)

需要强调的是:返回数组的下标位置不是 API 契约。从实现看,列表源于内部帧树以 frameId 为键的Map的遍历顺序(详见下文第三节),因此虽然仓库测试用例中常以frames()[0]代指主框架、以frames()[1]代指首个 iframe(见 test/src/frame.test.ts、test/src/acceptInsecureCerts.test.ts),但更稳健的写法是根据语义筛选,例如:

const mainFrame = page.frames().find(f => f.parentFrame() === null)!; const adFrame = page.frames().find(f => f.url().includes('ads.example.com'));

三、源码级拆解:frames()的完整调用链

3.1 抽象层声明:所有协议实现必须提供

在 packages/puppeteer-core/src/api/Page.ts#L1019-L1022 中,frames()被声明为抽象方法,其 JSDoc 即文档所描述的 “An array of all frames attached to the page.”。由于 Puppeteer 同时面向 Chrome(CDP)与 Firefox(WebDriver BiDi)等多协议后端,抽象层保证了无论底层走哪条协议,上层使用者拿到的都是统一的Frame[]

3.2 CDP 实现:逐层转发到帧树

Chrome/Chromium 后端的实现位于 packages/puppeteer-core/src/cdp/Page.ts#L621-L623:

override frames(): Frame[] { return this.#frameManager.frames(); }

随后FrameManager在 packages/puppeteer-core/src/cdp/FrameManager.ts#L280-L282 中进一步委托给帧树结构:

frames(): CdpFrame[] { return Array.from(this._frameTree.frames()); }

最终数据来源是内部维护帧树的FrameTree类(packages/puppeteer-core/src/cdp/FrameTree.ts),其核心状态与行为为:

  • #frames = new Map<string, FrameType>():以 frameId 为键存放当前已知的 Frame 实例,frames()方法即Array.from(this.#frames.values())(第 51-53 行);
  • #parentIds#childIds:分别记录frameId -> parentIdparentId -> 子 frameId 集合,支撑parentFrame()/childFrames()的查询;
  • addFrame()(第 55-70 行):当浏览器端通过Page.getFrameTree等协议事件发现新 Frame 时,按插入顺序写入 Map,若该 frame 无父 id 且主框架空缺或已失效,则将其设为主框架;
  • removeFrame()(第 72-80 行):Frame 销毁/导航脱离后从 Map 与父子索引中同步删除,并标记主框架可能已过期。

由此可以印证文档中 “attached(已附加)” 的措辞:只有仍存在于帧树中的 Frame 才会出现在frames()返回值里,已分离的 Frame 会被removeFrame移除,不再出现在该列表中。

3.3 帧的发现与生命周期

frames()返回的CdpFrame是在FrameManager初始化目标会话时,通过发送 CDP 命令Page.getFrameTree取得整棵帧树后逐节点构建的(见 packages/puppeteer-core/src/cdp/FrameManager.ts#L240-L246)。此后 iframe 的动态增删由帧树增量维护。

值得注意的边界情况:跨源 iframe 若运行在独立进程中(out-of-process iframe),其拥有自己的CDPSession。在 FrameManager 对全部 Frame 批量执行操作(如#forEachFrame下发脚本)时,会单独处理此类 frame 会话可能抛出的Target closed错误(packages/puppeteer-core/src/cdp/FrameManager.ts#L288-L307)。这从侧面说明:frames()是包含 OOPIF 的全量视图,跨进程 iframe 并不会缺席,因此在多 iframe 页面上遍历时需要做好目标随时关闭的兜底。

四、实战用法:基于frames()编写可靠脚本

4.1 基础示例:枚举页面上的所有 Frame

import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com/with-iframes', { waitUntil: 'networkidle0', }); // 列出所有附加在页面上的 frame for (const frame of page.frames()) { console.log({ url: frame.url(), isMainFrame: frame.parentFrame() === null, childCount: frame.childFrames().length, }); } // 统计 iframe 数量(排除主框架) const iframeCount = page.frames().filter(f => f.parentFrame() !== null).length; console.log(`iframe 总数: ${iframeCount}`); await browser.close();

4.2 定位并操作特定 iframe

// 按 URL 关键字筛选目标 iframe const frame = page.frames().find(f => f.url().includes('/embed/')); if (frame) { // 在 iframe 自己的文档上下文内求值 const title = await frame.evaluate(() => document.title); const h1 = await frame.$('h1'); // frame 内继续等待并点击元素 await frame.waitForSelector('button#confirm'); await frame.click('button#confirm'); }

仓库测试中大量使用这种「从frames()取出目标帧再在帧内evaluate」的范式,例如 test/src/evaluation.test.ts 分别对主框架与 iframe 求值,以及 test/src/cdp/network_restrictions.test.ts 用page.frames().find(f => f !== page.mainFrame())挑选非主框架。

4.3 处理嵌套 iframe

frames()返回的是全量扁平列表,嵌套深度不影响检索:三层嵌套的 iframe 同样会在数组中。对层级敏感的场景可结合childFrames()逐层下钻——测试用例 test/src/elementhandle.test.ts#L43 就通过page.frames()[1]!.childFrames()[1]!访问第二层子帧:

const outer = page.frames().find(f => f.url().endsWith('/outer.html')); const inner = outer?.childFrames().find(f => f.url().endsWith('/inner.html')); if (inner) { const text = await inner.evaluate(() => document.body.innerText); }

4.4 断言自动化脚本中的 iframe 状态

自动化测试中常用frames()断言页面结构是否符合预期:

// 主页面加载了两段嵌套 iframe expect(page.frames()).toHaveLength(2); expect(page.frames()[1]!.url()).toBe('https://example.com/empty.html'); expect(page.frames()[0]!.parentFrame()).toBeNull(); // 主框架没有父级

上述断言的写法直接对应仓库帧测试 test/src/frame.test.ts#L234-L244 中的既有模式,可用于校验单测/集成测试对帧树的预期。

4.5 与FrameAttached/waitForFrame配合处理动态 iframe

若 iframe 是在页面运行中异步注入的,初次调用frames()时可能尚未出现。此时应优先使用事件或等待机制:

import puppeteer, {PageEvent} from 'puppeteer'; const page = await browser.newPage(); // 方式一:事件驱动(PageEvent.FrameAttached / FrameDetached 见 // docs/api/puppeteer.pageevents.md 与 docs/api/puppeteer.pageevent.md) page.on(PageEvent.FrameAttached, frame => { console.log('frame attached:', frame.url()); }); // 方式二:等待指定 frame 出现 const widgetFrame = await page.waitForFrame( frame => frame.url().includes('widget'), {timeout: 10_000}, );

随后即可把从frames()中拿到的逻辑抽象成工具函数,在「轮询枚举」与「事件等待」两条路径之间复用同一套针对Frame的操作代码。

五、使用注意与常见误区

  1. 不要依赖下标语义:虽然当前 CDP 实现中帧列表按FrameTree内 Map 的插入顺序输出、主框架通常排在最前,但这属于内部实现细节而非公开保证。跨浏览器后端(如 WebDriver BiDi)与未来的版本更新都可能改变顺序,稳妥做法是用parentFrame()url()等语义化筛选。
  2. 结果随帧树动态变化frames()是每次调用时对当前FrameTree快照的取值,与页面实际帧状态可能有时差;若刚触发导航/插入 iframe,应先waitForFrame或等待网络空闲再做枚举。
  3. Frame 分离后的对象不可复用:被移除的 frame 不再出现在frames()中;若已持有其引用,其上操作可能抛错(跨进程 frame 被销毁时尤其明显),脚本应捕获相应错误或重新枚举。
  4. 在 frame 与 page 之间区分求值作用域page.evaluate作用于主框架文档,若要对 iframe 内 DOM 求值,必须使用从frames()(或waitForFrame)中取得的Frame实例的evaluate

六、小结

Page.frames()是 Puppeteer 页面模型中最常被低估的入口之一:文档(docs/api/puppeteer.page.frames.md)给出的契约只有一句话——“所有附加到页面上的 Frame 数组”,但其背后是 api/Page.ts 的抽象声明、cdp/Page.ts 的协议实现,以及 FrameTree 内以 frameId 为键的全量帧树。掌握它的返回值语义、与mainFrame()/childFrames()/waitForFrame()的分工,以及“语义化筛选 + 帧内求值”的实战组合拳,就能在任何含 iframe 的复杂页面上写出既精确又不易碎的 Puppeteer 自动化代码。

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询