Service Workers
Введение
Service Workers поддерживаются только в браузерах на базе Chromium.
Если вы ищете решение для общего мокинга сети, маршрутизации и перехвата запросов, сначала ознакомьтесь с Network Guide. Playwright предоставляет встроенные API для этого сценария, которые не требуют информации ниже. Однако, если вас интересуют запросы, выполняемые самими Service Workers, читайте дальше.
Service Workers предоставляют нативный для браузера способ обработки запросов, выполняемых страницей с помощью нативного Fetch API (fetch), а также других сетевых ресурсов (таких как скрипты, CSS и изображения).
Они могут выступать в роли сетевого прокси между страницей и внешней сетью для реализации логики кэширования или предоставлять пользователям офлайн‑режим, если Service Worker добавляет обработчик события FetchEvent.
Многие сайты, использующие Service Workers, применяют их лишь как прозрачную оптимизацию. Пользователи могут заметить более высокую скорость работы, однако сама реализация приложения «не знает» об их существовании. Запуск приложения с включёнными или отключёнными Service Workers выглядит функционально одинаково.
Как отключить Service Workers
Playwright позволяет отключать Service Workers во время тестирования. Это делает тесты более предсказуемыми и производительными. Однако если ваша реальная страница использует Service Worker, поведение может отличаться.
Чтобы отключить service workers, установите testOptions.serviceWorkers в значение 'block'.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
serviceWorkers: 'allow'
},
});
Доступ к Service Workers и ожидание активации
Вы можете использовать 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 возьмёт контроль над страницей, поэтому перед выполнением кода в воркере с помощью worker.evaluate() необходимо дождаться его активации.
Существуют более идиоматичные способы ожидания активации Service Worker, однако следующий вариант является независимым от конкретной реализации:
await page.evaluate(async () => {
const registration = await window.navigator.serviceWorker.getRegistration();
if (registration.active?.state === 'activated')
return;
await new Promise(resolve => {
window.navigator.serviceWorker.addEventListener('controllerchange', resolve);
});
});
Сетевые события и маршрутизация
Любой сетевой запрос, выполненный Service Worker, репортится через объект BrowserContext:
- Срабатывают события browserContext.on('request'), browserContext.on('requestfinished'), browserContext.on('response') и browserContext.on('requestfailed')
- Метод browserContext.route() «видит» этот запрос
- У request.serviceWorker() будет установлено значение — экземпляр Service Worker, а вызов request.frame() выбросит исключение
Кроме того, для любого сетевого запроса, выполненного Page, метод response.fromServiceWorker() возвращает true, если запрос был обработан fetch‑обработчиком Service Worker.
Рассмотрим простой service worker, который перехватывает и проксирует каждый запрос, сделанный страницей:
self.addEventListener('fetch', event => {
// фактически выполняем запрос
const responsePromise = fetch(event.request);
// отправляем его обратно на страницу
event.respondWith(responsePromise);
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
Если index.html регистрирует этот service worker, а затем запрашивает data.json, будут сгенерированы следующие события Request/Response (вместе с соответствующими событиями жизненного цикла сети):
| Event | Owner | URL | Routed | response.fromServiceWorker() |
|---|---|---|---|---|
| browserContext.on('request') | Frame | index.html | Yes | |
| page.on('request') | Frame | index.html | Yes | |
| browserContext.on('request') | Service Worker | transparent-service-worker.js | Yes | |
| browserContext.on('request') | Service Worker | data.json | Yes | |
| browserContext.on('request') | Frame | data.json | Yes | |
| page.on('request') | Frame | data.json | Yes |
Поскольку в примере Service Worker выступает лишь в роли простого прозрачного «прокси»:
- Для
data.jsonсуществует 2 события browserContext.on('request'): одно принадлежит Frame, другое — Service Worker. - Только запрос, принадлежащий Service Worker, может быть маршрутизирован через browserContext.route(); события, принадлежащие Frame, для
data.jsonне маршрутизируются, так как они даже не имели возможности попасть во внешнюю сеть — у Service Worker зарегистрирован обработчик fetch.
Важно отметить: вызов request.frame() или response.frame() выбросит исключение, если он выполнен для Request/Response, у которого request.serviceWorker() не равен null.
Маршрутизация только запросов Service Worker
await context.route('**', async route => {
if (route.request().serviceWorker()) {
// NB: вызов route.request().frame() здесь ВЫБРОСИТ исключение
await route.fulfill({
contentType: 'text/plain',
status: 200,
body: 'from sw',
});
} else {
await route.continue();
}
});
Известные ограничения
Запросы на обновление основного скрипта Service Worker в настоящее время не могут быть маршрутизированы (https://github.com/microsoft/playwright/issues/14711).