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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В 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 и объектам окна без визуального открытия нового окна или вкладки. API Offscreen поддерживает эти сценарии использования, открывая и закрывая неотображаемые документы, входящие в состав расширения, без нарушения пользовательского опыта. За исключением передачи сообщений, документы Offscreen не используют общие API с другими контекстами расширений, а функционируют как полноценные веб-страницы, с которыми могут взаимодействовать расширения.

Для использования API Offscreen создайте документ Offscreen из сервис-воркера.

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);
});

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

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

chrome.action.onClicked.addListener(handleActionClick);

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

Замените функцию XMLHttpRequest() на глобальную функцию fetch().

Вызов XMLHttpRequest() нельзя осуществить из сервис-воркера, расширения или иным образом. Замените вызовы XMLHttpRequest() из вашего фонового скрипта вызовами глобальной 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 хранилища в качестве источника достоверной информации. Пример покажет, как это сделать.

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

Фоновый скрипт Manifest 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 .

Работник сервиса Manifest 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 могут давать сбой в сервис-воркерах, поскольку таймеры отменяются при завершении работы сервис-воркера.

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

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

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

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

Не дайте работнику службы остаться в живых.

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

Обеспечьте безопасность работника сферы услуг до завершения длительной операции.

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

  • Запрос 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'];
}