Поле в лифте, анкета в самолёте, отчёт из склада с мёртвым вайфаем: сеть уходит в самый неподходящий момент, а работа остаётся. Классический сайт в этой ситуации проигрывает молча: кнопка отправки крутится, форма висит, и человек теряет двадцать минут ввода. Прогрессивное приложение обязано переживать разрыв сети как штатную ситуацию, и формы это первое место, где офлайн из обещания превращается в механику.
Рабочая схема складывается из четырёх слоёв: проверка значений прямо в браузере, черновики и очередь в локальном хранилище, фоновая отправка при возвращении сети и честный интерфейс, который всегда говорит человеку, что происходит с его работой. Ниже каждый слой разобран с кодом, ограничениями и типовыми ловушками, от ненадёжного флага соединения до двойной отправки одного отчёта.
Зачем формам жизнь без сети и какие сценарии этого требуют
Первый сценарий это мобильный: курьер, мерчендайзер, инженер на объекте работают в зоне слабого покрытия, и обрывы случаются по десять раз за смену. Форма приёма товара или акта осмотра обязана заполняться без оглядки на сеть, а отправляться сама, когда покрытие вернётся. Второй сценарий это длинные анкеты: заявка на сорок полей, которую заполняют час, не должна зависеть от стабильности линии в момент финального нажатия.
Третий сценарий это экономия: черновик, сохранённый локально, переживает случайную перезагрузку вкладки, севшую батарею и случайное закрытие окна. Человек, вернувшийся к анкете и увидевший свои значения на месте, это сэкономленный час и сохранённое доверие к продукту.
Четвёртый сценарий это узкие места приложения для мероприятий и полевых работ: офлайн-режим снимает требование к стабильности сети и позволяет вывозить продукты туда, где сети нет вообще, на склады, в цеха, в экспедиции. Форма без сети это не компромисс, а дверь в класс задач, недоступных классическим сайтам.
Общее у всех сценариев одно: сеть считается необязательной. Продукт, в котором офлайн обработан как исключение, будет сыпаться в полях и лифтах, а продукт, в котором офлайн это первый класс, просто работает, и пользователь даже не замечает, что соединения не было.
Локальная валидация без сервера от атрибутов до собственных правил
Браузер умеет проверять формы сам, и это фундамент офлайн-валидации. Атрибуты обязательности, формата почты, шаблона, границ числа и длины строки дают первый эшелон: разметка описывает правила, движок браузера проверяет их при вводе и отправке, а метод проверки пригодности формы сообщает годность одной строкой.
const form = document.querySelector("#report");
form.addEventListener("submit", (e) => {
if (!form.checkValidity()) {
e.preventDefault();
form.reportValidity();
return;
}
queueReport(new FormData(form));
});
Второй эшелон это собственные правила на скриптах: сверка контрольных сумм, зависимость одного поля от другого, проверка по справочникам, которые не жалко положить в браузер. Валидация здесь живёт в тех же функциях, что и на сервере, потому что правила обязаны совпадать, и расхождение проверок это баг, а не свойство. Готовые библиотеки описывают правила схемой и запускают их на обеих сторонах, и это дешевле, чем поддерживать два набора руками.
Третий эшелон это подсказки вместо отказов: сообщение рядом с полем, объяснение правила человеческим языком, пример корректного значения. Проверка, которая ругается после отправки всей формы, экономит код и тратит человека, а проверка на каждый ввод символа экономит человека и требует аккуратности с противозвёздами, чтобы не ругаться на каждый недописанный символ.
Файлы проверяются до очереди: размер сверяется с лимитом, тип с разрешённым списком, и невалидное вложение не занимает место в хранилище, а помечается сообщением у поля. Превентивная проверка файлов экономит диск, трафик при отправке и нервы при разборе неудач.
Дисциплина локальной валидации проста: правила описаны один раз, выполняются в браузере для скорости и вежливости, и повторяются на сервере без исключений. Первый пункт делает форму приятной, второй делает систему целой.
Черновики и очередь отправки как хранить незавершённую работу
Локальное хранилище выбирают по задаче. Простое хранилище пары ключ и значение годится для маленьких черновиков: текст заметки, последнее выбранное значение, позиция прокрутки. Уважаемое хранилище объектов требуется для очереди отправки: записи форм с сотнями полей и вложенными файлами, с индексами и транзакциями, и это работа для базы в браузере.
function putToQueue(entry) {
return new Promise((resolve, reject) => {
const req = indexedDB.open("pwa-forms", 1);
req.onupgradeneeded = () =>
req.result.createObjectStore("queue", { keyPath: "id" });
req.onsuccess = () => {
const tx = req.result.transaction("queue", "readwrite");
tx.objectStore("queue").add(entry);
tx.oncomplete = () => resolve();
};
req.onerror = () => reject(req.error);
});
}
Каждая запись очереди несёт собственный идентификатор, вид формы, полезную нагрузку, время создания и состояние. Идентификатор генерируется на клиенте при создании записи и не меняется до конца жизни: по нему сервер узнает повтор, по нему очередь помечает успех или провал, и он же служит ключом идемпотентности.
Файлы в формах требуют уважения отдельно: блоб хранится в той же базе, но в собственном поле, а не в строке, и ограничения размера проверяются до помещения в очередь, чтобы не забить хранилище одним видеофайлом. Квоту браузера спрашивают официально, и близость к пределу это сигнал интерфейсу, а не молчаливая потеря черновика.
Черновик и запись в очереди это разные вещи: черновик живёт с момента первого ввода и обновляется по мере заполнения, а запись очереди появляется только по кнопке отправки. Первое спасает от потери работы, второе гарантирует что отправленное не исчезнет, и продукт обязан не путать эти гарантии.
Фоновая отправка через Service Worker и честные запасные варианты
Механизм фоновой синхронизации живёт в воркере: страница регистрирует тег задачи, а браузер, дождавшись сети, будит воркер с этим тегом, который выгребает очередь и отправляет записи по одной. Отправка из воркера переживает закрытие вкладки, и это её главное достоинство: человек нажал кнопку, закрыл браузер и ушёл, а отчёт уедет при первом же покрытии.
const reg = await navigator.serviceWorker.ready;
await reg.sync.register("flush-forms");
Поддержка механизма сосредоточена в браузерах семейства Chromium, поэтому честный продукт строит запасной путь: страница слушает событие возврата сети и прогоняет отправку сама, а воркер подключается там, где поддерживается. Флаг соединения в браузере сам по себе ненадёжен: он говорит о наличии интерфейса, а не о работоспособном интернете, поэтому решение об отправке принимается по результату попытки, а не по одному флагу.
Повторы строятся с умом: первая неудача ждёт минуту, вторая десять, третья час, и никакого потока повторов: экспоненциальная пауза защищает и сервер, и батарею. Запись с тремя провалами подряд помечается для ручного разбора и показывается человеку как требующая внимания.
Очередь прогоняется транзакционно: запись помечается отправленной только после подтверждения сервера, а не в момент запроса, и обрыв на середине не теряет ничего. Порядок внутри одного вида формы сохраняется, между видами не обязателен, и это честная цена параллельных отправок.
Приоритеты очереди назначаются видом записи: транзакционные формы уходят первыми, черновики и служебные сведения ждут своей очереди, и при долгом офлайне критичное приезжает на сервер раньше украшений. Приоритет это продуктовое решение, и оно обязано быть записано, иначе очередь сама не придумает, что важнее.
Идемпотентность ключи и конфликты когда форма уходит дважды
Офлайн рождает повторы: одна и та же запись способна уйти дважды из-за обрыва на середине ответа, повторной синхронизации или дублирования в интерфейсе. Защита строится на клиентском ключе идемпотентности: уникальный идентификатор записи, сгенерированный в браузере, сопровождает форму до сервера, и сервер хранит увиденные ключи, отвергая повторы как уже принятые.
Конфликты содержимого это отдельный разговор: две версии одного документа, изменённые на разных устройствах без сети, при синхронизации сталкиваются. Политик немного: последний по времени побеждает в простых записях, поле за полем в структурированных документах, явный диалог слияния в критичных сценариях, и выбор политики это продуктовое решение, а не техническое.
Время в записях это ловушка сама по себе: часы устройств врут, и сравнение по местному времени сравнивает показания разных будильников. Честные схемы хранят и время устройства, и номер версии записи, а решение принимают по версии с человеческим подтверждением в спорных случаях.
Серверная проверка остаётся абсолютной: ключ идемпотентности спасает от дублей, но не отменяет проверку значений, прав пользователя и бизнес-правил на принимающей стороне. Клиент может ошибаться, отставать или быть подменён, и все три случая закрываются только на сервере.
Интерфейс очереди как показать человеку что его работа не пропала
Офлайн это не только механика, но и слова на экране. Индикатор соединения показывает честное состояние: в сети, без сети, есть очередь на отправку. Бейдж очереди с числом ждущих записей честнее общей надписи офлайн, потому что человек видит объём своей отложенной работы и понимает, что произошло.
После возвращения сети интерфейс прогоняет очередь на глазах: счётчик уменьшается, успешно отправленные записи исчезают из списка, неудачные подсвечиваются с причиной и кнопкой повторить. Молчаливое исчезновение очереди выглядит как потеря, и именно поэтому отчёт о каждой судьбе записи обязан быть видимым.
Когда вкладка закрыта, о судьбе очереди рассказывают локальные уведомления браузера: короткое сообщение об успешной отправке снимает тревогу лучше любого молчания. Разрешение спрашивают один раз, и для тех, кто не разрешил, всё остаётся по-тихому.
Черновики объявляются явно: плашка черновика с кнопкой продолжить встречает человека при возвращении к форме, и автосохранение подписано словами, а не догадками. Тайминг подсказок выбирается по ценности работы: длинная анкета сохраняется на каждый ввод, короткая форма на паузу и закрытие.
Ошибки отправки переводятся на человеческий язык: сервер отверг запись по правилу валидации, значит человек видит конкретное поле и правило; сеть опять ушла, значит запись остаётся в очереди со статусом ожидания. Стек ошибок на экране не появляется никогда, потому что очередь это нормальное состояние системы, а не её сбой.
Границы доверия почему сервер всё равно проверяет всё заново
Локальная валидация это удобство, серверная это безопасность, и первое никогда не заменяет второе. Значения, проверенные в браузере, проверяются на сервере тем же набором правил, плюс правила, которые клиенту недоступны: права доступа, лимиты частоты, справочники, меняющиеся ежеминутно.
Схема формы версионируется: поле добавили, поле переименовали, и записи из старой очереди обязаны корректно приниматься сервером новой версии. Клиентский номер версии сопровождает запись, и сервер либо обновляет её по карте соответствий, либо честно отвергает с понятной причиной и просьбой обновить страницу.
Порядок внедрения офлайн-форм свёрнут в короткую лестницу:
- описать правила проверки схемой и подключить их на обеих сторонах;
- включить автосохранение черновиков с явной плашкой интерфейса;
- построить очередь записей в базе браузера с ключами идемпотентности;
- повесить отправку на фоновую синхронизацию с запасным путём на странице;
- повторять неудачи с экспоненциальными паузами и ручным разбором упорных;
- показать очередь и судьбу каждой записи словами и счётчиком;
- повторять проверки на сервере и поддерживать версии схемы формы на приёме;
Форма, которая переживает отсутствие сети, меняет отношение человека к продукту сильнее любой анимации: его работа не исчезает, и он не заполняет поля второй раз. Офлайн это уважение к чужому времени, и именно за это пользователи прощают приложениям остальное.