Перейти к основному содержимому

(Экспериментально) События сети Service Worker

Введение

warning

Если вы хотите заниматься общим перехватом, маршрутизацией и имитацией сетевых запросов, пожалуйста, сначала ознакомьтесь с Руководством по сети. Playwright предоставляет встроенные API для этого случая, которые не требуют информации, представленной ниже. Однако, если вас интересуют запросы, сделанные самими Service Workers, читайте дальше.

Service Workers предоставляют встроенный в браузер метод обработки запросов, сделанных страницей с использованием нативного Fetch API (fetch), а также других сетевых ресурсов (таких как скрипты, CSS и изображения).

Они могут выступать в качестве сетевого прокси между страницей и внешней сетью для выполнения логики кэширования или могут предоставлять пользователям офлайн-опыт, если Service Worker добавляет слушатель FetchEvent.

Многие сайты, использующие Service Workers, просто используют их как прозрачную технику оптимизации. Хотя пользователи могут заметить более быструю работу, реализация приложения не осознает их существования. Запуск приложения с включенными или отключенными Service Workers выглядит функционально эквивалентным.

Как включить

Инспекция и маршрутизация запросов, сделанных Service Workers в Playwright, являются экспериментальными и по умолчанию отключены.

Установите переменную окружения PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS в 1 (или любое другое значение), чтобы включить эту функцию. В настоящее время поддерживаются только Chrome/Chromium.

Если вы используете (или заинтересованы в использовании этой функции), пожалуйста, оставьте комментарий в этом вопросе, сообщив нам о вашем случае использования.

Запросы Service Worker

Доступ к Service Workers и ожидание активации

Вы можете использовать browserContext.serviceWorkers() для получения списка Service Workers или специально следить за 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, будет иметь:

Кроме того, любой сетевой запрос, сделанный страницей (включая ее под-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() будут сгенерированы следующие события Request/Response (вместе с соответствующими событиями жизненного цикла сети):

СобытиеВладелец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 просто действует как базовый прозрачный "прокси":

  • Существует 2 события browserContext.on('request') для data.json; одно принадлежит Frame, другое - Service Worker.
  • Только запрос, принадлежащий Service Worker, для ресурса был маршрутизируемым через browserContext.route(); события, принадлежащие Frame, для data.json не маршрутизируемы, так как они даже не имели возможности попасть во внешнюю сеть, поскольку у Service Worker зарегистрирован обработчик fetch.
предупреждение

Важно отметить: вызов request.frame() или response.frame() выбросит исключение, если он вызван на Request/Response, у которого request.serviceWorker() не равен null.

Продвинутый пример

Когда Service Worker обрабатывает запрос страницы, Service Worker может сделать от 0 до n запросов к внешней сети. Service Worker может ответить напрямую из кэша, сгенерировать ответ в памяти, переписать запрос, сделать два запроса и затем объединить их в один и т.д.

Рассмотрите приведенные ниже фрагменты кода, чтобы понять, как 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() будут сгенерированы следующие события Request/Response:

СобытиеВладелец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 и обработки FetchEvents, если страница делает следующие запросы:

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

Будут сгенерированы следующие события Request/Response:

СобытиеВладелец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, поэтому существуют только события, принадлежащие Frame, для /foo, но не для /bar.
  • Аналогично, Service Worker никогда не обращался к сети для tracker.js, поэтому для этого запроса были сгенерированы только события, принадлежащие Frame.

Маршрутизация только запросов Service Worker

await context.route('**', async route => {
if (route.request().serviceWorker()) {
// NB: вызов 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).