Shared State для React. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Worker, SharedWorker, WebSocket, PUSH, IPC-вызовы в Electron и PWA-приложении

В данном цикле статей мы рассмотрим задачу синхронизации состояния приложения между окнами. В качестве подопытного у нас будет приложение на Electron, работающее в offline/online-режимах, которое также может запускаться в PWA-режиме.

Дискраймер

Меня зовут Владимир Завертайлов. В последние лет 15 я занимаюсь в основном управлением it-компанией. Программирование воспринимаю как хобби. Поэтому прошу заранее простить, если какие-то из приведенных примеров кода или концепций будут наивными.

Две недели назад я ничего из этого вообще не умел :)

Дано

Итак. У нас на руках есть довольно богатое приложение, написанное на TypeScript, React + Redux. Запускать мы его умеем в среде Electron (это платформа на основе браузера Chrome, чтобы делать полноценные портабельные приложения — хоть для Mac, хоть для Windows или Linux — у нас все это есть). Так же есть версия для PWA, использующая большую часть кода десктоп-приложения. 

Синхронизация стейта двух окон через Main-процесс при Drag&Drop

Приложение умеет работать в offline-режиме и синхронизироваться с облачным хранилищем (мы использовали протокол GRPC). Открывать несколько окон, синхронизировать состояние между ними. Позволяет делать Drag & Drop между окнами. Кроме того, есть богатая поддержка хоткеев. И хитрые операции, в том числе Redo / Undo.

Что плохо

  1. Огромное количество boilerplate-кода из-за Redux.

  2. Архитектурная ошибка: каждое окно хранит в стейте полную копию всей базы. И полагается на то, что стейт — есть истина. Итог: на больших базах (несколько десятков тысяч записей) окна открываются несколько секунд.Пожалуйста, никогда не делайте так! Безопаснее думать о state как о кеше, который нужен вам для синхронной отрисовки компонентов. 

  3. Много слоев абстракции. Сообщения между слоями недостаточно строго стандартизированы. Слушатели могут подключаться в самых разных, порой неожиданных, местах.

  4. Протокол GRPC довольно сложно расширять. А его подход с подстановкой дефолтных значений вместо не переданных параметров — вообще рассадник трудноуловимых багов.

    Текущее состояние: пора рефакторить State
    Текущее состояние: пора рефакторить State

Что хорошо

  1. Операции TimeTravel (Redo / Undo) легко делать на Redux: достаточно просто сохранить состояние в стек и путешествовать по нему.

  2. Код довольно стабильный. Все детские болячки в нем решены. Синхронизация работает хорошо.

  3. Автотесты, в том числе интеграционные. Имеются. CI работает как надо.

В общем, все довольно по-взрослому. Кроме работы со стейтом и Redux-портянок. Хочется чего-то… Понять бы конкретно чего…

Целевое состояние

  1. Убрать бойлерплейт-код по-максимуму. Стейт сделать динамическим, с ленивой загрузкой по мере надобности.

  2. Сообщения между слоями типизированы. Слои абстракции и передача сообщений между слоями максимально сокращены. Вся логика межпроцессного взаимодействия спрятана под капот. Я хочу, чтобы стейт обновлялся сам, если чего-то пришло от сервера, или случилось в другом окне!

  3. Попробовать заменить GRPC на REST/GrapQL/MessagePack. С одной стороны хочется оставить бинарный протокол. Но мне не нравятся схемы GRPS. С другой — использовать web-сокеты, для реалтайм  обновления от сервера через PUSH. С третей — нас очень просят сделать публичный REST-api, и это есть в планах на этот год. Нужно выбрать.

  4. PWA-приложение: сделать возможность работы в нескольких окнах.

А еще мне хотелось получить такой код, от которого бы душа радовалась. Это — важно. Помчались!

Пинарик

В качестве экспериментального подопытного компонента я выбрал Пинарик — простой трекер привычек, который несколько раз пользователи просили добавить в наше приложение на CustDev-интервью. Это табличка со списком привычек, которые ты отслеживаешь каждый день. И делаешь пометку “сделал/не сделал/так себе сделал”. Если делаешь что-то полезное каждый день, это сразу бросается в глаза. 

Пинарик. 2 окна. "Гирлянда". Синхронный стейт. Подопытный компонент в программистском дизайне.

Вообще в сторах полно таких приложений, но нас настойчиво просят добавить такую панель в SingularityApp. Окей. Будем планировать рефакторинг стора на этом компоненте.

Worker и SharedWorker

Браузеры умеют запускать фоновые процессы в worker-ах. Вы пишете какой-то скрипт, кладете его в отдельный файл и дальше просто создаете в основном окне объект Worker, натравливая его на этот скрипт:

    const worker = new Worker(“worker.js”);

SharedWorker работает еще интереснее: несколько экземпляров окон могут разделить между собой один и тот же SharedWorker.  Первое, что напрашивается — вынести стейт в SharedWorker и обращаться к нему. 

Однако эта идея мне не понравилась. Мы не можем шарить память между процессами браузера (и это правильно). Все что мы можем — это отправлять сообщения в воркер и принимать сообщения из него. Ассинхронно. Либо передать какой-то подготовленный объект из потока worker-а в поток окна. Очевидно, что этот метод нам тоже не подходит, поскольку в момент передачи worker потеряет объект у себя.

Интерфейсы отправки сообщений у Worker и SharedWorker хоть и похожи, но слегка разные. Worker имеет массу ограничений, в частности — не может сам взаимодействовать DOM-деревом (логично, у него нет своего окна). 

Более того, SharedWorker не может даже в консоль ничего написать, что делает его отладку особенно утомительной. Поэтому имеет смысл:

  1. Универсализировать отправку сообщений в Worker или Shared Worker

  2. Отладить все на Worker

  3. Переключиться на SharedWorker

Хинт: для отладки SharedWorker в браузере используйте chrome://inspect/#workers. Найдите свой SharedWorker, кликните Inspect. Так можно посмотреть консоль воркера.

В альтернативных браузерах — не подскажу. На крайний случай просто делайте http-запрос на какой-нибудь localhost:3000?<log>, логи от которого вам доступы. Но вообще это лучше один раз написать, отладить и забыть. Если вы не мазохист, конечно.

Сборка Worker и SharedWorker с TypeScript для Electron. Отладка.

Файл со скриптом для Worker должен быть отдельный. Мы используем TypeScript, поэтому не можем отдать напрямую какой-то js-файл, без трансплаера (ts->js — кстати, почему TypeScript нет в редакторе кода Habr?). Я пробовал два подхода;

  1. Сделать отдельную конфигурацию webpack, собирающую воркеры. Этот подход мы используем в боевом приложении — нам там нужно все пожатое и оптимизированное (только не надо пытаться резать worker на чанки — но это и ежу понятно

Источник: https://habr.com/ru/post/646791/


Интересные статьи

Интересные статьи

Цель статьи, – показать примеры управления реализацией стратегии с помощью корпоративной единой информационной площадки на доступном инструменте, - Битрикс24. В статье на простом языке обсуждаются воз...
Вселенная – это единая система Энергия-Пространство, именно вместе, так как одно и тоже. Энергия это и есть пространство, это и есть вселенная. Что такое энергия? Под энергией мы ч...
Насколько быстро работает gRPC? Довольно быстро, если вы понимаете, как построены современные клиенты и серверы. В первой части я показал, как легко добиться улучшения на...
Земля – колыбель человечества, но нельзя вечно жить в колыбели Эту знаменитую фразу К.Э.Циолковского не забывают и по сей день. NASA, ESA, Роскосмос, SpaceX и множество других к...
Первая часть Оглавление Не конкурент магнитной ленте Жизнь после DiscoVision Заключение Не конкурент магнитной ленте LaserDisc не завоевал рынок даже на ранних этапах, по...