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

Обновление офлайн-учебника

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

До этого урока выбранный кеш назывался pw-offline-v2. Следующее учебное состояние получает pw-offline-v3. Изменяется константа в worker, но этого недостаточно: app.js тоже должен понимать, куда сохранять главу. Новая страница, старый controller и ожидающий worker могут существовать одновременно. Это обычная часть жизненного цикла, а не исключительный сбой.

Подготовка не равна переключению

Worker устанавливается и готовит обязательные ответы. После успешной установки новый экземпляр может ждать, пока старый перестанет управлять открытыми страницами. Надпись «Новая версия подготовлена» поэтому отличается от «Эта вкладка уже обновлена». В третьем уроке мы наблюдали состояния; теперь используем это различие при принятии решения. Жизненный цикл service worker.

В базовом сценарии не вызываем skipWaiting и clients.claim автоматически. Закрытие старых вкладок и новое открытие — один понятный способ перейти к следующему экземпляру, когда выполнены условия браузера. Позднее предложим явную кнопку обновления. Пока цель состоит в том, чтобы не потерять работающий выпуск при неудачной подготовке.

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

Спрашиваем управляющий worker

Жёсткая константа v3 в странице может указать не на тот экземпляр, который обрабатывает её запросы. Добавим запрос имени кеша через MessageChannel. Следующий полный модуль assets/reader-cache.js возвращает ответ именно текущего controller. При его отсутствии или отсутствии ответа операция завершается ошибкой, а не выбирает случайную версию.

export function getReaderCacheName() {
  return new Promise((resolve, reject) => {
    const worker = navigator.serviceWorker?.controller;
    if (!worker) return reject(new Error("Вкладка ещё не управляется worker"));
    const channel = new MessageChannel();
    const timer = setTimeout(() => {
      channel.port1.close();
      reject(new Error("Версия управляющего worker не получена"));
    }, 3000);
    channel.port1.onmessage = event => {
      clearTimeout(timer);
      channel.port1.close();
      const value = event.data?.cacheName;
      if (typeof value !== "string" || !/^pw-offline-v\d+$/.test(value)) {
        return reject(new Error("Неизвестный формат имени кеша"));
      }
      resolve(value);
    };
    worker.postMessage({type: "GET_READER_CACHE"}, [channel.port2]);
  });
}

Следующий фрагмент добавляется в sw.js после объявления CACHE. Для нового состояния эта константа равна v3. Сообщение содержит только имя учебного кеша; оно не несёт секретов и не является механизмом авторизации. В дальнейшем новые команды получат отдельные названия, чтобы запрос версии не запускал обновление случайно.

self.addEventListener("message", event => {
  if (event.data?.type === "GET_READER_CACHE" && event.ports[0]) {
    event.ports[0].postMessage({cacheName: CACHE});
  }
});

В app.js замените фиксированную константу импортом функции и получением имени через await. Ошибку получения версии обработайте в общем начале интерфейса: оставьте обычные ссылки и покажите «Офлайн-сохранение пока недоступно; откройте страницу после завершения обновления». Кнопки сохранения создаются только после успешного ответа. Не прячьте исключение за надписью «Сохранено».

Модуль reader-cache.js входит в обязательный набор CORE нового worker с MIME-группой javascript. Это ещё одна зависимость главной. Пока старый v2 не поддерживает команду, новая функция получит timeout; первый переход на протокол требует новой управляемой навигации после завершённого обновления. Такое ограничение явно лучше предположения, что ожидание ready означает нужную версию controller.

Выбранные главы и новый выпуск

Кеш v3 начинает с обязательного набора. Markdown, сохранённый в v2, не появляется в нём автоматически. Это не означает, что прежняя копия исчезла: мы пока не удаляли старый кеш. Но новая версия интерфейса не должна искать произвольный HTML во всех старых выпусках и объявлять его совместимым с новым оформлением.

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

В нашей базовой ветке после перехода на v3 читатель снова выбирает необязательный Markdown. Интерфейс честно покажет отсутствие копии текущего выпуска. Перед очисткой v2 нужно убедиться, что в нём нет единственного нужного пользователю набора. Политика сохранения и удаления должна учитывать это отдельно от технической активации worker.

Отказ подготовки

Представьте, что v3 загрузил главную, но не получил app.js. Установка должна завершиться неуспешно; активный v2 продолжает свою работу. Частичные записи кандидата могут остаться. Наличие имени pw-offline-v3 среди ключей кеша поэтому не означает готовность. Это важное ожидание для будущей матрицы сценариев.

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

Отдельно продумайте повторный отказ одного кандидата. Если имя v3 уже содержит частичные ответы, следующая попытка не должна доверять их наличию вместо подготовки. Базовый цикл получает обязательные файлы заново и дожидается каждой записи. Для более сложного пакета понадобится явное состояние ready и собственный список ресурсов. Частичный набор можно удалить позже, но решение об уборке не должно опираться только на больший номер в имени. Номер удобен человеку; фактическая готовность подтверждается успешной последовательностью операций.

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

Регистрация браузером также не обязана моментально обнаружить каждое изменение серверного файла. Для будущего ручного сценария предусмотрен метод registration.update, но вызов не гарантирует успешную загрузку и активацию. Подготовка, ожидание и управление вкладкой всё равно наблюдаются отдельно. ServiceWorkerRegistration.update.

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