frontend · advanced · 6d
Офлайн-синхронизация PWA
Notes PWA с офлайн-приоритетом: локальная очередь записей (IndexedDB), которая синхронизируется при переподключении через last-writer-wins, service worker для кеширования ресурсов и background sync для пропущенных записей.
Результат
Приложение для заметок, полностью работающее офлайн, складывающее правки в IndexedDB и автоматически синхронизирующее их с сервером при переподключении без потери данных.
Этапы
0/3 · 0%- 01Локальная очередь записи
Перехвати все чтения и записи через очередь в IndexedDB и отрисуй UI из локального состояния — приложение должно работать при DevTools в режиме Offline.
Критерии готовности- Все чтения/записи идут через очередь в IndexedDB, а UI рендерится из локального состояния; приложение работает при DevTools в режиме Offline.
- Запись, сделанная офлайн, переживает перезагрузку (всё ещё в очереди, не потеряна).
- 02Кэширование service worker
Добавь service worker со стратегией stale-while-revalidate для оболочки и cache-first для ресурсов.
Критерии готовности- Оболочка приложения грузится из service worker офлайн; оболочка — stale-while-revalidate, статика — cache-first.
- Новый деплой обновляет закэшированную оболочку при следующем визите без жёсткого обновления.
- 03Синхронизация с LWW при переподключении
Сбрасывай очередь записей при переподключении, разрешай конфликты через last-writer-wins (сравни server_updated_at с local_updated_at) и регистрируй Background Sync tag как запасной вариант.
Критерии готовности- При переподключении очередь сливается, конфликты решаются last-writer-wins по сравнению server vs local updated_at; ни одна правка молча не теряется.
- Тег Background Sync сливает очередь, даже если вкладка была закрыта в момент переподключения.
Рубрика
| Джуниор | Миддл | Сеньор | |
|---|---|---|---|
| Стратегия кэширования service worker | Service worker зарегистрирован и перехватывает fetch, но кэширование единообразно: одна стратегия для оболочки, API-ответов и статики. | Оболочка использует stale-while-revalidate (всегда быстро, обновляется фоном), статика — cache-first (неизменяемые хэшированные имена файлов), а новый деплой вызывает замену кэша при следующем визите без жёсткого обновления. | Ты можешь назвать окно устаревшего контента для каждой стратегии и объяснить, почему жизненный цикл service worker (install → waiting → activate) делает тайминг обновления неочевидным: новый воркер ждёт закрытия всех вкладок перед захватом, а skipWaiting меняет компромисс с «устаревший до перезапуска» на «потенциальная рассогласованность в середине сессии». |
| Надёжность очереди записи и фоновая синхронизация | Правки хранятся в памяти офлайн; перезагрузка до переподключения теряет их. | Очередь записей живёт в IndexedDB и переживает перезагрузки; при переподключении очередь сбрасывается на сервер, и ни одна правка не теряется молча. | Тег Background Sync регистрируется при записи, чтобы сброс сработал даже если вкладка была закрыта в момент переподключения; ты можешь назвать оговорку по поддержке браузеров (частичная поддержка Safari) и поведение при отсутствии Background Sync. |
| Разрешение конфликтов при переподключении | При переподключении локальная запись молча затирает сервер, или сервер побеждает без проверки меток времени — данные одной из сторон исчезают незаметно. | Конфликты разрешаются сравнением server_updated_at и local_updated_at (last-writer-wins); ни одна правка не выбрасывается молча — проигравшая версия хотя бы логируется. | Ты можешь указать, где LWW на уровне документа ломается (два пользователя редактируют разные поля одной заметки; более поздний сброс затирает правку другого) и объяснить слияние на уровне полей из стретча — изменения разных полей никогда не перезаписывают друг друга, потому что ключ слияния — поле + метка времени, а не документ + метка времени. |
Эталонный разбор (спойлер)
Выбор стратегии кэша: stale-while-revalidate подходит для оболочки, потому что пользователи всегда получают быстрый ответ, а окно устаревания (один визит) приемлемо; cache-first подходит для версионированной статики (JS/CSS с хэш-именами файлов), потому что она по-настоящему неизменяема. Cache-first для оболочки грозит бесконечной раздачей устаревшей версии при сбое логики обновления.
Тайминг обновления service worker: новый воркер ждёт в состоянии 'installing', пока все открытые вкладки не выгрузятся, затем переходит в 'active'. skipWaiting форсирует немедленную активацию, но рискует рассогласованием в середине сессии, если старый и новый кэши различаются. Правильный выбор зависит от того, вызывает ли несовпадение версий жёсткий сбой или только устаревший UI.
LWW на уровне документа против поля: last-writer-wins по updated_at чисто разрешает конкурентные сбросы, когда каждая заметка принадлежит одному пользователю. Молча теряет работу, когда два пользователя редактируют разные части одного документа, потому что более поздний сброс заменяет весь документ. Слияние на уровне полей — ключевание разрешения конфликтов по идентичности поля, а не документа — предотвращает это без полноценного CRDT.
Режимы отказа Background Sync: браузер запускает sync-событие, когда считает сеть доступной, но не гарантирует доставку в конкретном временном окне. Неудачная синхронизация повторно ставится в очередь с экспоненциальной паузой, управляемой браузером. Если браузер убьёт service worker до завершения синхронизации, тег остаётся зарегистрированным и будет повторён — но только если браузер поддерживает Background Sync; частичная реализация Safari означает, что очередь должна сбрасываться и при переходах page-visible как запасной вариант.
Сделай по-сеньорски
- Замени last-writer-wins на слияние на уровне полей: изменения в разных полях одной заметки не затирают друг друга.
- Добавь стек undo, работающий через переходы офлайн/онлайн без порчи очереди синхронизации.