Переход на сервис-воркер

Замена фоновых страниц и страниц событий на сервис-воркер.

Сервис-воркер заменяет фоновую страницу или страницу событий расширения, чтобы фоновый код не выполнялся в основном потоке. Это позволяет расширениям работать только при необходимости, экономя ресурсы.

Фоновые страницы были фундаментальным компонентом расширений с момента их появления. Проще говоря, фоновые страницы обеспечивают среду, которая существует независимо от любого другого окна или вкладки. Это позволяет расширениям отслеживать события и реагировать на них.

На этой странице описаны задачи по преобразованию фоновых страниц в сервисные работники расширений. Дополнительную информацию о сервисных скриптах расширений можно найти в руководстве Обработка событий с помощью сервисных скриптов и разделе О сервисных скриптах расширений.

Различия между фоновыми скриптами и сервис-воркерами расширений

В некоторых контекстах сервисные рабочие процессы расширений называются фоновыми скриптами. Хотя сервисные работники расширений работают в фоновом режиме, называть их фоновыми скриптами несколько вводит в заблуждение, подразумевая идентичные возможности. Отличия описаны ниже.

Изменения на фоновых страницах

Сервисные работники имеют ряд отличий от фоновых страниц.

  • Они работают вне основного потока, то есть не мешают контенту расширения.
  • У них есть особые возможности, например перехват событий получения данных в источнике расширения, например из всплывающего окна на панели инструментов.
  • Они могут взаимодействовать с другими контекстами через интерфейс Clients.

Что нужно изменить

Вам потребуется внести несколько изменений в код, чтобы учесть различия в работе фоновых скриптов и сервисных скриптов. Для начала, способ указания сервисного работника в файле манифеста отличается от способа указания фоновых скриптов. Что ещё мы рекомендуем предпринять

  • Поскольку у них нет доступа к DOM или интерфейсу window, вам нужно будет перенести такие вызовы в другой API или в закадровый документ.
  • Прослушиватели событий не должны регистрироваться в ответ на возвращенные промисы или внутри обратных вызовов событий.
  • Поскольку они несовместимы с XMLHttpRequest(), вам нужно будет заменить вызовы этого интерфейса вызовами fetch().
  • Поскольку они завершают работу, когда не используются, вам нужно сохранять состояния приложения, а не полагаться на глобальные переменные. Принудительное завершение работы сервисных работников также может привести к остановке таймеров до их завершения. Вам нужно будет заменить их будильниками.

На этой странице подробно описаны эти задачи.

Обновите поле background в манифесте

В Manifest V3 фоновые страницы заменены на сервис-воркер. Изменения в манифесте перечислены ниже.

  • Замените "background.scripts" на "background.service_worker" в manifest.json. Обратите внимание, что поле "service_worker" принимает строку, а не массив строк.
  • Удалить "background.persistent" из manifest.json.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

Поле "service_worker" принимает одну строку. Поле "type" нужно только в том случае, если вы используете модули ES (с ключевым словом import). Его значение всегда будет "module". Подробнее об основах работы с сервис-воркерами расширений…

Перенесите вызовы DOM и window в документ вне экрана

Некоторым расширениям требуется доступ к DOM и объектам окна без визуального открытия нового окна или вкладки. Offscreen API поддерживает эти сценарии, открывая и закрывая скрытые документы, упакованные с расширением, не мешая пользователю. За исключением передачи сообщений, скрытые документы не используют API совместно с другими контекстами расширений, но функционируют как полноценные веб-страницы, с которыми могут взаимодействовать расширения.

Чтобы использовать Offscreen API, создайте скрытый документ из сервисного скрипта.

browser.offscreen.createDocument({
  url: browser.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

В документе вне экрана выполните любое действие, которое ранее выполнялось в фоновом скрипте. Например, вы можете скопировать текст, выбранный на главной странице.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

Обмен данными между фоновыми документами и сервис-воркерами расширений с помощью передачи сообщений.

Преобразование localStorage в другой тип

Интерфейс Storage веб-платформы (доступный из window.localStorage) нельзя использовать в сервисе-воркере. Чтобы устранить эту проблему, выполните одно из следующих действий: Во-первых, вы можете заменить его вызовами другого механизма хранения. Пространство имен browser.storage.local подходит для большинства случаев, но есть и другие варианты.

Вы также можете переместить вызовы в закадровый документ. Например, чтобы перенести данные, ранее хранившиеся в localStorage, в другой механизм:

  1. Создайте фоновый документ с процедурой преобразования и обработчиком runtime.onMessage.
  2. Добавьте в документ вне экрана процедуру преобразования.
  3. В сервис-воркере расширения проверьте свои данные в browser.storage.
  4. Если данные не найдены, создайте документ вне экрана и вызовите runtime.sendMessage(), чтобы запустить процедуру преобразования.
  5. В обработчике runtime.onMessage, который вы добавили в закадровый документ, вызовите процедуру преобразования.

Также есть некоторые нюансы, связанные с тем, как API веб-хранилища работают в расширениях. Подробнее о хранилище и файлах cookie…

Синхронная регистрация прослушивателей

Асинхронная регистрация прослушивателя (например, внутри промиса или обратного вызова) может не работать в Manifest V3. Рассмотрим следующий код.

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.browserAction.setBadgeText({ text: badgeText });
  browser.browserAction.onClicked.addListener(handleActionClick);
});

Это работает с постоянной фоновой страницей, потому что страница постоянно работает и никогда не инициализируется повторно. В Manifest V3 сервис-воркер будет повторно инициализирован при отправке события. Это означает, что при активации события прослушиватели не будут зарегистрированы (поскольку они добавляются асинхронно) и событие будет пропущено.

Вместо этого переместите регистрацию прослушивателя событий на верхний уровень скрипта. Это гарантирует, что Chrome сможет немедленно найти и вызвать обработчик кликов вашего действия, даже если расширение не завершило выполнение логики запуска.

browser.action.onClicked.addListener(handleActionClick);

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.action.setBadgeText({ text: badgeText });
});

Замените XMLHttpRequest() на глобальный fetch()

XMLHttpRequest() нельзя вызвать из сервисного работника, расширения или другим способом. Замените вызовы XMLHttpRequest() из фонового скрипта на вызовы global fetch().

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

Сохранение состояний

Сервис-воркеры недолговечны, то есть они могут запускаться, работать и завершать работу несколько раз за один сеанс браузера. Это также означает, что данные не сразу доступны в глобальных переменных, поскольку предыдущий контекст был удален. Чтобы обойти это ограничение, используйте Storage API в качестве источника достоверных данных. Рассмотрим пример.

В примере ниже для хранения имени используется глобальная переменная. В сервис-воркере эта переменная может сбрасываться несколько раз за сеанс браузера.

Фоновый скрипт Manifest V2
let savedName = undefined;

browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

browser.browserAction.onClicked.addListener((tab) => {
  browser.tabs.sendMessage(tab.id, { name: savedName });
});

В Manifest V3 замените глобальную переменную вызовом Storage API.

Сервис-воркер Manifest V3
browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    browser.storage.local.set({ name });
  }
});

browser.action.onClicked.addListener(async (tab) => {
  const { name } = await browser.storage.local.get(["name"]);
  browser.tabs.sendMessage(tab.id, { name });
});

Как преобразовать таймеры в будильники

Часто используются отложенные или периодические операции с помощью методов setTimeout() или setInterval(). Однако в сервисных рабочих процессах эти API могут не работать, поскольку таймеры отменяются при завершении процесса.

Фоновый скрипт Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

Вместо этого используйте Alarms API. Как и другие прослушиватели, прослушиватели будильников должны быть зарегистрированы на верхнем уровне скрипта.

Сервис-воркер Manifest V3
async function startAlarm(name, duration) {
  await browser.alarms.create(name, { delayInMinutes: 3 });
}

browser.alarms.onAlarm.addListener(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
});

Как поддерживать работу сервисного работника

Сервисные работники по определению управляются событиями и завершают работу при бездействии. Это позволит Chrome оптимизировать производительность и потребление памяти расширением. Подробнее о жизненном цикле сервисного работника… В исключительных случаях могут потребоваться дополнительные меры, чтобы сервисный работник оставался активным дольше.

Как поддерживать работу service worker до завершения продолжительной операции

Если сервисный работник выполняет длительные операции, не вызывающие API расширений, он может завершить работу до их окончания. Примеры:

  • Запрос fetch() может занять больше пяти минут (например, при скачивании большого объема данных по медленному каналу связи).
  • Сложное асинхронное вычисление, которое занимает более 30 секунд.

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

В следующем примере показана вспомогательная функция waitUntil(), которая поддерживает работу сервисного работника до тех пор, пока не будет выполнено заданное обещание:

async function waitUntil(promise) {
  const keepAlive = setInterval(browser.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Как поддерживать непрерывную работу сервисного работника

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

Чтобы сервис-воркер не отключался, используйте следующий фрагмент кода:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await browser.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}