Те сферы, где очень важен конечный пользователь – например, СМИ, говорят, что уже 13% пользователей уходят, если ваш сайт открывается больше четырёх секунд, не разбираясь в причинах. А теперь давайте представим такого пользователя, который еще пробует отправить комментарий, и он постоянно «отваливается» из-за проблем со связью?
Процент уходов и отказов будет заведомо больше. Как этого избежать? Что можно сделать в ситуации, когда данные должны быть гарантированно отправлены как от клиента, так и со стороны сервера?
На этот и другие вопросы отвечает Андрей Ситник – автор PostCSS и Автопрефиксера, ведущий фронтендер в «Злых Марсианах».
– Почему мы вообще говорим о проблемах связи? Разве это не вопрос, исключительно связанный с физическими/сетевыми возможностями?
– OSI тут не совсем в тему. Logux заменяет REST и AJAX. То есть это чисто прикладной уровень. Logux решает следующие проблемы:
- Сейчас требуется много кода для простых запросов.
- Живое обновление данных с сервера писать на порядок сложнее.
- Хорошую поддержку оффлайн вообще ад написать. Но оффлайн нужен всегда, так как у нас всегда есть «500 мс оффлайн» — эти постоянные крутилки на кнопках.
Для этого Logux как бы создаёт особый виртуальный канал событий между клиентом и сервером.
Клиент может положить в этот канал события — Logux сам их отправит, когда можно будет. И то же самое на сервер. Давай разберём это на техническом уровне пошагово:
Разбор полетов
1. На клиенте это будет библиотека с API один-в-один как у Redux (к Реакт не привязана, есть и более низкоуровневый API). Ты точно так же создаёшь action. Но у некоторых action можешь выставить ключ sync: true — тогда Logux доставит их сам на сервер. Эта библиотека держит веб-сокет, посылает пинг, проверяя, что связь есть.
– Так, а чем хорошо то, что эта библиотека не привязана к Реакту?
Не все разрабатывают на Реакте (и это очень хорошо для разнообразия среды). Кто-то может использовать Vue.js, Angular. Или просто иметь JS-приложение, где нет HTML, так что Реакт будет не так нужен. Идём дальше:
2. Дополнительная библиотека, которая при потере связи покажет спец. бейджик «Нет связи» вверху страницы и изменит фавикон. Если в канале на отправку окажутся события, а связи не будет — она тоже скажет пользователю, что не все данные сохранились.
– Зачем пользователю это знать? Для какого формата взаимодействия это может быть критически важным?
– Если пользователь передал нам какие-то данные, то мы обязаны его уведомить, что они не сохранились. Я бы сказал, что это поведение должно быть по умолчанию. Библиотека опциональна во многом, чтобы написать самому такое уведомление и лучше встроить его в дизайн. Хотя есть случаи, когда можно не показывать проблемы со связью — например, если Logux используется для сбора действий пользователя для аналитики.
Следующий шаг.
3. Когда связь появится, Logux-клиент и сервер отправят друг другу те события, которые добавились за время отсутствия связи.
– Понятно, логично, сколько действий может быть в очереди?
– Обычно событий 10-20. Но у нас нет жёстких ограничений — сколько поместится в памяти клиента и сервера.
– Порядок выполнения этих событий?
– Порядок событий контролируется очень строго. Тут же проблема не только в порядке событий одного пользователя. Но и при работе нескольких пользователей — порядок всех их событий должен быть одинаковым на всех системах (чтобы итоговое состояние было одинаковым). Поэтому в Logux каждому событию присваивается время создания. Оно довольно хитрое — например, учитывается и разница времени между клиентом и сервера.
– Не будет ли так, что какие-то события уже не надо выполнять? Взаимная проверка зависимости действий?
– Клиент может чистить лог от уже ненужных событий. Это уже определяется бизнес-логикой.
Следующий шаг: пользователь может закрыть браузер — неотправленные события сохранятся в localStorage. Но перед попыткой закрытия библиотека попросит пользователя сначала выйти в интернет, так как есть неотправленные данные.
– Опять возвращаемся к вопросу ёмкости и важности, и что произойдёт, если пользователь принудительно закроет браузер?
– Ничего страшного. Как только пользователь снова вернётся на сайт — данные сами отправятся.
Теперь все оставшиеся шаги:
5. Для написания Logux-сервера есть специальный серверный фреймворк. То есть это как express.js. Ты сам описываешь, как сервер реагирует на то или иное событие с клиента. При этом логика чуть хитрее — то, что событие пришло сейчас, не значит, что оно было создано сейчас. Поэтому у каждого события есть время создания. При соединении клиент и сервер определяют разницу времени и синхронизируют часы (и время создания старых событий).
6. Фреймворк для сервера пока на node.js. Потом сделаем для Elixir и Go. Но можно использовать и Ruby, Python и PHP — просто поставить node.js сервер как прокси, чтобы держать сокет. А уже этот прокси-сервер будет посылать старый REST-запрос в ruby-сервер.
7. Поскольку у нас есть этот лог событий и точное время, поверх него можно сделать CRDT как в Swarm.js — и автоматически разрешать конфликты во многих простых случаях.
8. Вообще, в большинстве случаев можно не рисовать крутилку — клиент может сразу отрисовывать, будто событие уже выполнилось (например, комментарий отправлен), Logux сам покажет пользователю, что данные не сохранились на сервер. Само собой, будут случаи, где так не получится — например, оплата. Там можно делать старую логику с крутилкой.
Logux и альтернативы
– Спасибо, а чем Logux тогда лучше уже существующих решений?
– Есть Relay/GraphQL — они сокращают количество кода при запросе данных. Но изменение самих данных тут тоже реализовано не очень просто. Живое обновление решено плохо. Оффлайн тоже не проработан.
Есть изоморфные базы данных — они работают на клиенте и на сервере и синхронизируют данные между собой. Например, CouchDB и Firebase. Там хорошо решено живое обновление данных. Готовый CRDT сейчас мало у кого есть, но, в целом, реализовать его можно. Но их довольно сложно расширять, многие разработчики боятся такого странного подхода к синхронизации, поэтому такие базы данных и не стали индустриальным стандартом.
Есть ещё CRDT-библиотеки — например, Swarm.js. Logux копирует много идей у Swarm.js. Но Swarm.js сложно подружить с Реактом и Редаксом. Также сложно передавать в нём не-CRDT операции.
– В чём сильные и слабые стороны?
– Меньше кода, живое обновление данных из коробки, легко сделать приложение для работы в оффлайн. И всё это со знакомой семантикой Редакса.
Главная слабая сторона — надо запускать дополнительный сервер. Но это есть у всех таких решений. Плюс этот сервер можно сделать прокси-сервером и всю логику продолжать хранить в PHP или Ruby on Rails.
– Откуда вообще появилась потребность в этом выделенном решении?
– На Амплифере нужно было показывать живую статистику публикаций и обновлять страницу с ошибками сразу же при появлении проблемы. И мы поняли, что хорошего решения не нашлось. Попробовали внедрить Swarm.js, но легко развернуть его не получилось. Так что я начал думать, как подружить Swarm.js и Редакс. В Питере в это время как раз был Дэн Абрамов, и в разговоре с ним родилась идея отдельной библиотеки.
– Имеет ли это шансы на то, чтобы стать индустриальным стандартом?
– Я стараюсь сделать Logux, как универсальное решение для всех веб-приложений. Но, мне кажется, в 2017 мы увидим много ещё попыток переделать AJAX/REST — сейчас это главная проблема веб-разработки, на мой взгляд. Кто победит в этой борьбе, узнаем только в 2018.
Примеры из жизни
– Есть ли 1-2 интересных практических кейса?
– Да, даже TODO MVC — вам же хочется продолжить работать со списком задач даже, если связи нет. Или если вы добавили задание на компьютере, то хотите, чтобы оно появилось тут же и на телефоне, без перезагрузки страниц.
Или комментарии — живое обновление, как в Фейсбуке или ВК, полезно для вовлечения людей.
Есть ещё одно преимущество Logux для любого сайта — это «оптимистичный интерфейс» (о нём как раз рассказывал другой спикер HolyJS). Сейчас при каждом действии мы показываем пользователю «крутилку». Каждое сохранение блокирует интерфейс и нарушает поток пользователя. С помощью Logux на порядок проще делать оптимистичный интерфейс, где сохранение будет происходить в фоне, отвлекая пользователя, только если ошибка действительно произошла. Например, так работает Google Inbox. Любое веб-приложение выиграет от оптимистичного интерфейса, так как пользователи всегда любят быстрые интерфейсы.
– Большое спасибо за интервью, и до встречи на конференции.
Конечно, в интервью удалось обсудить только общий взгляд на библиотеку и ее философию, на HolyJS Андрей выступит с подробным часовым докладом о разработке и работе с Logux. Кроме того, можно будет сразу посмотреть доклады, посвященные и другим обсуждаемым технологиям:
Комментарии (12)
23derevo
02.12.2016 21:56+4Каким образом гарантируется доставка данных от клиента серверу и наоборот? Как бороться с повторной отправкой данных в случае, если сломались клиент, сервер или интернет между ними? Через идемпотентные операции?
Iskin
02.12.2016 22:07+2Можно сказать, что и идемпотентные. Каждое событие имеет уникальный ID (он же время создания). Когда событие добавляется в лог, то проверяется, нет ли уже такого ID. Так что всё проблема повторной доставки сильно упрощается.
23derevo
02.12.2016 22:11+1ну да, это и есть идемпотентность в чистом виде. Просто синтетическая — за счет синтетического ID события.
Vestild
02.12.2016 23:44+1Я правильно понимаю, что Logux работает на WebSocket?
Это не создаёт проблем? Старые брауззеры, прокси?Iskin
03.12.2016 01:53Прокси большая проблема для обычных веб-сокетов, но если перейти на сокеты поверх SSL (WSS), то большинство проблем решится. Мы стараемся везде в интерфейсе форсить WSS.
Сокеты — IE 10+, это нормально. Логакс ориентирован на SPA, а для IE <10 SPA создают редко.
Но, сам Логакс не ограничивает как передаются данные. Просто стандартный драйвер использует веб-сокеты. Можно заменить на AJAX + keep-alive. Или внедрить протокол Логакса внутрь какого-то нибудь корпоративного XML.
bioforge
02.12.2016 23:46+1Может задам тупой вопрос, но почему бы в БД и на клиенте не хранить дату последнего изменения каждого поля?
При синхронизации сверять даты и обновлять поля с датой меньше присланной. Побеждает тот, кто последний менял данные.Iskin
03.12.2016 01:55Правильно мыслите — собственно в CRDT примерно так и сделано.
Но разделение по полям не идеальный способ. Есть типы данных, которые так мержить не получится — например, счётчики или длинный текст (где хочется параллельное редактирование). Для этого и придумали более абстрактный способ — хранить действие.
Но сборщик мусора в логе и так удаляет все старые значения поля (если это CRDT Map). Так что хранится очень близко к вашей идей. Просто чуть универсальнее для других типов.
Iskin
03.12.2016 01:57Кстати, как хранить конечные данные на сервере решаете вы. Так что даты полей можете не хранить, а использовать обычное хранение. Или хранить именно с датами, чтобы мерж работал, даже если клиента не было онлайн более месяца.
В общем Логакс никак не ограничивает БД на сервер. Свой лог он хранит в отдельном файле и этот лог лишь дополнительная вещь. Его можно смело чистить через неделю.
eugef
А как Logus подружить с Service Workers?
Так как SW и служат для решения проблемы оффлайна, то логично всю логику Логуса исполнять там.
Iskin
Есть две разные задачи в поддержке оффлайна:
1. Кеширование HTML/JS/CSS-файлов приложение.
2. Организация работы с данными приложения без связи.
Эти задачи очень разные. Сервис-воркеры созданы для решения только первой задачи. Для решения второй задачи они не созданы.
Можно конечно перенести работу с логом в сервис-воркер, но это только усложнит код и уменьшит поддержку браузеров.
eugef
Если запросы к API идут по HTTP(s), то в принципе можно кешировать их на уровне Service Workers.
Когда связи нет, то выполнять запросы отложенно а клиенту возращать, например, что все ок.
Как бонус — это прозрачно для приложения. Из минусов — не получится перехватить WebSockets, ну и меньше поддержка браузером
Iskin
Да, так можно — но смысла мало. На клиенте всё равно больше кода для AJAX-запроса. Непонятно как понятно и прозрачно интегрировать получение таких же событий с сервера.
Плюс запуск и обновление сервис-воркера — хитрая задача. В спеке есть не очень очевидные моменты (например, с адресом воркер-файла).
Ну и преимуществ не будет никаких (в фоне воркер сокет не может держать).