Перейти к содержанию

Расширение браузера: Service worker и краткая жизнь фонового контекста

Оглавление серии

Popup умеет обрабатывать кнопку, но пока вся работа относится к открытому окну. В Manifest V3 фоновую роль выполняет extension service worker. Он получает события, делает ограниченную работу и может завершаться, когда не нужен. Добавим простую команду PING, чтобы увидеть связь контекстов, не смешивая её с сохранением данных. Результат — popup знает, что обработчик ответил, а мы понимаем предел такой проверки.

Service worker не является скрытой HTML-страницей. У него нет document, textarea и DOM для вывода заметок. Если ему понадобятся данные страницы, их должен получить подходящий контекст и передать результат. Если ему нужно помнить записи между запусками, нужен storage. Глобальная переменная может существовать в текущем worker, но исчезнет после завершения. Поэтому её нельзя объявлять источником истины для сохранённых заметок пользователя.

Регистрация фона

Manifest получает background.service_worker со значением worker.js и type="module". Первый ключ называет входной файл, второй разрешает модульную организацию, которой воспользуемся в дальнейших шагах. Сейчас worker короткий и не импортирует другие файлы, но профиль пакета уже согласован. Мы не объявляем background.page и не предполагаем вечную страницу старой модели. Выбор Manifest V3 влияет не только на номер JSON, но и на способ жизни фонового кода.

Обработчик runtime.onMessage регистрируется на верхнем уровне файла. Это позволяет worker сразу знать, какие события он принимает, когда браузер запускает контекст. Не помещаем регистрацию внутрь случайной асинхронной загрузки пользовательских данных. В дальнейшей версии загрузка будет происходить внутри принятой операции. У события должна оставаться известная точка приёма независимо от того, успела ли прочитаться очередная настройка.

Сообщение содержит version=1 и type="PING". Оно не является строкой с произвольным JavaScript и не запрашивает имя API для динамического вызова. Такой небольшой контракт удобен для объяснения обмена: отправитель задаёт известную команду, получатель проверяет её и возвращает известную форму. Уже здесь проверяются sender.id и точный URL popup. Это ещё не полная проверка всех будущих данных, но правило области отправителя появляется до privileged операций.

Полные файлы шага

Показанные файлы полностью совпадают с выбранным состоянием архива. Остальные согласованные файлы находятся в examples/browser-extension-lab/lesson-03/.

manifest.json

{
  "manifest_version": 3,
  "name": "Заметки читателя",
  "version": "0.1.0",
  "minimum_chrome_version": "102",
  "description": "Локальные заметки о самостоятельно выбранных страницах",
  "action": {
    "default_popup": "popup.html",
    "default_title": "Заметки читателя"
  },
  "background": {
    "service_worker": "worker.js",
    "type": "module"
  }
}

worker.js

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (sender.id !== chrome.runtime.id ||
      sender.url !== chrome.runtime.getURL("popup.html") ||
      message?.version !== 1 || message?.type !== "PING") return false;
  sendResponse({ ok: true, data: { alive: true } });
  return false;
});

Ожидаемый результат

Это ожидаемый договор для самостоятельного исследования; расширение не устанавливалось и не запускалось.

PING из собственного popup → {"ok":true,"data":{"alive":true}}
Произвольная другая команда не обрабатывается.
Ответ не означает постоянную жизнь worker или сохранение заметок.

Ответ подтверждает конкретное событие

sendResponse возвращает объект ok=true и data.alive=true. Ответ синхронный, поэтому listener возвращает false и не удерживает канал для асинхронной работы. В следующем договоре storage потребует ожидания, и тогда понадобится literal true вместе с sendResponse. Важно не смешивать оба случая: наличие Promise где-то в файле не означает, что listener автоматически поддержит поздний ответ во всех выбранных браузерных версиях.

Popup вызывает runtime.sendMessage и ожидает Promise. При успешном ответе строка состояния говорит, что обработчик отвечает. Это утверждение относится к одной завершённой команде, а не к вечной жизни worker. Через некоторое время контекст может завершиться, и браузер запустит новый для следующего события. Поэтому слово alive в данных не является флагом, который надо хранить как постоянное свойство расширения. Оно описывает текущий разговор.

Если sendMessage завершится отказом, popup показывает текст ошибки. Мы не объявляем причиной именно отсутствие сервера: сервера здесь нет. Возможны проблема пакета, недоступный получатель или закрытие контекста. Для точного самостоятельного исследования читатель смотрит, существовал ли worker.js и был ли обновлён manifest. Ошибка передачи отличается от известного предметного отказа, который позже вернётся как ok=false с сообщением нашей проверки.

Непостоянная память

Представьте добавление let notes=[] в worker. В течение одного запуска несколько команд могут увидеть элементы массива, поэтому первый ручной опыт создаёт ощущение работающего хранения. После завершения worker новый запуск снова создаст пустой массив. Это не восстановление из диска и не сохранение через lifecycle. Поэтому постоянные записи позже будут находиться в chrome.storage.local, а оперативная очередь Promise будет отвечать только за порядок текущих обработчиков.

Точно так же setTimeout не следует объявлять надёжным планировщиком долгой фоновой работы. Если контекст завершится, отложенный callback не станет гарантированным пользовательским напоминанием. Наш проект не создаёт reminders и не обещает автономное событие через часы. Он реагирует на явные действия popup и options. Если понадобятся другие события, нужно выбрать соответствующий API и объяснить его договор отдельно, а не держать worker искусственно занятым ради видимости вечного процесса.

Ожидаемый результат третьей ступени — при открытом popup появляется сообщение «Обработчик расширения отвечает», а черновик остаётся прежним локальным вводом. Никакие заметки ещё не сохраняются. Закрытие popup не превращает PING в сохранение его текста. Важно проговорить это читателю, поскольку общая фоновая роль часто воспринимается как место, где все значения автоматически переживают интерфейс. Фон и хранилище являются разными механизмами.

Как разбирать жизненный цикл

Полный worker.js и изменение manifest показаны на этой странице. Автор не устанавливал расширение и не наблюдал остановку worker, поэтому описанное поведение является ожидаемым по первичной документации. Для самостоятельного разбора можно проследить одну команду: popup формирует объект, браузер доставляет событие, listener проверяет sender и type, sendResponse возвращает JSON-данные, Promise обновляет строку состояния. На каждом этапе известна конкретная обязанность без общей переменной между контекстами.

Этот путь пригодится следующему результату — чтению выбранной вкладки. Worker не будет сам обращаться к document чужой страницы. Вместо этого scripting.executeScript запустит наш ограниченный content.js в изолированном контексте, а popup получит структурированный результат. Мы уже знаем, куда потом передать данные для проверки, и почему их нельзя оставлять в оперативной памяти как окончательно сохранённую заметку.

Worker также не должен получать свою готовность из переменной, которую когда-то выставил popup. Окно может закрыться, а новый фоновый контекст ещё не существовал в тот момент. Каждый обработчик должен иметь достаточные определения при собственном запуске. PING отвечает именно на текущее событие, поэтому не требует прежнего флага. Такой принцип пригодится storage: сохранённые записи читаются по запросу, а временная очередь создаётся заново, без предположения, что предыдущий экземпляр успел передать всю память следующему.

Документация и исходники

Механизмы главы сверены с первичными источниками: документация 1, документация 2, документация 3. Полный архив «Заметок читателя» содержит все двенадцать состояний. Установка, запуск и публикация не выполнялись.