В прошлой статье мы рассказали, как в целом устроен фронтенд программы, обсудили технологический стек. Данную статью посвятим обсуждению Redux  —  почему мы называем его сердцем архитектуры ЕФС.



На первых порах, когда мы выбрали React ключевой технологией для нашего фронтенда, мы много экспериментировали с различными реализациями flux: свою реализацию писать не хотели. Посмотрели две реализации: alt и redux. Поскольку redux пропагандирует функциональный подход к разработке, а в штате у нас было много java-разработчиков, привыкших к объектно-ориентированному программированию, то на их взгляд alt оказался более простым. На субъективный взгляд, он имел главное преимущество: alt содержал в себе волшебный компонент AltContainer, который передавал Instance Store в дочерний компонент через Props. Изначально это прекрасно работало: у нас есть небольшое приложение, внутри которого есть несколько store’ов, не связанных между собой. Однако по мере увеличения функционала мы начали понимать, что данная технология была ошибочной для нас и приложение стало огромным и неуправляемым

One truth to rule’em all


Когда продумывалась реализация роутера в приложении, мы полагались на react-router. На backend у нас тогда использовался Spring WebFlow и было желание подружить react-router с WebFlow. В итоге, из-за особенностей реализации react-router, мы не смогли ничего реализовать, но подсмотрели, как он реализован.  Началась активная работа над своим компонентом, который в итоге получил название SWFRouter: backend говорил нам id формы, которую мы выводим на страницу, причем данные от backend мы проводили через специальное поле в react-контекст. Так случилось, что не только мы экспериментируем с технологиями. Backend-разработчики отказались от SWF в пользу собственных разработок, названных Workflow. Что делать дальше —  SWFRouter больше не нужен, нужно иное решение. Мы не хотели сильно переписывать текущую реализацию, перейдя на несколько модифицированную версию, названную WFRouter. Однако и он не смог удовлетворить наши амбиции. Нам нужен был единый источник истины, который мог бы направлять нас,  куда идти дальше. Перестав думать роутерами, мы перешли на несколько иной концепт: никаких роутеров – только flux. Мы пришли к тому, что alt нас не устраивал для реализации общей логики приложения, и мы, наконец, вернулись к redux.

Про то, что такое redux и как он устроен, есть уже много статей, мы же остановимся на небольшой вводной. Каждый раз при срабатывании action, срабатывает соответствующий reducer, возвращающий новый state. Redux придерживается immutable паттерна, который позволяет вернуться к любому состоянию приложения в любой момент. Помимо всего прочего, есть поддержка middleware, т.е. мы можем встраивать свои функции в процесс обновления state.

С redux переключать приложения стало просто: специальный action отправлял данные на backend, который возвращал либо id следующей формы, либо URL на bundle для последующей загрузки. Мы разрабатываем приложения для рабочего места независимо друг от друга, и при релизе передаем собранный bundle для последующего деплоя. Это удобно, поскольку мы можем собрать бизнес-процесс из отдельных приложений, созданных разными группами разработчиков.

Допустим, у нас есть планировщик задач. Нажав на задачу, мы попадаем в приложение, позволяющее ее выполнить. Проведя необходимые операции, мы возвращаемся в планировщик и видим, что его состояние аналогично тому, что было до перехода. При переходе в задачу мы полностью выгрузили планировщик из памяти, однако, его состояние сохранили в персистентное хранилище, что позволило его загрузить снова при возврате. Для этого мы использовали специальный middleware, который передал нам состояние для его последующего сохранения.

Так как же работает рабочее место? Мы принципиально отказались от идеи использовать iframe, т.к. это создает накладные расходы и необходимость в обходных приемах (workaround) в случае всплывающих окон.

Рабочее место состоит из нескольких компонентов: AppContainer – компонент для асинхронной загрузки AMD-модулей, UFSProvider – компонент-обертка над компонентом Provider из модуля react-redux, создающий store и передающий в него некоторый набор reducer’ов для связи всей архитектуры в единое целое.  Но обо всем по порядку.

AppContainer


Props


  • loadUrl – { () => Promise<React.Component> } – функция, возвращающая Promise, возвращающий React.Component
  • defaultUrl – { string } – URL приложения по умолчанию
  • defaultData – { Object } – данные, передаваемые в загруженное приложение

В проекте количество форм перевалило за несколько тысяч, и делать это монолитным приложением было бы ошибкой, т.к. каждая форма имеет собственный цикл разработки, тестирования и развертывания. В качестве модульной системы был выбран AMD: каждый проект упаковывает свой набор форм в AMD-модуль, который представляет собой React-компонент. При помощи SystemJS загружается следующее приложение, при загрузке он возвращает promise, который передается в компонент AppContainer. После загрузки приложение отрисовывается внутри AppContainer, и специальное поле reducer загруженного приложения передает корневой reducer, который отвечает за логику приложения и передается в качестве свойства компоненту UFSProvider.

UFSProvider


Props


  • reducer – { function } – редьюсер для логики рабочего места

Как говорилось ранее, UFSProvider создает store с преднастроенными reducer’ами. Мы не создаем отдельный store под каждое приложение, вместо этого мы предоставляем кусок существующего. Мы разделили логику store на пять составляющих:



  • appContainer – логика, используемая компонентом AppContainer для загрузки приложений
  • workspace – логика рабочего места, т.е. всей обертки вокруг приложения
  • app – непосредственно логика загруженного приложения
  • workflow – логика перехода между формами, задаваемая backend'ом

    hints – специальный reducer для внедрения подсказок на формы

Каждый раз, когда приложение в AppContainer заменяется, заменяется и набор reducer’ов в app. При необходимости в initialState передается сохраненное ранее состояние.

Подсказки


Краеугольным камнем во многих приложениях являются непонятные простому пользователю подсказки, которые на скорую руку писал программист. Мы стремимся к тому, чтобы все подсказки в интерфейсе были понятны пользователю. Для этого мы создали специальную админку для подсказок. Из нашей библиотеки компонентов мы поставляем определенный набор подсказок: блочные, всплывающие и другие. Все они на уровне библиотеки соединены со state, т.е. уже обернуты в функцию connect. Разработчику достаточно указать только код подсказки, и текст сам подцепится из state. Так как это работает? Каждый раз при обращении к серверу мы проверяем наличие поля hints в ответе, если оно есть, то содержимое этого поля мы передаем в state и таким образом «сконнекченные» компоненты получат текст вместо кода.

Загрузка нескольких приложений


Всё описанное выше отлично работает, когда на странице только одно приложение. Но что делать, когда таких приложений должно быть одновременно загружено два и более? При этом приложения должны иметь возможность обмениваться данными между собой. Для этого случая мы настроили UFSProvider таким образом, что при каждом action событие уходит на общую шину данных. Помимо этого, мы передаем state приложение. Это стало возможным благодаря специальному middleware, передающего action и state в специальный объект. Теперь вставляем на страницу несколько пар AppContainer – UFSProvider и задаем некоторую логику. Допустим, приложение A должно реагировать на событие приложения B, для этого A подписывается на action’ы приложения B и реализует собственную логику.

Пожалуй, это основное, что мы хотели рассказать о Redux технологии в Программе. Главное, не бойтесь экспериментов и создавайте такую архитектуру, которая подойдет именно вам.

Об этом и о других актуальных темах фронтенд-разработки будем рады пообщаться в комментариях к статье.
Поделиться с друзьями
-->

Комментарии (27)


  1. vintage
    04.05.2017 16:49

    Допустим, приложение A должно реагировать на событие приложения B, для этого A подписывается на action’ы приложения B и реализует собственную логику.

    Вам не кажется, что эти приложения получаются слишком сильно связанны? Коммуникацию между компонентами вы тоже так реализуете, что один компонент стучится к состоянию другого компонента? Если нет, то чем приложения хуже?


    1. Luanre
      04.05.2017 20:26

      Приложения могут разрабатываться разными командами, у них есть некоторые контракты, по которым они работают. Допустим, одно из приложений реализует broadcast-сообщения внутри себя, например, сервис уведомлений, тогда соседнее приложение может подписаться на этот канал и реагировать соответствующе при наличии срочных сообщений, на которые оно настроено. Внутри приложений компоненты используют общий store, поэтому это проблем с коммуникацией не вызывает.


      1. raveclassic
        04.05.2017 21:00

        Ну так первое тогда протекает наружу этими своими сообщениями. А второе еще и к тому же пользуется внутренней реализацией первого. Тут вопрос не в проблеме коммуникации между приложениями, а в сильной связанности.


      1. vintage
        04.05.2017 22:07

        Так а что мешает и приложениям использовать общий стор? У нас вот приложение — такой же компонент, как и все остальные. Можно запросто вставить одно приложение в другое. Или сделать третее, которое объединяет два независимых приложения в одном. Например, у нас есть приложение "webdav клиент" и есть "телефонная книга", а есть приложение "всё, что нужно работнику", которое в качестве отдельных разделов содержит списки файлов из разных директорий и телефонный справочник предприятия.


        1. Luanre
          05.05.2017 11:22

          Если над приложением работает одна команда, то да, это реализуется без проблем, но когда над разными кусками работают разные команды, могут возникать коллизии имен action'ов, отсюда вырастает потребность в синхронизации. С реализией в несколько store'ов такая проблема отпадает.


          1. vintage
            05.05.2017 11:28

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


            1. PaulMaly
              09.05.2017 18:12

              Прям с языка сняли. 100% поддерживаю.


  1. hamMElion
    04.05.2017 17:49

    Скажите, а как вы без React Router прикручивали state к истории браузера (если прикручивали)? И писали ли для этого свой компонент или нашли готовое решение? Спасибо!


    1. Luanre
      04.05.2017 20:29

      В hash у нас хранится ID загруженного приложения и ID операции, таким образом, история браузера сохраняется. Более того, перезагрузив страницу и имея оба ID, мы можем восстановить приложение на том экране, которые нужен и бек при этом пришлет данные для предзаполнения форм.


  1. Hydro
    05.05.2017 08:11
    +2

    … срабатывает соответствующий reducer, возвращающий новый state


    Срабатывают ВСЕ редьюсеры и вся middleware.


    1. Splo1ter
      05.05.2017 09:41
      -2

      Срабатывают ВСЕ редьюсеры и вся middleware.

      Срабатывают ВСЕ редьюсеры и ВЕСЬ middleware.


      1. Hydro
        05.05.2017 10:58
        +1

        Не знаю как у Вас в компании, но я часто слышу от коллег в т.ч. из других компании, что для произношения слова «middleware» используется простонародное выражение «мидла» (ж.р. ед.ч.).


        1. Miraage
          05.05.2017 11:34

          Можно перефразировать, как «вся цепочка middleware». Всё верно.


        1. Fortop
          05.05.2017 13:45
          -2

          middleware того же рода что и software

          Если бы, как указали ниже, подразумевалась «цепочка», то было бы правильно


          1. Hydro
            05.05.2017 14:24
            +1

            malware того же рода, что не мешает всем произносить как «малварь»


            1. raveclassic
              05.05.2017 14:28
              +1

              ну кстати я так и зову — мидлварь :)


            1. Fortop
              07.05.2017 10:41
              -3

              "все" это отличный аргумент.


              Но Хьюстон, у вас проблемы.
              Средний уровень интеллекта этих "всех" по правилам нормального распределения не слишком высок


              1. PsyHaSTe
                11.05.2017 17:03

                По правилам нормального распределения он средний.


                1. Fortop
                  11.05.2017 17:15
                  -3

                  В точку. Именно из-за этого низкого уровня середины и начали склонять пальто (пальта, пальте), писать вкуснОЕ кофе...


                  1. PsyHaSTe
                    12.05.2017 01:43

                    Середина не может быть низкого уровня, поэтому она называется серединой.


                    1. Fortop
                      12.05.2017 17:19
                      -3

                      Вы отличный пример такой середины.

                      Какую из них вы упомянули? Среднее арифметическое? Медиану? Мат.ожидание?

                      И, да, это разные величины.

                      И, да, второй раз, низкое/высокое имеет значение относительно точки отсчета.
                      Если уж упомянут низкий средний уровень интеллекта, то вполне естественно что точкой отсчета является нечто находящееся выше этого самого уровня.

                      Ваш учитель и К.О по совместительству…


                      1. PsyHaSTe
                        12.05.2017 18:10
                        +2

                        А ваш КО говорит, что все они для нормального распределения совпадают, ибо эксцесс и асимметрия для него равны нулю.


                        1. Fortop
                          12.05.2017 18:21
                          -3

                          Молодец. Пирожок на полочке.

                          А теперь уточните еще и про точку отсчета, для того чтобы иметь оценку выше 3-ки…


  1. jankovsky
    05.05.2017 14:49

    Большая вероятность фейла при ситуации с гигантским state-ом подкрепленным еще и собственными велосипедами.


    1. raveclassic
      05.05.2017 15:23

      Если стейт раздувает, и с ним становится сложно работать, это связано с неправильной компоновкой.

      Нужно все как следует нормализовать и отделить данные (кэши сущностей) от состояния сессии (все, что связано с текущим пользователем) и от ui (все, что относится непосредственно к рендерингу — текущие открытые попапы, выезжающие меню, роутинг и т.п.).

      Самая большая ошибка при старте — пытаться уложить все в редьюсерах для кусков интерфейса из серии «тут у нас редьюсер меню, тут хэдер, тут страница пользовательских настроек). Тогда как в редьюсерах по большей части должна лежать модель приложения, абсолютно отвязанная от интерфейса.


      1. jankovsky
        05.05.2017 15:52

        Странно, но некоторые коллеги наоборот рекомендуют делать редюсеры как раз для кусков интерфейса. И сам так делаю.


        1. raveclassic
          05.05.2017 18:22

          Это не совсем верно, так как такие данные плохо поддаются нормализации, и когда вам нужно в одной части интерфейса выгрузить какой-то кусок из другой части интерфейса, это достаточно сложно в поддержке.

          Redux подразумевает работу приложения вообще без какого-либо UI, ну т.е. это просто модель, управляемая экшенами. То, что эта модель имеет отображение в UI — только надстройка. Соответственно, опираться в модели на куски этого отображения неверно. Максимум, что можно сделать — выделить отдельный ui-редьюсер с минимальным необходимым набором значений, без которых, что самое главное, приложение продолжит работать.

          Update: более того, с ростом приложения вы начнете искать средство избавления от головной боли толстых экшенов и сайд-эффектов. Ближайшие кандидаты — redux-saga и redux-observable. Оба описывают процессы в модели, и оперировать в них вещами из UI-представления — по меньшей мере странно.