Playwright BrowserContext 隔离边界:什么共享什么不共享

2026-08-18

写用例的人迟早会撞上同一个问题:我在上一个用例里登录了、授权了定位、往 localStorage 塞了一个 flag,下一个用例里这些东西还在不在?如果在,是哪一层把它留下来的;如果不在,我又该在哪一层把它塞回去?

Playwright 仓库对这件事的答案集中在一个类上。docs/src/api/class-browser.mdBrowser.newContext 的说明只有一句话,但这句话基本定义了整篇文章的范围:

Creates a new browser context. It won’t share cookies/cache with other browser contexts.

cookie 和 cache 都不跨 context 共享。剩下的问题是:除了这两样,还有什么挂在 context 上,以及有没有东西是它管不到的。下面按 docs/src/api/class-browsercontext.mddocs/src/browser-contexts.md 的原文逐层拆。

三层结构:browser / context / page

docs/src/browser-contexts.md 把 BrowserContext 描述成「incognito-like profiles」——类似无痕窗口的独立配置档。同一个 browser 进程里可以并存多个这样的档,彼此完全隔离。docs/src/api/class-browsercontext.md 开篇补了一句更硬的:Playwright 用 Browser.newContext 创建的是非持久化 context,这类 context 不往磁盘写任何浏览数据

创建的写法在共用页里各语言绑定都给了,JS 与 Python 同步版分别是这样(原样抄自 docs/src/browser-contexts.md):

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

用 Test Runner 时你不需要自己写这几行:docs/src/browser-contexts.md 写明 runner 会为每个 test 建一个新 context,并在里面提供一个默认的 page,test 的入参 { page, context } 就是这一对。所以「用例之间自动干净」这件事,实现机制就是每个 test 换一个 context。

cookie:context 级,而且能按维度部分清

cookie 是最典型的 context 级状态。BrowserContext.addCookies 的文档写得很直白:加进去之后,这个 context 里所有页面都会带上这些 cookie。读回来用 BrowserContext.cookies,不传参返回全部,传了 urls 就只返回影响这些 URL 的那些。

cookie 对象的字段在 addCookies.cookies 的参数表里列全了:namevalue,以及 url 或者 domain + path 二选一必填,另有 expires(Unix 秒)、httpOnlysecuresameSite,还有一个 partitionKey——文档标明它是给分区第三方 cookie(CHIPS)用的分区键。domain 前面加一个点表示对子域名也生效,这条也是文档原话。

清除这一侧值得单说。BrowserContext.clearCookies 早就有,但它的三个过滤选项 name / domain / path 是自 v1.43 起可用的(这是文档里逐条标注的 since 版本)。Python 同步绑定的用法照抄 docs/src/api/class-browsercontext.md

context.clear_cookies()
context.clear_cookies(name="session-id")
context.clear_cookies(domain="my-origin.com")
context.clear_cookies(path="/api/v1")
context.clear_cookies(name="session-id", domain="my-origin.com")

也就是说,你不必为了「登出」而丢掉整个 context——可以只把会话 cookie 摘掉,保留其它状态。JS 侧的示例里 domain 还可以传正则。

存储:storageState 覆盖到哪,就到哪

真正容易踩的是存储。BrowserContext.storageState 的返回结构在文档里写死了两块:cookies 数组,和 origins 数组——后者每项是一个 origin 加上该源下的 localStorage 键值对。方法说明这一句要连着读:它返回的是当前 cookie、local storage 快照、IndexedDB 快照与虚拟 WebAuthn 凭据。

但后两样默认不在里面,各有开关:

选项语义起始版本
indexedDB设为 true 才把 IndexedDB 纳入快照v1.51
credentials设为 true 才把 context 的虚拟 WebAuthn 凭据(passkey)纳入快照v1.61

文档给 indexedDB 的场景说明是:如果你的应用把认证令牌存在 IndexedDB 里(举的例子是 Firebase Authentication),就要打开它。credentials 那条附了一个必须知道的副作用——抓下来的凭据带私钥,可以通过 Browser.newContextstorageState 选项或 BrowserContext.setStorageState 重新种回新 context;而恢复含凭据的存储状态会自动安装虚拟 WebAuthn authenticator,并使该 context 内所有真实 authenticator 失效。这不是可选行为,是文档明写的连带结果。

反向写入是 BrowserContext.setStorageState(自 v1.59 起可用),它的语义是先清空既有 cookie、local storage、IndexedDB 条目与虚拟 WebAuthn 凭据,再写入新状态:

// Load storage state from a file and apply it to the context.
await context.setStorageState('state.json');

sessionStorage 不在这套体系里。 docs/src/auth.md 讲得很清楚:复用认证状态覆盖的是 cookie、local storage、IndexedDB 与 passkey(WebAuthn)这几类,而 session storage 是按域名隔离且不跨页面加载保留的,Playwright 没有提供持久化 session storage 的 API,文档给的是一段用 page.evaluate 自己存取的代码片段。所以如果你的登录态藏在 sessionStorage 里,storageState 那条路走不通,得自己搬。

权限:context 级 override,且文档自带一个警告

权限也挂在 context 上。BrowserContext.grantPermissions 授予权限,可选的 origin 参数把授予范围限定到某个源;BrowserContext.clearPermissions 的说明是「清除该 browser context 的所有权限 override」——注意是全清,没有单条撤销的参数。建 context 时也可以直接用 permissions 选项一次性给一组,docs/src/api/params.md 里这个选项的默认值写的是不授予任何权限(这是仓库当前文档里的默认值,随版本可能变动)。

grantPermissions 的参数说明里带了一个 danger 级别的提示,必须照实转述:各浏览器支持的权限并不相同,同一浏览器的不同版本之间也可能不同,任何一项权限都可能在浏览器更新之后不再工作。 文档随后给的那一串权限名(geolocationclipboard-readnotifications 等)措辞是「有些浏览器可能支持」,不是保证清单。把它当成硬约定去写断言,是会被浏览器更新掀翻的。

缓存与网络:归属比想象中散

  • HTTP 缓存:跟 cookie 一样不跨 context 共享,依据就是 Browser.newContext 那句 “won’t share cookies/cache”。
  • 一旦开路由,缓存就没了BrowserContext.route 的说明里有一条 note——「Enabling routing disables http cache」。做网络拦截的用例和依赖缓存行为的用例不能混在一个 context 里,这是文档给的机制,不是经验之谈。
  • route 的层级关系:context 级 route 拦的是这个 context 里任何页面发出的匹配请求;而 Page.route 与 context route 同时匹配时,文档写明 page route 优先。
  • Service Worker 是个洞:同一段 note 说明 BrowserContext.route 不会拦截被 Service Worker 拦下的请求,并建议做请求拦截时把 serviceWorkers 选项设为 'block'。该选项的默认值文档写的是 'allow'(同样以仓库最新内容为准)。另外 BrowserContext.serviceWorkers 与对应事件都标了 langs: js, python,且只在基于 Chromium 的浏览器上支持。
  • 代理docs/src/network.md 同时给了两种写法——在 launch 时设,和在 newContext 时按 context 设。也就是说代理这一项 browser 级和 context 级都能落。

顺带一提,BrowserContext.backgroundPagesbackgroundPage 事件已标 deprecated,文档说明是背景页随 Manifest V2 扩展一起从 Chromium 移除,前者现在返回空列表、后者不再触发。别再拿它们写新逻辑。

哪些不归 context 管

两个方向容易搞混。

往上launchPersistentContext 是另一套。它用 userDataDir 指向的目录做持久化存储,返回的是唯一的那个 context,关掉它浏览器也跟着关。docs/src/api/class-browsertype.md 里两条限制要记住:浏览器不允许用同一个 User Data Directory 同时启动多个实例;另有一条 warning 明说,由于 Chrome 的策略变化,不支持自动化 Chrome 的默认用户配置,把 userDataDir 指向你日常浏览用的 “User Data” 目录可能导致页面加载不出来或浏览器退出,应当另建一个空目录当自动化 profile。Windows 上就是 <你的项目目录>\.pw-profile 这类路径,Linux/macOS 上是 <你的项目目录>/.pw-profile——形式不同,「一个目录只能被一个实例占用」这条约束是一样的,并行跑用例时它会直接变成硬冲突。

往下:有些东西 context 给默认值、page 可以单独覆盖。Page.setViewportSize 的文档写明,同一个浏览器里多个 page 可以各有各的视口尺寸,而 Browser.newContext 是一次性给这个 context 里所有 page 设定视口(以及更多项)。还有一个反直觉的方向——弹窗不独立class-browsercontext.md 开头就说,如果一个页面用 window.open 打开另一个页面,弹出的那个 page 属于父 page 所在的 browser context。你以为开了新窗口就换了身份,其实 cookie 和存储都还是那一份。

BrowserContext.addInitScript 的作用域也值得对照着看:文档列的触发时机是 context 里任何页面被创建或导航时,以及任何页面里的子 frame 被附加或导航时,脚本在文档创建之后、页面自身脚本运行之前求值。这是 context 级注入,不是 page 级。

多开 context 还是多开 browser

docs/src/browser-contexts.md 自述 context「创建起来 fast and cheap」,并且即使跑在同一个浏览器里也是完全隔离的——这是仓库文档的说法,我们没有对此做过任何测量,这里只转述。它给的多用户场景写法是在一个 browser 上开两个 context:

test('admin and user', async ({ browser }) => {
  // Create two isolated browser contexts
  const adminContext = await browser.newContext();
  const userContext = await browser.newContext();

  // Create pages and interact with contexts independently
  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();
});

而 browser 这一层的粒度在哪,docs/src/test-parallel-js.md 讲并行时说得很明确:所有 test 跑在 worker 进程里,这些是独立的操作系统进程,每个 worker 各自启动自己的浏览器。所以一次运行里 browser 实例的数量由 worker 数决定,context 的数量由 test 数决定,两者不是一个量级的东西。同一篇还有一句可以直接拿来对照:worker 进程各有自己隔离的 BrowserContext,所以 cookie、存储和内存里的全局变量本来就是隔离的,并行下的不稳定几乎都来自单个 test 之外的状态(后端数据、共享文件路径这些)。

据此可以给一条只在文档写明范围内成立的判断:需要换掉的东西如果是 cookie / 存储 / 权限 / 视口 / 代理 / UA 这一类,换 context 就够;需要换掉的是浏览器内核本身、启动参数,或者要用持久化 profile,那才必须动到 browser 这一层。

怎么确认自己判断对了

三个都在 API 文档里能查到的自检动作:Browser.contexts 返回当前打开的所有 context(新建的浏览器上返回零个);BrowserContext.cookies 不传参就是这个 context 的全部 cookie;BrowserContext.storageState 打出来的 JSON 直接告诉你哪些 origin 下留了什么 localStorage、以及你开没开 IndexedDB 那个开关。想确认 context 是不是已经关掉,还有 BrowserContext.isClosed(自 v1.59 起可用)。

以上代码片段除注明外均原样取自仓库文档,为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。

最后提醒一句本来就该说的:Playwright 会启动真实浏览器并在真实页面里执行脚本,context 隔离解决的是「用例之间不互相污染」,它不是安全边界——授予了 clipboard-readgeolocation 这类权限,页面就真的能读到对应的东西。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。