ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

Puppeteer Page.frames() 方法详解:获取页面上的全部 Frame 并精准操作 iframe Puppeteer Page.frames() 方法详解获取页面上的全部 Frame 并精准操作 iframe【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteerPage.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引入的文档可多层嵌套跨源/跨进程 iframeOOPIF即使是运行在独立渲染进程中的跨源 iframe也仍然会出现在该数组中。从 abstract Frame 类的源码 可以看到Frame自身是EventEmitterFrameEvents的子类每个实例携带_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.mdframe.detachedgetter当前 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 同时面向 ChromeCDP与 FirefoxWebDriver BiDi等多协议后端抽象层保证了无论底层走哪条协议上层使用者拿到的都是统一的Frame[]。3.2 CDP 实现逐层转发到帧树Chrome/Chromium 后端的实现位于 packages/puppeteer-core/src/cdp/Page.ts#L621-L623override 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 Mapstring, 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 基础示例枚举页面上的所有 Frameimport 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 处理嵌套 iframeframes()返回的是全量扁平列表嵌套深度不影响检索三层嵌套的 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),仅供参考
返回列表