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

Обновление в нескольких вкладках

До этого обновление ожидало завершения старых вкладок. Такой вариант безопасен для знакомства, но неудобен, когда читатель держит несколько окон долго. Добавим явное предложение переключиться на подготовленный worker. При этом не будем считать согласие в одной вкладке разрешением принудительно перезагрузить все остальные.

Есть два независимых обновления: новый worker оболочки и новый пакет статьи. Первый меняет обработку запросов и обязательные файлы; второй меняет выбранную сохранённую копию XML. Их версии не обязаны совпадать. В этой главе оболочка получает CACHE=pw-offline-v4, а XML-пакет может оставаться v1. Закладки IndexedDB версии2 продолжают храниться независимо.

Явный выбор

На главной подготовьте кнопку id="update-button" и сообщение id="update-status" с role=status. Модуль assets/update-ui.js вызывается после получения registration. Он наблюдает waiting и updatefound, а кнопку включает только для подготовленного ожидающего экземпляра. Вызов registration.update можно добавить как отдельное действие «Проверить обновление», сохраняя обработку его ошибки.

Следующая полная функция не содержит универсального автоматического reload. Перезагрузка связана с локальным подтверждением пользователя. Если controller изменился без такого подтверждения, текущая вкладка получает сообщение и отключает действия сохранения до обновления своего интерфейса. Используем data-reader-write на кнопках загрузки и записи, которым нужна актуальная версия приложения.

export function connectUpdates(registration, button, status) {
  let approved = false;
  let reloaded = false;
  const refresh = () => {
    button.disabled = !registration.waiting;
    if (registration.waiting) {
      status.textContent = "Новая оболочка подготовлена. Можно обновить вкладку";
    }
  };
  refresh();
  registration.addEventListener("updatefound", () => {
    const worker = registration.installing;
    worker?.addEventListener("statechange", refresh);
  });
  button.addEventListener("click", () => {
    const worker = registration.waiting;
    if (!worker) return refresh();
    approved = true;
    button.disabled = true;
    status.textContent = "Переключение оболочки";
    worker.postMessage({type: "ACTIVATE_READER_UPDATE"});
  });
  navigator.serviceWorker.addEventListener("controllerchange", () => {
    if (approved && !reloaded) {
      reloaded = true;
      location.reload();
      return;
    }
    document.querySelectorAll("[data-reader-write]").forEach(control => {
      control.disabled = true;
    });
    status.textContent = "Оболочка изменилась в другой вкладке. Сохраните своё состояние и обновите эту страницу";
    refresh();
  });
}

Для нового worker добавляется следующий обработчик сообщения. Команда явно названа и не пересекается с GET_READER_CACHE. Функция skipWaiting вызывается после решения пользователя, а не в install без условия. Не добавляйте clients.claim по привычке: захват ещё неуправляемых страниц имеет отдельные последствия и не требуется для этого ограниченного сценария существующих контролируемых вкладок.

self.addEventListener("message", event => {
  if (event.data?.type === "ACTIVATE_READER_UPDATE") {
    event.waitUntil(self.skipWaiting());
  }
});

Это сообщение не доказывает завершённую активацию. Наблюдаем controllerchange и состояние регистрации. Преждевременное выполнение location.reload прямо после postMessage могло бы снова открыть страницу под прежним worker. ServiceWorker.postMessage, skipWaiting.

Что происходит в соседнем окне

Представьте вкладки A и B. В A человек выбрал обновление; в B продолжает читать XML. Новый worker может стать активным для регистрации, и B получит изменение controller. Наш код не перезагрузит её без локального approved. Уже открытый HTML остаётся в DOM, а программа сообщает о необходимости обновить интерфейс перед новыми действиями записи.

Блокировка требует заранее назначить data-reader-write нужным кнопкам. Она не делает любую неизвестную старую страницу совместимой автоматически. Если вкладка содержит несохранённый текст, его нужно сохранить отдельно до подтверждения. Наш учебник не включает редактор заметок, поэтому код не обещает универсальное обнаружение незаписанных данных всех будущих форм.

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

Ресурсы прежнего HTML

Одного сохранения старого кеша недостаточно. Обработчик девятого урока ищет только выбранный пакет, поэтому после переключения указателя старый HTML может попросить ресурс прежней версии. Добавим ограниченный поиск только наших immutable assets, никогда произвольного HTML. Их versioned-имена обязаны сохранять прежнее содержание.

Полная функция ниже размещается в sw.js. В fetch-обработчике девятого урока вызовите её после блока выбранного пакета, перед обычным fetch: если возвращён Response, отдайте его. Ошибка отдельного описателя пропускается. Прежняя отдельная ветка catch для повреждённого указателя сохраняется: сбой метаданных не должен остановить обычную сеть/CORE.

async function findRetainedAsset(request) {
  const url = new URL(request.url);
  if (url.search || !/^\/assets\/(site\.v\d+\.css|xml\.v\d+\.svg)$/.test(url.pathname)) {
    return null;
  }
  for (const name of await caches.keys()) {
    if (!name.startsWith("pw-package-")) continue;
    try {
      const cache = await caches.open(name);
      const ready = await cache.match("/__offline-ready.json");
      if (!ready) continue;
      const plan = await ready.json();
      const item = plan.resources?.find(resource => resource.url === url.pathname);
      if (!item) continue;
      const response = await cache.match(request);
      if (response?.ok && (response.headers.get("content-type") ?? "").includes(item.type)) {
        return response;
      }
    } catch (error) {
      console.warn("Прежний ресурс не прочитан", name, error);
    }
  }
  return null;
}

Ожидаемый результат: старый XML с ссылкой на site.v1.css может получить точный прежний CSS из готового v1-пакета, даже если выбран v2. Обычный путь XML не ищется по всем кешам, поэтому старый HTML не подменяет текущий выбранный материал случайно. Неполный кандидат без ready тоже не становится источником ресурсов.

Вставляемый вызов внутри async-ветки fetch имеет следующий вид. Он располагается после обработки выбранного пакета и перед сетевым try. Функция вызывается только в уже проверенной ветке same-origin GET; поиск по чужим адресам не добавляется.

const retained = await findRetainedAsset(request);
if (retained) return retained;

Рассмотрим обратный случай: старая вкладка просит XML по постоянному адресу после смены выбранного пакета. Сам HTML при новой навигации будет выбран текущим pointer, а не номером прежней страницы. Это осознанное ограничение одного общего указателя. Независимое закрепление каждой вкладки за выпуском потребовало бы отображения client_id→package и обработки закрытых клиентов. Наш код такой функции не реализует и не обещает.

Вместе с этим правило immutable assets остаётся обязательным. Если редакция перезапишет site.v1.css новым содержимым под тем же URL, ограниченный поиск не способен определить, какая копия соответствует старому HTML. Версия в имени полезна только при сохранении договорённого смысла. Для строгой проверки содержимого можно отдельно развивать хеши, но название файла само не является криптографическим доказательством.

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

Когда можно очистить прежний набор

Не удаляйте старые пакеты сразу в activate. Другие вкладки могут сохранять HTML со старыми ссылками. Простой осторожный вариант — держать прежние ресурсы до явной уборки после закрытия старых окон. Более сложная система учитывает клиентов и выпуск каждого документа, но это отдельный протокол, которого маленький пример не реализует.

Указатель выбранного пакета общ для origin. Вкладки не получают независимый выбор только потому, что открыты раздельно. Поэтому сообщение о новом пакете должно предлагать новую навигацию и объяснять, какая копия будет открыта. Локальные закладки сохраняются по ID и не стираются при смене pointer или worker.

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