Миграция на сервисного работника

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

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

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

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

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

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

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

Service Workers имеет ряд отличий от фоновых страниц.

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

Изменения, которые вам нужно будет внести

Вам потребуется внести несколько изменений в код, чтобы учесть различия в работе фоновых сценариев и сервис-воркеров. Начнем с того, что способ указания сервисного работника в файле манифеста отличается от способа указания фоновых сценариев. Кроме того:

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

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

Обновите поле «фон» в манифесте.

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

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

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

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

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

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

chrome.offscreen.createDocument({
  url: chrome.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 ) нельзя использовать в сервисном работнике. Чтобы решить эту проблему, сделайте одно из двух. Во-первых, вы можете заменить его вызовами другого механизма хранения. Пространство имен chrome.storage.local подходит для большинства случаев использования, но доступны и другие варианты .

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

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

Также есть некоторые нюансы работы API веб-хранилищ в расширениях. Узнайте больше в разделе «Хранение и файлы cookie» .

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

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

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

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

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

chrome.action.onClicked.addListener(handleActionClick);

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

Замените XMLHttpRequest() на глобальную выборку().

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);
принести()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

Сохраняться состояния

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

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

Фоновый сценарий манифеста V2
let savedName = undefined;

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

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

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

Сервисный работник манифеста V3
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

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

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

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

Фоновый сценарий манифеста V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

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

Сервисный работник манифеста V3
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

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

Поддерживайте жизнь сервисного работника

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

Поддерживать работоспособность сервисного работника до завершения длительной операции

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

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

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

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

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.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 chrome.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 chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}