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等内部元数据,并向上关联所属Page(abstract 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() |
| 需要遍历或统计全部内嵌 iframe | page.frames() |
| 明确知道 frame 的 id | page.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 -> parentId与parentId -> 子 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的操作代码。
五、使用注意与常见误区
- 不要依赖下标语义:虽然当前 CDP 实现中帧列表按
FrameTree内 Map 的插入顺序输出、主框架通常排在最前,但这属于内部实现细节而非公开保证。跨浏览器后端(如 WebDriver BiDi)与未来的版本更新都可能改变顺序,稳妥做法是用parentFrame()、url()等语义化筛选。 - 结果随帧树动态变化:
frames()是每次调用时对当前FrameTree快照的取值,与页面实际帧状态可能有时差;若刚触发导航/插入 iframe,应先waitForFrame或等待网络空闲再做枚举。 - Frame 分离后的对象不可复用:被移除的 frame 不再出现在
frames()中;若已持有其引用,其上操作可能抛错(跨进程 frame 被销毁时尤其明显),脚本应捕获相应错误或重新枚举。 - 在 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),仅供参考