2. 浏览器启动与页面导航

2.浏览器启动与页面导航

启动浏览器是与 Puppeteer 打交道的第一步。这个过程看似简单,背后却涉及浏览器进程管理、协议通信和资源分配等一系列复杂操作。好在 Puppeteer 把这些细节都封装了起来,我们只需要几行代码就能完成浏览器的启动和关闭。

浏览器实例生命周期管理

浏览器实例的生命周期从 puppeteer.launch() 开始,到 browser.close() 结束。这中间包含了页面创建、脚本执行、数据提取等一系列操作。理解这个生命周期对编写稳定的自动化脚本至关重要。

启动浏览器

最基础的启动方式只需要一行代码:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();

这行代码会下载特定版本的 Chromium(如果还没下载的话),启动浏览器进程,并返回一个 Browser 实例。默认情况下,浏览器以无头模式运行,这意味着不会显示图形界面,所有操作都在后台完成。

实际项目中,我们通常需要更多控制。launch 方法接受一个配置对象,可以指定各种启动参数:

const browser = await puppeteer.launch({
  headless: false, // 显示浏览器窗口,方便调试
  slowMo: 50,      // 每个操作延迟50毫秒,让自动化过程肉眼可见
  args: [
    '--start-maximized',          // 最大化窗口
    '--disable-notifications',    // 禁用通知
    '--disable-geolocation',      // 禁用地理位置
  ]
});

args 数组传递的是 Chromium 命令行参数,这些参数直接控制浏览器行为。比如 --start-maximized 让浏览器启动时最大化,--disable-notifications 屏蔽掉那些烦人的通知弹窗。完整的参数列表可以在 Chromium 官方文档中找到,不过日常开发中常用的也就十几个。

关闭浏览器

使用完浏览器后,必须调用 browser.close() 方法关闭实例。这不仅仅是礼貌问题,更是资源管理的关键。不关闭浏览器会导致进程残留,占用内存和端口,长期积累会让系统变得缓慢。

await browser.close();

关闭操作会终止浏览器进程,释放所有相关资源。如果脚本异常退出,浏览器可能无法正常关闭。这时候需要在代码中添加异常处理:

import puppeteer from 'puppeteer';

let browser;
try {
  browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  // ... 执行其他操作
} catch (error) {
  console.error('自动化任务失败:', error);
} finally {
  if (browser) {
    await browser.close();
  }
}

这个 try...finally 结构确保即使发生错误,浏览器也能被正确关闭。finally 块中的 if (browser) 检查很有必要,因为如果 launch() 本身失败,browser 变量会是 undefined,直接调用 close() 会抛出新的错误。

浏览器上下文与多页面隔离

浏览器上下文是 Puppeteer 提供的一个强大功能,用于实现会话隔离。每个上下文都有独立的 Cookie、本地存储和会话存储,就像浏览器的"隐身模式"窗口一样。

创建独立上下文

默认情况下,所有页面都在同一个浏览器上下文中运行。如果需要隔离不同任务,可以创建新的上下文:

const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();

const page1 = await context.newPage();
const page2 = await context.newPage();

await context.close();

这段代码创建了一个新上下文,并在其中打开两个页面。这两个页面共享同一个会话,但与其他上下文完全隔离。调用 context.close() 会关闭该上下文中的所有页面,这是一个批量清理的便捷方式。

上下文隔离的实际应用场景很多。比如同时爬取多个网站,每个网站使用独立的上下文可以避免 Cookie 冲突。或者测试多用户场景,为每个用户创建独立上下文,模拟真实的多浏览器环境。

默认上下文与权限管理

浏览器启动后会自动创建一个默认上下文,可以通过 browser.defaultBrowserContext() 获取:

const context = browser.defaultBrowserContext();

默认上下文通常用于设置全局权限。比如允许某个网站访问地理位置:

await context.overridePermissions('https://html5demos.com', ['geolocation']);

overridePermissions 方法接受两个参数:目标网址和权限列表。权限可以是 geolocationnotificationscameramicrophone 等。这在测试需要特定权限的 Web 应用时非常有用,避免了手动点击授权弹窗的麻烦。

需要注意的是,权限设置是针对上下文的,不是全局的。如果在新创建的上下文中访问需要权限的网站,需要重新设置权限。

多页面管理

一个浏览器实例可以创建多个页面,每个页面都是独立的标签页:

const page1 = await browser.newPage();
const page2 = await browser.newPage();

await page1.goto('https://example.com');
await page2.goto('https://another.com');

console.log(await browser.pages()); // 获取所有页面

browser.pages() 返回当前所有打开页面的数组,包括所有上下文中的页面。这个方法在需要遍历所有页面或检查特定页面是否存在时很有用。

页面创建后,可以像操作普通浏览器标签一样操作它。每个页面都有自己的导航历史、Cookie 和会话状态。关闭单个页面使用 page.close()

await page1.close();

关闭页面会释放该页面占用的内存,但浏览器实例仍然保持运行。如果所有页面都关闭,浏览器会保持空壳状态,直到调用 browser.close()

页面导航策略与事件监听

页面导航是自动化测试和爬虫中最常见的操作。Puppeteer 对导航的定义很宽泛:任何改变页面 URL 的行为都算导航,包括普通的链接跳转、锚点导航、History API 操作等。这个设计让 Puppeteer 能很好地处理单页应用。

基本导航与等待策略

page.goto() 是最常用的导航方法:

await page.goto('https://example.com');

这行代码看似简单,背后却包含了复杂的等待逻辑。默认情况下,goto 会等待到 load 事件触发,也就是页面所有资源(图片、样式、脚本)都加载完毕。但有时候我们不需要等这么久,可以通过 waitUntil 参数调整等待策略:

// 只等待 DOMContentLoaded 事件
await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded'
});

// 等待网络空闲(500ms内没有新的网络请求)
await page.goto('https://example.com', {
  waitUntil: 'networkidle0'
});

// 等待网络基本空闲(500ms内不超过2个请求)
await page.goto('https://example.com', {
  waitUntil: 'networkidle2'
});

domcontentloaded 是最快的选项,适合只需要操作 DOM 的场景。networkidle0networkidle2 适合需要等待异步数据加载完成的场景,比如 Ajax 请求。选择哪种策略取决于具体需求,没有绝对的好坏。

导航事件监听

Puppeteer 提供了多个导航相关的事件,可以在页面生命周期不同阶段执行操作:

page.on('load', () => {
  console.log('页面加载完成');
});

page.on('domcontentloaded', () => {
  console.log('DOM 解析完成');
});

page.on('framenavigated', (frame) => {
  console.log('框架导航:', frame.url());
});

framenavigated 事件在页面或任何 iframe 导航时触发,参数是导航的框架对象。这在处理嵌套页面或跟踪导航历史时很有用。

单页应用导航处理

现代单页应用使用 History API 改变 URL 而不重新加载页面。Puppeteer 能自动识别这种导航:

// 点击导航链接
await page.click('a[href="/about"]');

// 等待 URL 变化
await page.waitForFunction(
  () => window.location.pathname === '/about'
);

// 或者等待特定元素出现
await page.waitForSelector('.about-page-content');

对于 SPA,通常不需要重新调用 goto(),而是通过点击按钮或链接触发路由变化,然后等待新内容加载。waitForSelectorwaitForFunction 是处理 SPA 导航的可靠方式。

前进、后退与重载

浏览器的前进后退功能在 Puppeteer 中同样支持:

await page.goto('https://example.com/page1');
await page.goto('https://example.com/page2');

await page.goBack();    // 后退到 page1
await page.goForward(); // 前进到 page2

goBackgoForward 返回一个布尔值,表示操作是否成功。如果导航历史中没有上一页或下一页,会返回 false

页面重载也很简单:

await page.reload();

reload 方法接受可选参数,可以控制是否忽略缓存:

await page.reload({ ignoreCache: true });

忽略缓存的重载相当于按下 Ctrl+F5,会强制从服务器重新获取所有资源。

连接远程调试端口

有时候我们需要连接已经运行的浏览器,而不是启动新实例。比如调试正在开发的网页,或者利用已登录的浏览器会话。Puppeteer 提供了 connect 方法实现这个功能。

获取 WebSocket 地址

首先需要启动一个支持远程调试的浏览器:

chrome --remote-debugging-port=9222 --user-data-dir=/path/to/profile

--remote-debugging-port 参数开启调试协议,--user-data-dir 指定用户数据目录,确保浏览器使用特定的配置和会话。

浏览器启动后,访问 http://localhost:9222/json/version 可以获取 WebSocket 调试地址:

{
  "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/..."
}

连接到运行中的浏览器

获取 WebSocket 地址后,使用 puppeteer.connect() 连接:

const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/...'
});

const pages = await browser.pages();
const page = pages[0]; // 使用第一个页面

await page.goto('https://example.com');

// 操作完成后断开连接
browser.disconnect();

connect 方法返回的 Browser 实例与 launch 创建的实例功能基本相同,但有一个关键区别:disconnect() 不会关闭浏览器,只是断开 Puppeteer 与浏览器的连接。浏览器会继续运行,所有页面保持打开状态。

这在调试场景下非常有用。可以在浏览器中手动操作到特定状态,然后用 Puppeteer 接管继续自动化。测试完成后断开连接,浏览器保持原状,方便下次使用。

连接与启动的选择

选择 launch 还是 connect 取决于具体场景。launch 适合独立的自动化任务,环境干净,可重复性强。connect 适合需要与现有浏览器交互的场景,比如调试、利用已有会话等。

需要注意的是,连接的浏览器版本必须与 Puppeteer 版本兼容。Puppeteer 每个版本都绑定特定版本的 Chromium,如果连接不同版本的 Chrome,可能会遇到协议不兼容的问题。

协议支持与跨浏览器自动化

从 Puppeteer v23.0.0 开始,项目正式支持 WebDriver BiDi 协议,这是 W3C 制定的跨浏览器自动化标准。这意味着 Puppeteer 不再局限于 Chrome,也能自动化 Firefox。

WebDriver BiDi 与 CDP 的选择

默认情况下,Puppeteer 使用 Chrome DevTools Protocol(CDP)自动化 Chrome,使用 WebDriver BiDi 自动化 Firefox。对于 Chrome,也可以显式指定使用 WebDriver BiDi:

const browser = await puppeteer.launch({
  browser: 'chrome',
  protocol: 'webDriverBiDi'
});

WebDriver BiDi 是未来的方向,它提供了跨浏览器的一致性。但目前 CDP 在 Chrome 上功能更完整,比如网络拦截、性能分析等高级功能在 WebDriver BiDi 上还不支持。

根据官方文档,WebDriver BiDi 目前不支持的功能包括:各种模拟(CPU 节流、媒体特性、视觉缺陷等)、CDP 特定功能(如 createCDPSession)、无障碍、覆盖率、追踪等。但基础的浏览器自动化、页面操作、脚本执行、选择器定位等功能已经完全支持。

跨浏览器测试的注意事项

编写跨浏览器自动化脚本时,应尽量避免使用协议特定的功能。如果必须使用,需要做好错误处理:

try {
  await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]);
} catch (error) {
  if (error instanceof UnsupportedOperation) {
    console.log('当前协议不支持媒体特性模拟');
  } else {
    throw error;
  }
}

UnsupportedOperation 错误表示当前协议不支持该操作。通过捕获这个错误,可以让脚本在不同浏览器上优雅降级。

在特殊环境中运行 Puppeteer

Puppeteer 不仅能在 Node.js 环境中运行,还能在浏览器和 Chrome 扩展中运行。这些场景下,Puppeteer 的功能会受到一定限制,但提供了新的可能性。

在浏览器中运行

通过打包工具,可以把 Puppeteer 核心库编译成浏览器可用的版本:

import puppeteer from 'puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js';

const browser = await puppeteer.connect({
  browserWSEndpoint: wsUrl // 需要提供远程浏览器的 WebSocket 地址
});

const pages = await browser.pages();
console.log(`浏览器有 ${pages.length} 个页面`);

browser.disconnect();

这种方式下,Puppeteer 无法直接启动浏览器,只能连接已运行的浏览器实例。所有操作都通过 WebSocket 发送到远程浏览器执行。这在需要在网页界面中控制其他浏览器的场景下很有用,比如在线调试工具。

在 Chrome 扩展中运行

Chrome 扩展可以通过 chrome.debugger API 访问 CDP,Puppeteer 利用这一点实现了在扩展中运行:

import { connect, ExtensionTransport } from 'puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js';

// 创建或获取标签页
const tab = await chrome.tabs.create({ url: 'https://example.com' });

// 使用扩展传输层连接
const browser = await connect({
  transport: await ExtensionTransport.connectTab(tab.id)
});

// 扩展中只能操作单个页面
const [page] = await browser.pages();

console.log(await page.evaluate('document.title'));

browser.disconnect();

在扩展中运行时,Puppeteer 的视图被限制在单个标签页,无法创建新页面。这是因为 chrome.debugger API 一次只能附加到一个标签页。如果需要操作多个标签页,必须为每个标签页创建独立的连接。

这些特殊运行环境展示了 Puppeteer 的灵活性,但也提醒我们:核心功能始终围绕浏览器自动化展开,不同环境只是连接方式的变化。

浏览器启动与页面导航是 Puppeteer 自动化的基础。掌握了浏览器的生命周期管理、上下文隔离、导航策略和远程连接,就能构建出稳定可靠的自动化脚本。下一章将深入元素定位与交互,这是自动化操作的核心技能。