Кэширование кода в веб-приложениях
- вторник, 5 марта 2024 г. в 00:00:15
Эта статья - изложение персонального опыта работы с кэшем на стороне браузера при создании веб-приложений. В повседневной разработке я использую десктопный Chrome. У него есть панель инструментов и он в принципе удобен для разработчика. Но когда нужно проверять приложение на смартфонах, начинается геморрой - каким образом доставить на смартфон новый код, если там уже есть старый? Больше всего меня бесит Safari on iPhone. Если в Chrome есть возможность удалить все данные для отдельного сайта, то в iPhone все данные удаляются для всего Safari. Если и есть в iPhone какой-нибудь способ удалить через конфигурацию смартфона/приложения данные для отдельного сайта, то мне так и не удалось его найти. Буду благодарен, если кто-либо мне о нём сообщит в комментах.
Что такое веб-страничка? Это прежде всего данные. HTML - это язык разметки данных. А что такое веб-приложение? Это код, который обрабатывает данные и формирует веб-странички уже в самом браузере. В веб-приложении кэширование данных подчиняется тем же правилам, что и кэширование веб-страниц - зависит от бизнес-логики. Что-то мы можем кэшировать в браузере на подольше, а что-то нужно запрашивать с сервера каждый раз. С кэшированием кода же всё просто - код веб-приложения должен сохраняться в браузере до тех пор, пока на сервере не изменится версия этого веб-приложения (вернее, не изменится версия фронтальной части веб-приложения).
Для отслеживания соответствия версии файла в браузере версии файла на сервере используются HTTP заголовки Etag
и If-None-Match
. Вместе с содержимым ресурса (страницы, изображения, видео и т.д.) сервер передаёт в браузер в заголовке ETag
некую метку, соответствующую содержимому ресурса (версию или хэш, вычисленный по содержимому ресурса):
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600
<!doctype html>
…
Эта метка передаётся браузером на сервер в запросе к ресурсу в заголовке If-None-Match
:
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "33a64df5"
Если ресурс на сервере не изменился, то сервер просто возвращает 304 Not Modified
и браузер использует имеющуюся у него копию ресурса из кэша. Если у нас весь код приложения находится в одном или нескольких бандлах, то это вполне себе рабочий способ. Если, конечно, смириться с тем, что на время max-age=3600
браузер не станет запрашивать изменения с сервера, даже если они там есть. Для обычных пользователей веб-приложения такая схема может считаться рабочей, но для разработчика это явно не подходит. Для разработчика Cache-Control
должен стоять max-age=0
(или no-cache
).
Что делать, если в веб-приложении множество файлов? Не один-два больших бандла и десяток картинок, а несколько сотен файлов с кодом приложения и сопутствующим медиа? В этом случае на помощь приходят Service Worker и Cache Storage - первый позволяет перехватывать обращения браузера за внешним ресурсом, а второй позволяет сохранять ресурс в браузере.
Если загрузить весь исходный код в Cache Storage, то вся логика за обработку обращений к ресурсам ляжет на плечи Service Worker’а. И там уже не важно, что сервер запрещает кэширование ресурсов через заголовки ответа - Service Worker всё равно может брать ответы из Cache Storage и возвращать обратно без выхода в Сеть. Именно так и организована поддержка работы веб-приложений в offline-режиме.
Для своих экспериментов с кэшем я сделал демо-приложение (вот исходный код):
no_cache/
: сервер возвращает текущее время и HTTP-заголовок Cache-Control: no-store, no-cache, must-revalidate, private
.
cached/
: сервер возвращает текущее время и HTTP-заголовок Cache-Control: public, max-age=60
.
sw/
: аналогично режиму Cached, но при этом данные сохраняются Service Worker’ом в Cache Storage, который очищается по кнопке “Clear Cache” на результирующей странице.
Можно использовать эту демку для того, чтобы посмотреть, как различные браузеры на различных устройствах работают с кэшем - видно по приходящему с сервера timestamp’у.
Таким образом на данный момент я пришёл к следующей схеме работы с кодом в веб-приложении:
Все ресурсы сервера, относящиеся к коду, передаются с заголовками, в явном виде запрещающими кэширование.
Service Worker самостоятельно кэширует все ответы в Cache Storage.
При разработке используется десктопная версия браузера, в которой есть возможность затребовать от Service Worker’а обращаться в Сеть каждый раз за новой версией ресурса (DevTools / Application / Service workers / Bypass for networks).
Для проверки работы приложения на смартфонах в приложении самостоятельно реализуется опция принудительной очистки разработчиком Cache Storage через UI.
Для обычных пользователей реализуется “инсталляция приложения” - при запуске приложения на фронте проверяется текущая версия установленного приложения и текущая версия на сервере. Если версии не совпадают, то с сервера в виде единого файла закачиваются все исходные файлы новой версии, распаковываются и сохраняются в Cache Storage.
Спасибо за прочтение и хорошего дня!
P.S. Телеграм-канала у меня нет, а то бы я по свежескладывающейся традиции обязательно добавил бы ссылку на него :)