(实验性功能) Service Worker 网络事件
简介
如果您需要进行常规的网络请求模拟、路由和拦截,请先查阅网络指南。Playwright 为此类用例提供了内置 API,无需使用下文介绍的功能。但如果您对 Service Worker 自身发起的请求感兴趣,请继续阅读。
Service Workers 提供了一种浏览器原生机制,用于处理页面通过原生 Fetch API (fetch
) 发起的请求以及其他网络资源(如脚本、CSS 和图片)。
它们可以充当页面与外部网络之间的网络代理来执行缓存逻辑,或者如果 Service Worker 添加了 FetchEvent 监听器,还可以为用户提供离线体验。
许多使用 Service Worker 的网站仅将其作为透明的优化技术。虽然用户可能会感受到更快的体验,但应用程序的实现并不感知它们的存在。无论是否启用 Service Worker,应用程序的运行在功能上看起来都是等效的。
如何启用
Playwright 对 Service Worker 发起请求的检查和路由功能目前是实验性的,默认禁用。
设置环境变量 PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS
为 1
(或任意值)以启用该功能。目前仅支持 Chrome/Chromium 浏览器。
如果您正在使用(或有意使用此功能),请在此 issue 中留言告知您的使用场景。
Service Worker 请求处理
访问 Service Worker 并等待激活
您可以使用 browserContext.serviceWorkers() 来列出所有的 Service Worker,或者如果您预期页面会触发其注册,可以专门监听 Service Worker:
const serviceWorkerPromise = context.waitForEvent('serviceworker');
await page.goto('/example-with-a-service-worker.html');
const serviceworker = await serviceWorkerPromise;
browserContext.on('serviceworker') 事件会在 Service Worker 的主脚本执行之前触发,因此在调用 serviceworker.evaluate() 之前,您应该等待其激活。
以下是等待 Service Worker 激活的更符合惯用方式的实现,但以下是一种与实现无关的方法:
await page.evaluate(async () => {
const registration = await window.navigator.serviceWorker.getRegistration();
if (registration.active?.state === 'activated')
return;
await new Promise(res =>
window.navigator.serviceWorker.addEventListener('controllerchange', res),
);
});
网络事件与路由
Service Worker 发起的任何网络请求都会触发:
- browserContext.on('request') 及其对应事件(browserContext.on('requestfinished') 和 browserContext.on('response'),或 browserContext.on('requestfailed'))
- browserContext.route() 会捕获该请求
- request.serviceWorker() 会被设置为 Service Worker 实例,而 request.frame() 会抛出异常
- response.fromServiceWorker() 将返回
false
此外,Page(包括其子 Frame)发起的任何网络请求都会触发:
- browserContext.on('request') 及其对应事件(browserContext.on('requestfinished') 和 browserContext.on('response'),或 browserContext.on('requestfailed'))
- page.on('request') 及其对应事件(page.on('requestfinished') 和 page.on('response'),或 page.on('requestfailed'))
- page.route() 和 page.route() 将不会捕获该请求(如果已注册 Service Worker 的 fetch 处理程序)
- request.serviceWorker() 会被设置为
null
,而 request.frame() 会返回 Frame - response.fromServiceWorker() 将返回
true
(如果已注册 Service Worker 的 fetch 处理程序)
许多 Service Worker 实现只是简单地执行来自页面的请求(为简化说明,可能省略了一些自定义缓存/离线逻辑):
self.addEventListener('fetch', event => {
// 实际发起请求
const responsePromise = fetch(event.request);
// 将响应返回给页面
event.respondWith(responsePromise);
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
如果页面注册了上述 Service Worker:
<!-- filename: index.html -->
<script>
window.registrationPromise = navigator.serviceWorker.register('/transparent-service-worker.js');
</script>
通过 page.goto() 首次访问页面时,将触发以下请求/响应事件(以及对应的网络生命周期事件):
事件 | 所有者 | URL | 可路由 | response.fromServiceWorker() |
---|---|---|---|---|
browserContext.on('request') | Frame | index.html | 是 | |
page.on('request') | Frame | index.html | 是 | |
browserContext.on('request') | Service Worker | transparent-service-worker.js | 是 | |
browserContext.on('request') | Service Worker | data.json | 是 | |
browserContext.on('request') | Frame | data.json | 是 | |
page.on('request') | Frame | data.json | 是 |
由于示例中的 Service Worker 仅作为基础透明"代理":
- 对于
data.json
会有两个 browserContext.on('request') 事件:一个属于 Frame,另一个属于 Service Worker - 只有 Service Worker 所属的资源请求可以通过 browserContext.route() 路由;而 Frame 所属的
data.json
事件不可路由,因为它们甚至不可能到达外部网络,因为 Service Worker 已注册了 fetch 处理程序
重要提示:如果在 Request/Response 上调用 request.frame() 或 response.frame(),且该请求/响应的 request.serviceWorker() 非空,则会抛出异常。
高级示例
当 Service Worker 处理页面的请求时,它可能会向外部网络发起 0 到 n 次请求。Service Worker 可能直接从缓存响应、在内存中生成响应、重写请求、发起两次请求然后合并为 1 次响应等。
通过以下代码片段可以理解 Playwright 如何观察请求/响应事件,以及这些情况如何影响路由行为。
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
// 1. 预获取并缓存 /addressbook.json
return cache.add('/addressbook.json');
})
);
});
// 选择处理来自页面的 FetchEvent
self.addEventListener('fetch', event => {
event.respondWith(
(async () => {
// 1. 首先尝试从缓存直接响应
const response = await caches.match(event.request);
if (response)
return response;
// 2. 将 /foo 请求重写为 /bar
if (event.request.url.endsWith('foo'))
return fetch('./bar');
// 3. 阻止 tracker.js 的获取,返回占位响应
if (event.request.url.endsWith('tracker.js')) {
return new Response('console.log("no trackers!")', {
status: 200,
headers: { 'Content-Type': 'text/javascript' },
});
}
// 4. 其他情况直接执行 fetch 并响应
return fetch(event.request);
})()
);
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
以及一个简单注册 Service Worker 的页面:
<!-- filename: index.html -->
<script>
window.registrationPromise = navigator.serviceWorker.register('/complex-service-worker.js');
</script>
当首次通过 page.goto() 访问页面时,会触发以下请求/响应事件:
事件 | 所有者 | URL | 是否路由 | response.fromServiceWorker() |
---|---|---|---|---|
browserContext.on('request') | Frame | index.html | 是 | |
page.on('request') | Frame | index.html | 是 | |
browserContext.on('request') | Service Worker | complex-service-worker.js | 是 | |
browserContext.on('request') | Service Worker | addressbook.json | 是 |
需要注意 cache.add
会导致 Service Worker 发起请求(Service Worker 所有),甚至在页面请求 addressbook.json
之前。
当 Service Worker 激活并处理 FetchEvent 后,如果页面发起以下请求:
await page.evaluate(() => fetch('/addressbook.json'));
await page.evaluate(() => fetch('/foo'));
await page.evaluate(() => fetch('/tracker.js'));
await page.evaluate(() => fetch('/fallthrough.txt'));
会触发以下请求/响应事件:
事件 | 所有者 | URL | 是否路由 | response.fromServiceWorker() |
---|---|---|---|---|
browserContext.on('request') | Frame | addressbook.json | 是 | |
page.on('request') | Frame | addressbook.json | 是 | |
browserContext.on('request') | Service Worker | bar | 是 | |
browserContext.on('request') | Frame | foo | 是 | |
page.on('request') | Frame | foo | 是 | |
browserContext.on('request') | Frame | tracker.js | 是 | |
page.on('request') | Frame | tracker.js | 是 | |
browserContext.on('request') | Service Worker | fallthrough.txt | 是 | |
browserContext.on('request') | Frame | fallthrough.txt | 是 | |
page.on('request') | Frame | fallthrough.txt | 是 |
需要注意:
- 页面请求了
/foo
,但 Service Worker 请求的是/bar
,所以只有/foo
的 Frame 所有事件,没有/bar
的 - 同样,Service Worker 从未为
tracker.js
发起网络请求,所以该请求只有 Frame 所有的事件
仅路由 Service Worker 请求
await context.route('**', async route => {
if (route.request().serviceWorker()) {
// 注意:在此处调用 route.request().frame() 会抛出异常
return route.fulfill({
contentType: 'text/plain',
status: 200,
body: 'from sw',
});
} else {
return route.continue();
}
});
已知限制
目前无法路由 Service Worker 主脚本代码的更新请求 (https://github.com/microsoft/playwright/issues/14711)。