跳到主要内容
版本:Next

(Experimental) Service Worker Network Events

Introduction​

注意

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.

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).

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.

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's inspection and routing of requests made by Service Workers are experimental and disabled by default.

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​

Accessing Service Workers and Waiting for Activation​

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') is fired before the Service Worker's main script has been evaluated, so before calling serviceworker.evaluate() you should wait on its activation.

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​

Any network request made by the Service Worker will have:

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

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

If a page registers the above Service Worker:

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

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):

EventOwnerURLRoutedresponse.fromServiceWorker()
browserContext.on('request')Frameindex.htmlYes
page.on('request')Frameindex.htmlYes
browserContext.on('request')Service Workertransparent-service-worker.jsYes
browserContext.on('request')Service Workerdata.jsonYes
browserContext.on('request')Framedata.jsonYes
page.on('request')Framedata.jsonYes

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

  • There's 2 browserContext.on('request') events for data.json; one Frame-owned, the other Service Worker-owned.
  • 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.
警告

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​

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.

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

And a page that simply registers the Service Worker:

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

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

EventOwnerURLRoutedresponse.fromServiceWorker()
browserContext.on('request')Frameindex.htmlYes
page.on('request')Frameindex.htmlYes
browserContext.on('request')Service Workercomplex-service-worker.jsYes
browserContext.on('request')Service Workeraddressbook.jsonYes

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.

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:

EventOwnerURLRoutedresponse.fromServiceWorker()
browserContext.on('request')Frameaddressbook.jsonYes
page.on('request')Frameaddressbook.jsonYes
browserContext.on('request')Service WorkerbarYes
browserContext.on('request')FramefooYes
page.on('request')FramefooYes
browserContext.on('request')Frametracker.jsYes
page.on('request')Frametracker.jsYes
browserContext.on('request')Service Workerfallthrough.txtYes
browserContext.on('request')Framefallthrough.txtYes
page.on('request')Framefallthrough.txtYes

It's important to note:

  • The page requested /foo, but the Service Worker requested /bar, so there are only Frame-owned events for /foo, but not /bar.
  • 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​

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