跳到主要内容

(实验性功能) 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_EVENTS1(或任意值)以启用该功能。目前仅支持 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 发起的任何网络请求都会触发:

此外,Page(包括其子 Frame)发起的任何网络请求都会触发:

许多 Service Worker 实现只是简单地执行来自页面的请求(为简化说明,可能省略了一些自定义缓存/离线逻辑):

transparent-service-worker.js
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')Frameindex.html
page.on('request')Frameindex.html
browserContext.on('request')Service Workertransparent-service-worker.js
browserContext.on('request')Service Workerdata.json
browserContext.on('request')Framedata.json
page.on('request')Framedata.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 如何观察请求/响应事件,以及这些情况如何影响路由行为。

complex-service-worker.js
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')Frameindex.html
page.on('request')Frameindex.html
browserContext.on('request')Service Workercomplex-service-worker.js
browserContext.on('request')Service Workeraddressbook.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')Frameaddressbook.json
page.on('request')Frameaddressbook.json
browserContext.on('request')Service Workerbar
browserContext.on('request')Framefoo
page.on('request')Framefoo
browserContext.on('request')Frametracker.js
page.on('request')Frametracker.js
browserContext.on('request')Service Workerfallthrough.txt
browserContext.on('request')Framefallthrough.txt
page.on('request')Framefallthrough.txt

需要注意:

  • 页面请求了 /foo,但 Service Worker 请求的是 /bar,所以只有 /fooFrame 所有事件,没有 /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)。