首页
/ Puppeteer Frame.content() 方法深度解析:获取含 DOCTYPE 的完整 HTML

Puppeteer Frame.content() 方法深度解析:获取含 DOCTYPE 的完整 HTML

2026-09-06 18:30:29作者:江焘钦

导读

在 Puppeteer 自动化测试与网页抓取场景中,读取页面当前 DOM 的完整 HTML 是最基础也最高频的需求之一。Frame.content() 是 Puppeteer 提供的用于获取一个 frame(主 frame 或 iframe 子 frame)完整 HTML 内容的官方方法,其返回值包含 <!DOCTYPE> 声明。本文以官方 API 文档为基础,结合 Puppeteer 仓库源码(packages/puppeteer-core/src/api/Frame.ts)与测试用例,深入剖析该方法的方法签名、底层实现原理、与 setContent() 的配套使用,以及针对子 frame 与内联 frame 的实战场景,帮助读者掌握一套可靠、可复用的页面内容读取方案。

方法签名:返回 Promise<string>

根据 官方 API 文档content() 属于 Frame 类的一个实例方法,其类型签名如下:

class Frame {
  content(): Promise<string>;
}
  • 语义:返回该 frame 的完整 HTML 内容,包含 DOCTYPE 声明(The full HTML contents of the frame, including the DOCTYPE)。
  • 返回类型Promise<string>。由于需要在浏览器页面上下文中执行脚本并取回结果,该方法是异步的,必须使用 await.then() 消费结果。
  • 是否抽象:从 Frame.ts 源码 可以看出,content()Frame 基类中的具体实现方法(非 abstract),因此无论底层走 CDP 还是 WebDriver BiDi,用户都能拿到一致的 API 形态。

Page.content():主 frame 的快捷方式

对绝大多数场景(单页、无 iframe),用户习惯直接调用 page.content() 而不必关心 frame。查看 Page.ts 源码 可以发现,Page.content() 本质上就是对主 frame 的转发:

/**
 * The full HTML contents of the page, including the DOCTYPE.
 */
async content(): Promise<string> {
  return await this.mainFrame().content();
}

也就是说 page.content()page.mainFrame().content()。理解这一层转发关系后,当页面存在多个 frame 时,就应该改用本文主角 Frame.content() 去精确读取特定 frame 的内容,而不是 page.content()——后者只能拿到主 frame 的 HTML。

源码级原理:如何“完整”到包含 DOCTYPE

content() 的实现并不依赖任何特殊的 CDP 命令,而是通过在目标 frame 的页面上下文中执行一段 JavaScript 来现场序列化 DOM。核心逻辑位于 packages/puppeteer-core/src/api/Frame.ts

@throwIfDetached
async content(): Promise<string> {
  return await this.evaluate(() => {
    let content = '';
    for (const node of document.childNodes) {
      switch (node) {
        case document.documentElement:
          content += document.documentElement.outerHTML;
          break;
        default:
          content += new XMLSerializer().serializeToString(node);
          break;
      }
    }

    return content;
  });
}

从源码可以提炼出几个关键设计点:

  1. @throwIfDetached 装饰器:如果该 frame 已从页面分离(如页面已跳转、iframe 被移除),调用会直接抛错,避免在失效的 frame 上做无意义操作。
  2. 遍历 document.childNodes 而非只取 documentElementdocument 的直接子节点不仅包含 <html>document.documentElement),还包含 <!DOCTYPE html> 以及可能存在的顶层注释节点。这正是 content() 能“包含 DOCTYPE”的根因——如果只序列化 document.documentElement.outerHTML,DOCTYPE 会丢失。
  3. documentElement 使用 outerHTML:外层元素用 outerHTML 可一并带上 <html> 标签本身。
  4. 对其余节点使用 XMLSerializer().serializeToString(node):DOCTYPE 与注释节点没有 outerHTML 意义上的等价物,必须借助 XMLSerializer 进行序列化。serializeToString<!DOCTYPE html> 这类节点会输出标准的 <!DOCTYPE html> 文本。
  5. 实现本质是同步拼接:循环内直接做字符串累加后返回,因此对于极大页面,返回字符串体积可能很大,内存占用与结果字符串成正比,批量抓取时需注意。

需要补充说明的是:content() 是在“被调用时刻”对当前 DOM 序列化得到的一张快照字符串,它不代表将来文档的实时变化;调用后页面再通过 JS 改动 DOM,content() 的结果并不会随之更新,必须重新调用才能取得最新内容。

与 CDP / BiDi 协议无关的实现事实

由于 content() 建立在 Frame.evaluate 之上,它不依赖某一特定协议专有接口。从代码结构可以推断,只要 frame 的 realm 上下文可用,CDP 连接与 WebDriver BiDi 连接都能复用同一实现路径,跨浏览器(Chrome / Firefox)行为保持一致。

配套 API:与 setContent() 的读写闭环

content() 的“写”侧镜像方法是 Frame.setContent()(以及 SetContentWaitForOptions 用于控制等待条件与超时),其签名见 官方文档

abstract setContent(
  html: string,
  options?: SetContentWaitForOptions,
): Promise<void>;

内部负责真正写入的辅助方法 setFrameContent() 同样在 Frame.ts 中,它通过 document.open()document.write(html)document.close() 在页面上下文写入 HTML。读写二者配合,可以实现“内存中构造 HTML → 写入页面 → 再读回验证”的闭环,广泛用于测试 mock 页面、PDF 生成前的内容准备等场景。

仓库中的测试 test/src/page.test.ts 对“写入后读回”做了精确断言,恰好验证了 content() 保留 DOCTYPE 的行为:

describe('Page.setContent', function () {
  const expectedOutput =
    '<html><head></head><body><div>hello</div></body></html>';

  it('should work', async () => {
    const {page} = await getTestState();
    await page.setContent(htmlRaw`<div>hello</div>`);
    const result = await page.content();
    expect(result).toBe(expectedOutput);
  });

  it('should work with doctype', async () => {
    const {page} = await getTestState();
    const doctype = '<!DOCTYPE html>';
    await page.setContent(htmlRaw`${doctype}<div>hello</div>`);
    const result = await page.content();
    expect(result).toBe(`${doctype}${expectedOutput}`);
  });
});

这两条用例表明一个可复现的行为契约:无 DOCTYPE 时 content() 返回标准化的 <html><head></head><body>...</body></html> 骨架;写入 <!DOCTYPE html> 后读回结果中 DOCTYPE 被原样保留在最前。

多 Frame 场景:读取 iframe 子 frame 的 HTML

当页面内嵌 iframe 时,需要先用 page.frames()(见 Page.frames 相关说明)拿到子 frame 对象,再调用 frame.content()。以仓库中的网络拦截测试 test/src/cdp/network_restrictions.test.ts 为参考,典型的取子 frame 并检查内容的代码模式如下:

const {page, server} = state;
await page.goto(server.PREFIX + '/empty.html');
await page.setContent(html`
  <iframe src="${server.PREFIX}/title.html"></iframe>
`);
const frame = page.frames().find(f => {
  return f !== page.mainFrame();
})!;

const content = await frame.content();
expect(content).not.toContain("Hi, I'm frame");

该模式在生产代码中同样通用:

// 1. 定位子 frame(示例按 URL 前缀过滤,可按需改用 name/id 等条件)
const childFrame = page.frames().find(f => f.url().includes('/iframe-page'));

// 2. 读取子 frame 的完整 HTML
if (childFrame) {
  const html = await childFrame.content();
  // 对 html 做解析、断言或提取
}

测试中还出现了对“跨进程 iframe(OOPIF)”与“iframe 内容被 blocklist/allowlist 拦截”时调用 frame.content() 校验内容的用例(见 network_restrictions.test.ts),这说明 content() 同样适用于 OOPIF 等复杂 frame 场景——只要 frame 本身仍处于 attach 状态,就能读到其当前文档的序列化结果。

注意事项与使用边界

综合文档、源码与测试,使用 content() 时有以下几点值得留意:

  1. 读的是快照不是流:结果是调用瞬间的完整 HTML 字符串,后续 DOM 变更不会反映到已取得的字符串上,需要最新状态就重新调用。
  2. 已分离 frame 会抛错@throwIfDetached 使 content() 在 detached frame 上抛错,而非返回空串;配合 frame.detached 属性或 page.waitForSelector/导航等待可以规避时序问题。若 frame 对应文档还在加载中,建议先等待合适的导航/加载事件再读取(可参考 goto 与相关等待方法)。
  3. 子 frame 必须显式指定page.content() 只序列化主 frame,若要读 iframe/OOPIF 内容必须使用 frame.content()
  4. 字符串体积:序列化是对全树的一次性遍历与拼接,超大页面会产生较大的字符串对象,连续高频调用可能带来 GC 压力。
  5. 规范化输出:从测试结果可见,读回的内容是浏览器序列化后的规范化 HTML(补齐 <html><head> 等结构),并非原始写入字符串的逐字复刻;做精确比对时务必基于规范化后的形态。

小结

Frame.content() 是 Puppeteer 中读取页面 HTML 的规范入口:它通过 evaluate 在页面上下文遍历 document.childNodes,以 outerHTML + XMLSerializer 组合序列化出包含 DOCTYPE 的完整 HTML,并通过 @throwIfDetached 规避失效 frame 的误用。掌握它与 page.content() 的转发关系、与 setContent() 的读写闭环,以及子 frame 场景下的取用方式,即可在自动化测试、抓取与内容校验等任务中稳定地获取和验证页面内容。上述结论均有官方 API 文档Frame 实现源码 佐证,读者也可进一步阅读 Frame 类总览setContent 官方文档 获取相邻 API 的完整语义。

登录后查看全文
热门项目推荐
相关项目推荐