Без сети: оболочка, запасная страница и честный onLine
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Со второй загрузки доской управляет воркер: код и стили отдаёт кеш, страницу он просит у сети, данные показывает старыми и чинит фоном. Главная страница переживёт даже метро: «сначала сеть» откатится на её копию в кеше. А вот карточка задачи, которую человек ещё не открывал, кончится без сети страницей ошибки браузера: что запрос за страницей особенный, наш маршрутизатор не знает. В этой статье разберёмся, что входит в оболочку приложения, а доска получит свою ветку для навигации, запасную страницу вместо динозавра и честный ответ на вопрос «есть ли связь».
Чтобы доска открылась без сети, всё нужное должно оказаться в кеше заранее. Набор файлов, из которых страница поднимается и показывает хоть что-то, называют оболочкой приложения: каркас разметки, стили, код, шрифты и иконки интерфейса. Данные в оболочку не входят — они устаревают и приезжают своей дорогой.
Кладут оболочку в install, и это не случайное место: установка идёт ровно тогда, когда страница открылась и скачала воркера, то есть сеть точно есть. По той же причине туда кладут и запасную страницу — ту, которую мы покажем вместо динозавра: когда она понадобится, скачивать её будет уже неоткуда.
| Что | В оболочку? | Почему |
|---|---|---|
Каркас страницы (/ или /index.html) | да | без него открывать нечего |
| Код и стили с хешем в имени | да | адрес уникален для версии, старое по нему не прилипнет |
| Запасная страница | да | без сети её уже не достать |
| Шрифты и иконки интерфейса | да, если их немного | оболочка должна оставаться короткой: один 404 рушит всю установку |
Данные (/api/…) | нет | устареют раньше, чем пригодятся; их кладёт в кеш стратегия на лету |
Вот sw.js доски. Главных правок с прошлой статьи три: в списке оболочки появилась запасная страница, у обработчика fetch — первая ветка, для навигации, а ветка «всё остальное — сначала сеть» ушла: страницы теперь ловит навигация, а прочим редким запросам воркер не нужен.
Сама запасная страница — обычный файл на сервере, и чем она проще, тем лучше: каждая её картинка и каждый шрифт тоже должны лежать в кеше, иначе без сети она откроется голой.
Ссылка на доску здесь не для красоты: доска-то в кеше лежит, и человеку есть куда уйти с тупиковой страницы. Стили берутся те же, с хешем в имени, — они в оболочке, значит, запасная страница не будет выглядеть как документ из девяностых.
Что происходит при первой загрузке, видно в журнале сервера:
Первые шесть строк — обычная загрузка: страница, код, стили, данные, favicon.ico с честным 404 и sw.js, который запросила регистрация. А дальше те же файлы едут по второму разу, и с ними offline.html, которого страница не просила ни разу. Это addAll в install набивает кеш: запасная страница скачана заранее, в этом весь смысл.
Соблазн простой: спросить у браузера, есть ли сеть, и дальше плясать от ответа. Свойство такое действительно есть, и в песочнице оно настоящее. Прогоним его рядом с поддельной сетью, которую мы сами же и выключили.
Две строки — и обе правдивы. navigator.onLine отвечает на вопрос про интерфейс: подключена ли машина хоть к какой-нибудь сети. За этим подключением может быть роутер без интернета, гостиничный Wi-Fi с формой входа, упавший сервер, сломанный DNS или прокси, который режет ваш домен, — во всех случаях свойство скажет true, а запрос не дойдёт.
Обратное значение надёжнее, но и оно не приговор. Как решать, «в сети» ли машина, каждая система придумывает сама: в Windows, например, это значит «удалось достучаться до проверочного сервера Microsoft», и файрвол или VPN, которые его режут, дают false при работающем интернете. А до вашего собственного localhost запрос доходит и вовсе без сети. Поэтому флаг годится на подсказку — значок в углу, надпись «похоже, связи нет», — но не на решение: решает ответ на настоящий запрос.
Надпись «нет сети» ставят после неудачного запроса, а не по значению флага. Иначе получается интерфейс, который врёт в обе стороны: обещает работу там, где её нет, и пугает пользователя там, где всё в порядке.
Браузер сообщает о смене состояния событиями online и offline на window. Настоящие события шлёт он сам, когда интерфейс появляется или пропадает; здесь мы отправляем их руками через dispatchEvent — тем же приёмом, каким в статье про хранилище изображали событие storage от соседней вкладки.
Свойство не поменялось ни разу, и это важнее самого примера: событие мы подделали, а navigator.onLine подделке не подчиняется — его ставит браузер по состоянию интерфейса. У настоящего offline в обработчике было бы false. Годятся эти события ровно на то же, на что и флаг: обновить подсказку в интерфейсе и лишний раз попробовать отправить накопившееся, когда связь, похоже, вернулась.
В прошлой статье мы сказали: запись в кеше живёт, пока её не удалит ваш код. Пора добавить вторую половину: или пока браузер не выселит данные сайта целиком.
Про своё хранилище можно спросить у navigator.storage: persisted() — защищено ли оно от выселения, persist() — просьба защитить, estimate() — сколько занято и сколько разрешено. В песочнице учебника все три отклоняются TypeError (происхождение непрозрачное, хранилища нет), поэтому дальше — листинг с выводом, снятым на доске с localhost.
Просьбу отклонили. Ничего личного: профиль чистый, сайт открыт второй раз в жизни. Chrome решает по своим признакам — установлено ли приложение на устройство, в закладках ли сайт, разрешены ли ему уведомления, часто ли им пользуются. Safari с 17-й версии тоже решает сам, и один из его признаков — открыт ли сайт как приложение с экрана Домой. Firefox вместо эвристики спрашивает разрешение у пользователя. Общее правило одно: persist() — это просьба, а не команда, и отказ — нормальный ответ, к которому код обязан быть готов.
Восемь с небольшим килобайт — вся наша оболочка вместе с копией данных. Число в мегабайтах — оценка того, сколько браузер готов отдать этому происхождению; оно зависит от свободного места и от браузера, у вас будет другое. Дело не в цифре, а в том, что она конечна: когда на диске становится тесно, браузер начинает выселять сайты, и первым уходит тот, к кому давно не заходили.
Жёстче всех к забытым сайтам WebKit. Данные, записанные скриптом сайта в Safari, — localStorage, IndexedDB, кеши воркера и саму его регистрацию — он удаляет после семи дней пользования браузером без взаимодействия с сайтом, то есть без единого нажатия или касания на его страницах. Считаются дни, когда человек работал в Safari, а не календарные; это часть защиты от межсайтового отслеживания, а она включена по умолчанию. У сайта, поставленного на домашний экран, счёт дней свой, отдельный от Safari: пока приложением пользуются, счётчик не набегает и данные остаются на месте.
Из этого следует вещь, которую стоит запомнить раньше, чем начнёте обещать заказчику «работает без сети». Выселение уносит не только кеш, но и регистрацию воркера — то есть сам механизм. Сайт после этого не «показывает старые данные», он просто снова требует сеть, как обычная страница. Надёжный офлайн на iPhone получается у приложения на экране Домой, а не у вкладки; как туда попасть, разберём в статье про манифест и установку.
И последнее, чего доска пока не умеет, — работать без сети на запись. Пока сети нет, новую карточку некуда отправить, и путей два. Отложить отправку до возвращения связи умеет Background Sync, но он есть только в браузерах на Chromium, и строить на нём единственную дорогу нельзя. Значит, остаётся второй путь: складывать изменения в очередь у себя и досылать их самому, разбираясь с порядком, повторами и чужими правками. Это отдельная большая тема, и разберём мы её там, где без неё совсем никак, — в пятой части, про мобильные приложения.
Доска открывается без сети, показывает запасную страницу вместо динозавра и не верит флагу onLine. Осталась беда, которая приходит вместе с хорошей новостью: вы выпустили новую версию. Ветка навигации и запасная страница уезжают в следующую статью готовыми — там выясним, почему новый воркер неделями не может сменить старого и чем за спешку берёт skipWaiting.
- Навигационный запрос делает браузер, а не код: у него
mode === "navigate", и вручную такойRequestне собрать. Если воркер не вызвал для негоrespondWith, браузер едет в сеть сам — и без сети показывает свою страницу ошибки, каким бы полным ни был кеш. - Оболочка приложения — каркас страницы, код, стили и запасная страница — кладётся в кеш в
install: это единственный момент, когда сеть заведомо есть. - Ветка навигации: сначала сеть с тайм-аутом, потом тот же адрес из кеша, потом общая запасная страница. Если не нашлось ничего,
respondWithполучаетundefined— и это ошибка, а не возврат в сеть: отдать запрос браузеру можно, только не вызвавrespondWithвовсе. navigator.onLineотвечает про подключение к сети, а не про связь с сервером:trueничего не обещает, да иfalseбывает ложным. Флаг — подсказка для интерфейса, а решение принимают по результату запроса.- Кеш живёт, пока его не удалит ваш код — или пока браузер не выселит данные сайта. Выселение уносит и кеши, и регистрацию воркера; Safari чистит забытые сайты после семи дней пользования браузером, а у приложения на домашнем экране счёт дней свой.
persist()— просьба, отказ нормален. - Запись без сети — отдельная задача: Background Sync есть только в Chromium, и очередь изменений приходится вести самому.
1/4Воркер ловит запросы к /assets/ и /api/, оболочка в кеше. Сети нет, пользователь перезагружает страницу. Что он увидит?
2/4navigator.onLine вернул true. Что из этого следует?
3/4Почему запасную страницу кладут в кеш при install, а не тогда, когда сеть пропала?
4/4Браузер выселил данные сайта. Что потерялось?
Запасная страница
Ветка навигации написана до половины: сеть не ответила — ищем в кеше тот же адрес, нашли — отдаём. А если не нашли, функция сдаётся и уступает браузеру, хотя запасная страница лежит в кеше рядом (её положили при установке вместе с доской).
Допишите ветку так, чтобы вместо капитуляции отдавалась запасная страница. Порядок проверок менять нельзя: свой адрес важнее общей заглушки. Сейчас вторая строка вывода говорит, что страницу рисует браузер; она должна говорить про запасную.
Запасная страница лежит в кеше под своим адресом, значит, и достаётся она обычным match — просто запрос для него приходится собрать самому.
Вторая строка вывода становится такой: /task/17 → сети нет, но доска сохранена | запасная страница. Последнюю строку с капитуляцией не выбрасывайте: запасной страницы может не оказаться в кеше — например, установка сорвалась и воркер поднялся на старой версии оболочки. В настоящем воркере эта ветка означает, что respondWith получит undefined и браузер покажет свою ошибку — причём ошибкой всё кончится и при живой сети: назад в сеть браузер запрос, уже отданный воркеру, не повезёт.
Честное «связь есть»
Доска собирается отправить накопившиеся правки на сервер и спрашивает у canSync, стоит ли пробовать. Сейчас ответ строится на одном navigator.onLine, поэтому вторая строка вывода врёт: сервер молчит, а функция обещает связь.
Перепишите canSync так, чтобы решение принималось по результату запроса, а не по флагу. Пробный запрос уже написан — это ping; он бросает исключение, если сеть не ответила за 200 мс.
Флаг из решения уходит совсем. Ложным бывает не только его true, но и false, и отсечка по нему заперла бы синхронизацию у человека, у которого за VPN всё работает.
Теперь вторая строка — false. В настоящем приложении у такой проверки есть цена: это лишний запрос перед каждой отправкой. Поэтому пробный запрос обычно не делают вовсе, а просто отправляют данные и смотрят на результат — неудача и есть ответ «связи нет». Отдельный ping оправдан там, где до отправки нужно показать состояние: кнопку, значок, надпись в углу.