首页 / Playwright 入门教程 / 浏览器/上下文/页面 三层级模型

Playwright 入门教程

浏览器/上下文/页面 三层级模型

本教程共 59 篇 · 第 24 篇 · 更新于 2026-08-04 · 约 10 分钟阅读

PlaywrightBrowserContextPage测试隔离Browser架构模型

24. 浏览器/上下文/页面 三层级模型

本节目标:说清 Browser、BrowserContext、Page 三者的关系,理解测试隔离是怎么实现的。

前面二十多章,我们一直在用 page 这个东西。它从哪来?为什么每个测试拿到的 page 都是干净的?

答案在 Playwright 的三层模型里。这一章讲结构,理解了它,后面多标签、多用户、状态复用这些主题都会变得简单。

三层是什么

从大到小:

Browser(浏览器进程)
 └── BrowserContext(隔离环境,类似隐身窗口)
      └── Page(标签页)
           └── Frame(iframe)

Browser(浏览器)对应一个真实的浏览器进程。启动它要几百毫秒到几秒,比较贵,所以得复用。

BrowserContext(浏览器上下文)是一个隔离的浏览器环境。把它理解成「一个全新的隐身窗口」就对了:独立的 cookie、独立的 localStorage、独立的缓存、独立的权限设置。创建它非常快,几乎没有开销。

Page(页面)就是一个标签页。它归属于某个 context,负责导航和页面交互。

Frame(框架)是页面里的 iframe,第 15 章讲过,这里不展开。

关键的一句话:贵的是 Browser,便宜的是 Context。 Playwright 的整个隔离策略就建立在这个事实上。

为什么需要隔离

先讲问题。假设你有两个测试:

  1. 测试 A:登录后修改用户昵称为「张三」。
  2. 测试 B:检查未登录用户看到「请先登录」。

如果两个测试共用一个浏览器环境,测试 B 会因为 A 留下的登录 cookie 而失败。更糟的是,这种失败取决于执行顺序——单独跑 B 是通过的,一起跑就挂。

这类问题的排查成本极高。因为报错出现在 B,根因却在 A。

隔离带来三个直接好处:

  1. 失败不会传染。 一个测试挂了,不影响其他测试。
  2. 好调试。 单独重跑某个测试,行为和整体跑时一致。
  3. 能放心并行。 顺序无关,随便打乱、分片。

两种隔离思路

业界有两条路。

一是「用完清理」:测试结束后删 cookie、清 localStorage、恢复数据。

这条路问题不小。清理容易漏,而且有些状态根本清不掉——比如「已访问链接」的样式、Service Worker 的注册、浏览器的自动填充记忆。漏掉的状态就会渗到下一个测试。

二是「从零开始」:每个测试拿一个全新环境。

新环境里什么都没有,测试失败时只用看这一个测试的代码。Playwright 走的就是这条路。

它敢这么选,前提是 BrowserContext 足够便宜。要是每个测试都得重启一次浏览器,这个方案早就被成本压死了。

Playwright 怎么做到的

用测试运行器时,这一切是自动的:

import { test } from '@playwright/test';

test('第一个测试', async ({ page, context }) => {
  // context 是专为这个测试创建的隔离环境
  // page 属于这个 context
});

test('第二个测试', async ({ page, context }) => {
  // 全新的 context 和 page
  // 和上一个测试完全不共享任何状态
});

每跑一个测试,运行器就新建一个 context,在里面开一个默认的 page,通过 Fixture(夹具)注入给你。测试一结束,context 关闭,里面的一切随之销毁。

Browser 实例是复用的。同一个 worker 进程里的多个测试共享一个浏览器,只换 context。既隔离,又不慢。

Note

这就是 page 为什么「拿来就能用」。你不用创建,也不用关闭,全由夹具托管。夹具机制在第 37 章细讲。

Context 隔离了哪些东西

具体来说,两个 context 之间不共享:

  • Cookie
  • localStorage / sessionStorage
  • IndexedDB / Cache Storage
  • HTTP 缓存
  • 权限授予(通知、地理位置等)
  • 已注册的 Service Worker
  • 浏览历史与已访问链接

也就是说,从页面的角度看,两个 context 就像两台不同的电脑。

共享的是浏览器进程级的东西:可执行文件、启动参数、扩展(如果加载了)。这些一般不影响测试。

库模式下手动创建

不用测试运行器、直接调 API 时,三层要自己搭:

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

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

// 用完自己关,顺序从内到外
await context.close();
await browser.close();

有个简写,browser.newPage() 会顺手建一个 context:

const page = await browser.newPage();  // 内部隐式创建了一个 context

省事,但拿不到 context 引用,权限、地理位置这类配置就没法做了。正式代码里我还是老实写三行。

Tip

关闭 context 会连带关闭它下面所有的 page。不用一个个关。同理,关 browser 会关掉所有 context。

一个测试里开多个 Context

有一类需求绕不过去:同时模拟两个用户。聊天、协同编辑、管理员操作影响普通用户,都属于这一类。

一个 context 只能是一种身份。所以要开两个:

import { test, expect } from '@playwright/test';

test('管理员禁言后用户不能发言', async ({ browser }) => {
  // 两个隔离的环境
  const adminContext = await browser.newContext();
  const userContext = await browser.newContext();

  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();

  // 各自登录、各自操作,互不干扰
  await adminPage.goto('https://example.com/admin');
  await userPage.goto('https://example.com/chat');

  await adminPage.getByRole('button', { name: '禁言' }).click();
  await expect(userPage.getByPlaceholder('说点什么')).toBeDisabled();

  await adminContext.close();
  await userContext.close();
});

注意这里注入的夹具是 browser 而不是 page。要手动建 context,就得从 browser 这一层拿起。

Warning

手动创建的 context 必须手动关闭。夹具只负责它自己创建的那个。忘了关会泄漏资源,测试多了内存就上去了。

Context 还能配什么

创建 context 时可以传一堆选项,这些都是「环境级」的设置:

const context = await browser.newContext({
  viewport: { width: 1280, height: 720 },
  locale: 'zh-CN',
  timezoneId: 'Asia/Shanghai',
  geolocation: { longitude: 120.15, latitude: 30.28 },
  permissions: ['geolocation'],
  colorScheme: 'dark',
  userAgent: '自定义 UA',
  offline: false,
  httpCredentials: { username: 'user', password: 'pass' },
  storageState: 'auth.json',  // 复用登录态
});

这些选项在第 27、28 章会逐个展开,storageState 在第 33 章讲。

用测试运行器时,同样的东西写在配置的 use 里,或者在测试文件里用 test.use()

import { test } from '@playwright/test';

test.use({
  viewport: { width: 1600, height: 1200 },
  colorScheme: 'dark',
});

test('暗色模式下的首页', async ({ page }) => {
  // page 所在的 context 已经应用了上面的设置
});

一个 Context 里的多个 Page

同一个 context 下可以开多个 page,它们共享状态:

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

// page1 登录后,page2 也是登录状态,两者共用同一套 cookie
await page1.goto('https://example.com/login');
await page2.goto('https://example.com/profile');

这和真实浏览器里开两个标签页是一回事。想模拟「同一个用户开了两个标签页」,就这么写;想模拟「两个不同用户」,必须用两个 context。

这条区分很关键,实际写代码时想清楚再动手。

常见误区

误区一:一个测试文件共用一个 page。

有人为了省事,在 beforeAll 里建一个 page 给所有测试用。这等于放弃了隔离,回到了「用完清理」的老路。除非有极强的性能理由,否则别这么做。

误区二:以为 context 很贵。

不贵。创建一个 context 是毫秒级的,和启动浏览器完全不是一个量级。放心用。

误区三:把 browser 和 context 搞混。

browser.newPage() 每次都会隐式创建新 context,所以两次 newPage() 拿到的页面是互相隔离的,不是两个标签页。想要真正的标签页关系,必须 context.newPage()

小结

  • 三层结构:Browser(进程)→ BrowserContext(隔离环境)→ Page(标签页)。
  • Browser 贵要复用,Context 便宜可以随便建,这是隔离策略的基础。
  • 测试运行器为每个测试自动建一个 context,测试结束自动销毁。
  • Context 隔离 cookie、存储、缓存、权限等一切页面可见状态。
  • 同 context 的多个 page 共享状态;不同 context 完全独立。
  • 模拟多用户就开多个 context,记得手动关闭。
  • 环境级配置(视口、语言、权限、时区)都挂在 context 上。

下一章预告:多页面与多标签处理——同一个 context 里开出来的标签页怎么拿到、怎么切换、怎么管好。