(实验性)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:
-
browserContext.on('request') 及其相应事件(browserContext.on('requestfinished') 和 browserContext.on('response'),或 browserContext.on('requestfailed'))
¥browserContext.on('request') and its corresponding events (browserContext.on('requestfinished') and browserContext.on('response'), or browserContext.on('requestfailed'))
-
browserContext.route() 将看到该请求
¥browserContext.route() will see the request
-
request.serviceWorker() 将被设置为 Service Worker 实例,request.frame() 将抛出
¥request.serviceWorker() will be set to the Service Worker instance, and request.frame() will throw
-
response.fromServiceWorker() 将返回
false
¥response.fromServiceWorker() will return
false
此外,页面(包括其子 Frame)触发的任何网络请求都将具有:
¥Additionally, any network request made by the Page (including its sub-Frames) will have:
-
browserContext.on('request') 及其相应事件(browserContext.on('requestfinished') 和 browserContext.on('response'),或 browserContext.on('requestfailed'))
¥browserContext.on('request') and its corresponding events (browserContext.on('requestfinished') and browserContext.on('response'), or browserContext.on('requestfailed'))
-
page.on('request') 及其相应事件(page.on('requestfinished') 和 page.on('response'),或 page.on('requestfailed'))
¥page.on('request') and its corresponding events (page.on('requestfinished') and page.on('response'), or page.on('requestfailed'))
-
page.route() 和 page.route() 将看不到该请求(如果 Service Worker 的获取处理程序已注册)
¥page.route() and page.route() will not see the request (if a Service Worker's fetch handler was registered)
-
request.serviceWorker() 将设置为
null
,request.frame() 将返回 Frame¥request.serviceWorker() will be set to
null
, and request.frame() will return the Frame -
response.fromServiceWorker() 将返回
true
(如果 Service Worker 的获取处理程序已注册)¥response.fromServiceWorker() will return
true
(if a Service Worker's fetch handler was registered)
许多 Service Worker 实现只是执行来自页面的请求(为了简单起见,可能省略了一些自定义缓存/离线逻辑):
¥Many Service Worker implementations simply execute the request from the page (possibly with some custom caching/offline logic omitted for simplicity):
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.
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).