Skip to main content

(实验性)Service Worker 网络活动

介绍

¥Introduction

警告

如果你想要进行一般的网络模拟、路由和拦截,请首先查看 网络指南。Playwright 为此用例提供内置 API,不需要以下信息。但是,如果你对 Service Workers 本身提出的请求感兴趣,请阅读以下内容。

¥If you're looking to do general network mocking, routing, and interception, please see the Network Guide first. Playwright provides built-in APIs for this use case that don't require the information below. However, if you're interested in requests made by Service Workers themselves, please read below.

服务工作进程 提供了一种浏览器原生方法,用于处理具有原生 获取 API (fetch) 的页面触发的请求以及其他网络请求的资源(如脚本、CSS 和图片)。

¥Service Workers provide a browser-native method of handling requests made by a page with the native Fetch API (fetch) along with other network-requested assets (like scripts, css, and images).

它们可以充当页面和外部网络之间的网络代理来执行缓存逻辑,或者如果 Service Worker 添加 FetchEvent 监听器,则可以为用户提供离线体验。

¥They can act as a network proxy between the page and the external network to perform caching logic or can provide users with an offline experience if the Service Worker adds a FetchEvent listener.

许多使用 Service Worker 的站点只是将它们用作透明的优化技术。虽然用户可能会注意到更快的体验,但应用的实现并不知道它们的存在。在启用或不启用 Service Worker 的情况下运行应用看起来功能相同。

¥Many sites that use Service Workers simply use them as a transparent optimization technique. While users might notice a faster experience, the app's implementation is unaware of their existence. Running the app with or without Service Workers enabled appears functionally equivalent.

如何启用

¥How to Enable

Playwright 对 Service Worker 触发的请求的检查和路由是实验性的,默认情况下处于禁用状态。

¥Playwright's inspection and routing of requests made by Service Workers are experimental and disabled by default.

PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS 环境变量设置为 1(或任何其他值)以启用该功能。目前仅支持 Chrome/Chromium。

¥Set the PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS environment variable to 1 (or any other value) to enable the feature. Only Chrome/Chromium are currently supported.

如果你正在使用(或有兴趣使用此功能),请在 这个问题 上发表评论,让我们知道你的用例。

¥If you're using (or are interested in using this feature), please comment on this issue letting us know your use case.

服务工作进程获取

¥Service Worker Fetch

访问 Service Worker 并等待激活

¥Accessing Service Workers and Waiting for Activation

你可以使用 browserContext.serviceWorkers() 列出服务 Worker,或者如果你预计某个页面将触发其 registration,则特别注意服务 Worker

¥You can use browserContext.serviceWorkers() to list the Service Workers, or specifically watch for the Service Worker if you anticipate a page will trigger its registration:

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() 之前,你应该等待它的激活。

¥browserContext.on('serviceworker') is fired before the Service Worker's main script has been evaluated, so before calling serviceworker.evaluate() you should wait on its activation.

还有更惯用的等待 Service Worker 被激活的方法,但以下是与实现无关的方法:

¥There are more idiomatic methods of waiting for a Service Worker to be activated, but the following is an implementation agnostic method:

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),
);
});

网络事件和路由

¥Network Events and Routing

Service Worker 触发的任何网络请求都将具有:

¥Any network request made by the Service Worker will have:

此外,页面(包括其子 Frame)触发的任何网络请求都将具有:

¥Additionally, any network request made by the Page (including its sub-Frames) will have:

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

¥Many Service Worker implementations simply execute the request from the page (possibly with some custom caching/offline logic omitted for simplicity):

transparent-service-worker.js
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());
});

如果一个页面注册了上述 Service Worker:

¥If a page registers the above Service Worker:



<!-- filename: index.html -->


<script>
window.registrationPromise = navigator.serviceWorker.register('/transparent-service-worker.js');
</script>

通过 page.goto() 第一次访问页面时,将触发以下请求/响应事件(以及相应的网络生命周期事件):

¥On the first visit to the page via page.goto(), the following Request/Response events would be emitted (along with the corresponding network lifecycle events):

事件所有者URL路由response.fromServiceWorker()
browserContext.on('request')[Frame]index.html是的
page.on('request')[Frame]index.html是的
browserContext.on('request')服务[工作线程]透明服务工作者.js是的
browserContext.on('request')服务[工作线程]data.json是的
browserContext.on('request')[Frame]data.json是的
page.on('request')[Frame]data.json是的

由于示例 Service Worker 只是充当基本透明的 "proxy":

¥Since the example Service Worker just acts a basic transparent "proxy":

  • data.json 有 2 个 browserContext.on('request') 事件;一个由 Frame 所有,另一个由服务 Worker 所有。

    ¥There's 2 browserContext.on('request') events for data.json; one Frame-owned, the other Service Worker-owned.

  • 只有服务 Worker 拥有的资源请求可以通过 browserContext.route() 路由;Frame 拥有的 data.json 事件不可路由,因为 Service Worker 注册了一个获取处理程序,因此它们甚至不可能访问外部网络。

    ¥Only the Service Worker-owned request for the resource was routable via browserContext.route(); the Frame-owned events for data.json are not routeable, as they would not have even had the possibility to hit the external network since the Service Worker has a fetch handler registered.

提醒

需要注意的是:如果在具有非空 request.serviceWorker()Request/Response 上调用,则调用 request.frame()response.frame() 将引发异常。

¥It's important to note: calling request.frame() or response.frame() will throw an exception, if called on a Request/Response that has a non-null request.serviceWorker().

高级示例

¥Advanced Example

当 Service Worker 处理页面的请求时,Service Worker 可以向外部网络触发 0 到 n 个请求。Service Worker 可能直接从缓存中响应,在内存中生成响应,重写请求,触发两个请求,然后合并为 1,等等。

¥When a Service Worker handles a page's request, the Service Worker can make 0 to n requests to the external network. The Service Worker might respond directly from a cache, generate a response in memory, rewrite the request, make two requests and then combine into 1, etc.

请考虑下面的代码片段,以了解 Playwright 对请求/响应的看法以及它在某些情况下如何影响路由。

¥Consider the code snippets below to understand Playwright's view into the Request/Responses and how it impacts routing in some of these cases.

complex-service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
// 1. Pre-fetches and caches /addressbook.json
return cache.add('/addressbook.json');
})
);
});

// Opt to handle FetchEvent's from the page
self.addEventListener('fetch', event => {
event.respondWith(
(async () => {
// 1. Try to first serve directly from caches
const response = await caches.match(event.request);
if (response)
return response;

// 2. Re-write request for /foo to /bar
if (event.request.url.endsWith('foo'))
return fetch('./bar');

// 3. Prevent tracker.js from being retrieved, and returns a placeholder response
if (event.request.url.endsWith('tracker.js')) {
return new Response('console.log("no trackers!")', {
status: 200,
headers: { 'Content-Type': 'text/javascript' },
});
}

// 4. Otherwise, fallthrough, perform the fetch and respond
return fetch(event.request);
})()
);
});

self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});

还有一个简单注册 Service Worker 的页面:

¥And a page that simply registers the Service Worker:



<!-- filename: index.html -->


<script>
window.registrationPromise = navigator.serviceWorker.register('/complex-service-worker.js');
</script>

通过 page.goto() 第一次访问页面时,将触发以下请求/响应事件:

¥On the first visit to the page via page.goto(), the following Request/Response events would be emitted:

事件所有者URL路由response.fromServiceWorker()
browserContext.on('request')[Frame]index.html是的
page.on('request')[Frame]index.html是的
browserContext.on('request')服务[工作线程]复杂服务工作者.js是的
browserContext.on('request')服务[工作线程]地址簿.json是的

需要注意的是,即使在页面中请求 addressbook.json 之前,cache.add 也会导致 Service Worker 触发请求(服务 Worker 拥有)。

¥It's important to note that cache.add caused the Service Worker to make a request (Service Worker-owned), even before addressbook.json was asked for in the page.

一旦 Service Worker 被激活并处理 FetchEvents,如果页面触发以下请求:

¥Once the Service Worker is activated and handling FetchEvents, if the page makes the following requests:

await page.evaluate(() => fetch('/addressbook.json'));
await page.evaluate(() => fetch('/foo'));
await page.evaluate(() => fetch('/tracker.js'));
await page.evaluate(() => fetch('/fallthrough.txt'));

将触发以下请求/响应事件:

¥The following Request/Response events would be emitted:

事件所有者URL路由response.fromServiceWorker()
browserContext.on('request')[Frame]地址簿.json是的
page.on('request')[Frame]地址簿.json是的
browserContext.on('request')服务[工作线程]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')服务[工作线程]fallthrough.txt是的
browserContext.on('request')[Frame]fallthrough.txt是的
page.on('request')[Frame]fallthrough.txt是的

需要注意的是:

¥It's important to note:

  • 页面请求 /foo,但 Service Worker 请求 /bar,因此 /foo 仅有 Frame 拥有的事件,但没有 /bar

    ¥The page requested /foo, but the Service Worker requested /bar, so there are only Frame-owned events for /foo, but not /bar.

  • 同样,Service Worker 从未访问 tracker.js 的网络,因此仅针对该请求触发 Frame 拥有的事件。

    ¥Likewise, the Service Worker never hit the network for tracker.js, so only Frame-owned events were emitted for that request.

仅路由服务工作线程请求

¥Routing Service Worker Requests Only

await context.route('**', async route => {
if (route.request().serviceWorker()) {
// NB: calling route.request().frame() here would THROW
return route.fulfill({
contentType: 'text/plain',
status: 200,
body: 'from sw',
});
} else {
return route.continue();
}
});

已知限制

¥Known Limitations

当前无法路由对更新的 Service Worker 主脚本代码的请求 (https://github.com/microsoft/playwright/issues/14711)。

¥Requests for updated Service Worker main script code currently cannot be routed (https://github.com/microsoft/playwright/issues/14711).