Playwright Service Workers 实战指南:禁用策略、激活等待与 Worker 请求路由
本篇技术指南基于 Playwright 官方文档 Service Workers 展开,系统讲解 Playwright 对 Service Worker 的完整支持体系:通过 serviceWorkers 上下文选项控制注册策略、使用 BrowserContext 事件捕获并等待 Service Worker 激活、区分页面请求与 Worker 请求的网络事件模型,以及仅针对 Service Worker 发起的请求进行路由拦截。读完后,你可以针对依赖 Service Worker 缓存的应用编写确定性的自动化测试,并精确模拟离线与响应改写场景。
什么是 Service Workers,Playwright 的支持边界
Service Workers 是浏览器原生的请求拦截机制:页面通过原生 Fetch API(fetch)以及脚本、CSS、图片等网络资源发起的请求,都可以被 Service Worker 代理处理。它可以在页面与外部网络之间充当网络代理,执行缓存逻辑;只要 Service Worker 注册了 FetchEvent 监听器,还能为用户提供离线体验。
大量站点把 Service Worker 仅当作透明的性能优化手段:用户感知到页面变快了,但应用实现本身对它的存在无感知,运行在开启或关闭 Service Worker 的环境里表现功能等价。
Playwright 对 Service Worker 的支持有两个重要前提,官方文档中均有明确标注:
- 仅 Chromium 系浏览器支持:Service Worker 相关 API 只在 Chromium-based 浏览器上可用,Firefox 与 WebKit 不在支持范围内。这一点从源码结构也能印证:Service Worker 的服务端实现集中在 crServiceWorker.ts,是 Chromium 协议(CDP)专属的
CRServiceWorker类,而 WebKit 与 Firefox 的驱动目录中不存在对应实现。 - 普通网络 Mock 不需要本文方案:如果只是想做通用的请求拦截、路由与 Mock,应优先使用 Playwright 内置的
page.route/context.route能力(参见 网络指南),那些 API 不需要理解 Service Worker 的任何细节。只有当测试目标本身涉及 Service Worker 发起的请求(例如验证缓存命中、改写 Worker 的响应)时,才需要本文介绍的事件模型与路由技巧。
禁用 Service Workers:让测试更确定
测试环境中,Service Worker 带来的缓存与异步注册会让断言变得不稳定且拖慢执行速度。Playwright 允许在测试期间直接禁用 Service Worker,官方建议的做法是将 serviceWorkers 选项设为 'block'。
JavaScript / TypeScript 写法
在 playwright.config.ts 中通过 use 设置:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
serviceWorkers: 'block'
},
});
Python 写法
在 conftest.py 中通过 browser_context_args fixture 注入上下文选项:
import pytest
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {
**browser_context_args,
"service_workers": "block"
}
该选项的合法取值为 'allow'(默认)与 'block' 两种,类型定义见 types.d.ts。需要留意官方文档同时给出的警示:如果你的被测页面实际依赖 Service Worker,禁用后的行为可能与真实环境不同——因此"默认禁用换确定性,按需放行保真实"是两条并行的策略,应按测试目标选择。
底层实现:一条注入脚本完成屏蔽
禁用逻辑的实现非常轻量。从源码看,当上下文创建时检测到 serviceWorkers === 'block',Playwright 会向该上下文注入一段初始化脚本,把 navigator.serviceWorker.register 替换为空实现并打印一条警告:
// packages/playwright-core/src/server/browserContext.ts (L175-176)
if (this._options.serviceWorkers === 'block')
await this.addInitScript(nullProgress,
`if (navigator.serviceWorker) navigator.serviceWorker.register = async () => {
console.warn('Service Worker registration blocked by Playwright');
};`);
也就是说,被屏蔽的不是浏览器能力本身,而是应用侧的注册入口——任何 navigator.serviceWorker.register(...) 调用都会静默失败。对应测试 browsercontext-service-worker-policy.spec.ts 验证了完整行为链:默认策略下 registrationPromise 能成功 resolve;而 serviceWorkers: 'block' 时,页面 console 会捕获到 Service Worker registration blocked by Playwright 这条警告。该文件同时覆盖了 about:blank 页面上不抛错的边界情况。
获取 Service Worker 实例并等待激活
当测试需要与 Service Worker 交互时(读取缓存、注入逻辑、验证行为),首先要拿到它的句柄。Playwright 通过 BrowserContext 提供两类入口:
context.serviceWorkers:列出上下文中当前所有 Service Worker 实例(返回Array<Worker>,类型定义见 types.d.ts);context.waitForEvent('serviceworker')/expect_event("serviceworker"):在预期页面将触发注册时,先挂起等待,注册完成后拿到实例。
JavaScript 写法
const serviceWorkerPromise = context.waitForEvent('serviceworker');
await page.goto('/example-with-a-service-worker.html');
const serviceworker = await serviceWorkerPromise;
Python 写法(同步与异步)
with context.expect_event("serviceworker") as worker_info:
page.goto("/example-with-a-service-worker.html")
service_worker = worker_info.value
async with context.expect_event("serviceworker") as worker_info:
await page.goto("/example-with-a-service-worker.html")
service_worker = await worker_info.value
拿到实例后即可使用 Worker 对象的标准能力:url() 返回 Worker 脚本地址,evaluate() 在 Worker 上下文中执行表达式,waitForEvent() 等待其上的事件。这些方法都定义在客户端类 Worker 中,Service Worker 与 Web Worker 共享该基类,仅通过内部 _context 归属区分。
关键时序:注册事件早于激活,必须显式等待
一个容易踩坑的时序问题:serviceworker 事件在 Service Worker 接管页面之前就会触发,此时对 Worker 执行 evaluate 可能拿到尚未就绪的执行环境。官方文档给出了一段实现无关(implementation agnostic)的等待激活方案——在页面上下文中检查 navigator.serviceWorker 的注册状态,未激活则等待 controllerchange 事件:
await page.evaluate(async () => {
const registration = await window.navigator.serviceWorker.getRegistration();
if (registration.active?.state === 'activated')
return;
await new Promise(resolve => {
window.navigator.serviceWorker.addEventListener('controllerchange', resolve);
});
});
page.evaluate("""async () => {
const registration = await window.navigator.serviceWorker.getRegistration();
if (registration.active?.state === 'activated')
return;
await new Promise(resolve => {
window.navigator.serviceWorker.addEventListener('controllerchange', resolve);
});
}""")
await page.evaluate("""async () => {
const registration = await window.navigator.serviceWorker.getRegistration();
if (registration.active?.state === 'activated')
return;
await new Promise(resolve => {
window.navigator.serviceWorker.addEventListener('controllerchange', resolve);
});
}""")
这段脚本的逻辑是幂等的:已激活则直接返回,否则挂起直到浏览器把页面控制权交给 Worker。配合一个注册了 clients.claim() 的 Worker 脚本(即激活后立即接管页面),可以稳定复现"等待激活"路径。仓库中的测试资产 tests/assets/serviceworkers/fetch/sw.js 正是这种模式的最小实现:
self.addEventListener('fetch', event => {
self.intercepted.push(event.request.url)
event.respondWith(fetch(event.request));
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
网络事件模型:页面请求与 Worker 请求如何区分
这是整篇文档最核心的部分。Service Worker 引入后,一次页面资源获取可能产生两条独立的请求链路,Playwright 通过 BrowserContext 把它们全部上报,并提供清晰的归属标识。
事件归属规则
任何由 Service Worker 发起的网络请求都通过 BrowserContext 对象报告:
context.request、context.requestFinished、context.response、context.requestFailed事件正常触发;context.route能拦截到这些请求;- 对应的
Request对象上,request.serviceWorker()返回该 Service Worker 实例,而request.frame会抛出异常(Worker 请求没有归属 Frame)。
对于 页面 发起的请求,则通过 response.fromServiceWorker() 判断该请求是否被 Service Worker 的 fetch 处理器接管——返回 true 表示响应实际来自 SW 的 respondWith。
事件流推演:一个透明代理 Worker
考虑文档中给出的透明代理示例——Worker 对页面的每个请求原样转发:
self.addEventListener('fetch', event => {
// actually make the request
const responsePromise = fetch(event.request);
// send it back to the page
event.respondWith(responsePromise);
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
若 index.html 注册了该 Worker,随后页面 fetch 了 data.json,Playwright 将按以下顺序发出请求事件(对应网络生命周期事件同步触发):
| 事件 | 发起者(Owner) | URL | 可被 route 拦截 | Response.fromServiceWorker() |
|---|---|---|---|---|
context.request |
Frame | index.html | 是 | — |
page.request |
Frame | index.html | 是 | — |
context.request |
Service Worker | transparent-service-worker.js | 是 | — |
context.request |
Service Worker | data.json | 是 | — |
context.request |
Frame | data.json | 否 | 是(true) |
page.request |
Frame | data.json | 否 | 是(true) |
两个要点值得展开:
data.json出现两次context.request事件:一条由 Frame 拥有(页面视角),一条由 Service Worker 拥有(Worker 实际发出fetch的视角)。若监听context.request统计请求量,遇到有 fetch 处理器的 SW 时必须按request.serviceWorker()去重,否则会把同一个资源数两遍。- 只有 Worker 侧那条
data.json请求可被路由。Frame 侧的请求根本没有机会触达外部网络(fetch 处理器已注册,浏览器不会再发出原始请求),因此对 Frame 侧请求调用route.fulfill/route.continue没有意义;真正能改写响应的是 Worker 侧那条。
一个必须记住的陷阱
官方文档特别加粗警告:在 request.serviceWorker() 非空的 Request / Response 上调用 request.frame 或 response.frame 会抛异常。这是因为 Worker 请求不存在归属 Frame。编写通用的 route 处理器时,应先判 serviceWorker() 再访问 frame,顺序颠倒会直接让路由回调崩溃。
仅路由 Service Worker 发起的请求
理解了归属标识后,"只拦截 Worker 请求、放行页面请求"就是一个条件分支。官方示例(JavaScript):
await context.route('**', async route => {
if (route.request().serviceWorker()) {
// NB: calling route.request().frame() here would THROW
await route.fulfill({
contentType: 'text/plain',
status: 200,
body: 'from sw',
});
} else {
await route.continue();
}
});
def handle_route(route: Route):
if route.request.service_worker:
# NB: accessing route.request.frame here would THROW
route.fulfill(content_type="text/plain", status=200, body="from sw")
else:
route.continue_()
context.route("**", handle_route)
async def handle_route(route: Route):
if route.request.service_worker:
# NB: accessing route.request.frame here would THROW
await route.fulfill(content_type="text/plain", status=200, body="from sw")
else:
await route.continue_()
await context.route("**", handle_route)
注意 JS 中 request.serviceWorker() 是方法调用,Python 中 request.service_worker 是属性访问,两者语义一致:非空即代表该请求由 Service Worker 发出。这条技巧的典型用途包括:模拟 Worker 拉取资源时断网/超时以验证回退逻辑、为 Worker 的预取请求注入固定响应、或验证缓存未命中时 Worker 发出的真实请求内容。
底层链路:CRServiceWorker 如何上报请求
从源码看,Worker 请求能进入 context.route 的完整链路是:Chromium 协议侧为每个 Service Worker 会话创建独立的 CRNetworkManager,请求经过 CDP 的 Fetch 域拦截后,回调到 CRServiceWorker.requestStarted:
requestStarted(request: network.Request, route?: network.RouteDelegate) {
this.browserContext.emit(BrowserContext.Events.Request, request);
if (route)
new network.Route(request, route).handle(this.browserContext.requestInterceptors);
}
事件直接以 BrowserContext 为发射源(而非 Page),这正是文档中"Worker 请求通过 BrowserContext 上报"结论的实现依据。同一类还同步了 offline、httpCredentials、extraHTTPHeaders、userAgent 等上下文选项到 Worker 的网络栈——从 updateOffline 等方法可以看出,context.setOffline 同样作用于 Service Worker 的请求,这意味着"离线模式 + SW 缓存"组合测试(验证缓存命中路径)是完全可操作的。另外源码中保留了 PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK 环境变量用于在开发时关闭 Worker 网络检测,属于调试用开关,测试代码一般无需接触。
已知限制
文档同时明确了当前版本的一条已知限制:更新后的 Service Worker 主脚本(SW 脚本自身的新版本下载)请求目前无法被路由。这与透明代理示例中"Worker 脚本 URL 可被路由"并不矛盾——首次加载的脚本请求可拦截,而浏览器在后台检查 Worker 脚本更新时发出的验证请求走的是浏览器内部通道,绕过了 Playwright 的拦截层。编写"强制 Worker 脚本被 Mock"的测试时需要规避该场景,通常做法是固定 Worker 脚本内容后依赖缓存,或改用 fulfill 响应页面侧对脚本的初始加载。
小结
回到 官方文档原文,Playwright 的 Service Worker 支持可以浓缩为四件事:
- 策略控制:
serviceWorkers: 'block'以一行注入脚本屏蔽注册,换取测试确定性(实现、策略测试); - 实例获取与激活等待:
waitForEvent('serviceworker')/expect_event("serviceworker")拿句柄,controllerchange幂等等待确保可安全evaluate; - 双链路事件模型:
request.serviceWorker()与response.fromServiceWorker()两个标识符区分 Frame 侧与 Worker 侧请求,且 Worker 请求上访问frame会抛异常; - 定向路由:
context.route中按serviceWorker()分支,精准改写 Worker 发起的请求。
需要再次强调适用前提:以上能力均限于 Chromium 系浏览器;若测试目标是 Firefox 或 WebKit 且页面依赖 Service Worker,只能验证"无 Worker"路径下的行为。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00