浏览器/上下文/页面 三层级模型
本教程共 59 篇 · 第 24 篇 · 更新于 2026-08-04 · 约 10 分钟阅读
本节目标:说清 Browser、BrowserContext、Page 三者的关系,理解测试隔离是怎么实现的。
前面二十多章,我们一直在用 page 这个东西。它从哪来?为什么每个测试拿到的 page 都是干净的?
答案在 Playwright 的三层模型里。这一章讲结构,理解了它,后面多标签、多用户、状态复用这些主题都会变得简单。
三层是什么
从大到小:
Browser(浏览器进程)
└── BrowserContext(隔离环境,类似隐身窗口)
└── Page(标签页)
└── Frame(iframe)
Browser(浏览器)对应一个真实的浏览器进程。启动它要几百毫秒到几秒,比较贵,所以得复用。
BrowserContext(浏览器上下文)是一个隔离的浏览器环境。把它理解成「一个全新的隐身窗口」就对了:独立的 cookie、独立的 localStorage、独立的缓存、独立的权限设置。创建它非常快,几乎没有开销。
Page(页面)就是一个标签页。它归属于某个 context,负责导航和页面交互。
Frame(框架)是页面里的 iframe,第 15 章讲过,这里不展开。
关键的一句话:贵的是 Browser,便宜的是 Context。 Playwright 的整个隔离策略就建立在这个事实上。
为什么需要隔离
先讲问题。假设你有两个测试:
- 测试 A:登录后修改用户昵称为「张三」。
- 测试 B:检查未登录用户看到「请先登录」。
如果两个测试共用一个浏览器环境,测试 B 会因为 A 留下的登录 cookie 而失败。更糟的是,这种失败取决于执行顺序——单独跑 B 是通过的,一起跑就挂。
这类问题的排查成本极高。因为报错出现在 B,根因却在 A。
隔离带来三个直接好处:
- 失败不会传染。 一个测试挂了,不影响其他测试。
- 好调试。 单独重跑某个测试,行为和整体跑时一致。
- 能放心并行。 顺序无关,随便打乱、分片。
两种隔离思路
业界有两条路。
一是「用完清理」:测试结束后删 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 里开出来的标签页怎么拿到、怎么切换、怎么管好。