Нам в Хекслете нравится ReactJS и Flux. Нам кажется, что это правильное направления развития. Мы любим функциональное программирование и чистые функции, и когда сложные архитектуры упрощаются за счет подходов, связанных с ними — это круто. По Реакту уже есть немало ресурсов в интернете, в том числе наш практический курс по React JS. Последний урок в этом курсе называется «Однонаправленное распространение данных», и там мы подходим к интересной теме, которая лежит в основе архитектуры Flux.

Flux это паттерн распространения даных в приложении. Flux и React выросли в стенах Фейсбука вместе. Многие используют их одновременно, но ничто не мешает использовать их по отдельности. Они были созданы для решения конкретной проблемы в Фейсбуке.

Мы используем React и Flux в своей браузерной среде разработки Hexlet IDE (она в опен-сорсе), в которой учащиеся выполняют практические задания. Flux одновременно очень популярен и очень непонятен для многих в мире веба. Сегодняшний перевод — попытка объяснить Flux на пальцах (ну, то есть картинках).

Проблема


Вначале нужно понять, какую проблему решает Flux.



Известный пример — баг с нотификациями. Вы заходите в Фейсбук, видите нотификацию у иконки сообщений. Кликаете, но нового сообщения нет. Нотификация пропадает. Потом, через несколько минут взаимодействия с сайтом, нотификация возвращается. Кликаете снова… и снова нет сообщений. И так снова и снова и снова.



Это был не просто циклический баг для юзера. Это была циклическая проблема в команде Фейсбука. Они чинили его, и на время баг исчезал, но потом вновь появлялся, и так снова и снова: issue > fixed > issue > fixed…

Они искали способ решить проблему раз и навсегда. Не просто исправить баг, а сделать так, чтобы система не позволяла таким багам появляться.

Фундаментальная проблема


Фундаментальная проблема крылась в механизме движения данных внутри приложений.



Есть модели, содержащие данные, и они передают данные в слой, который рендерит данные.

Юзер взаимодействует с видом (views), поэтому виду иногда нужно обновлять модель на основе пользовательского ввода. И иногда моделям нужно обновлять другие модели.

К тому же, экшны иногда генерируют каскадные изменения. Я представляю все это как напряженную игру в Pong: сложно понять, куда полетит мячик (и не свалится ли за пределы экрана вообще).



Добавьте к этому возможные асинхронные изменения. Одно изменение может генерировать несколько других. Это как выкинуть кучу шариков для настольного тенниса на экран Pong'а, так чтобы они летали во все стороны по пересекающимся траекториям.

В общем, этот поток данных хрен отладишь.

Решение: однонаправленный поток данных


Фейсбук решил попробовать другую архитектуру, в которой данные двигаются всегда в одном и только одном направлении. И когда нужно добавить новые данные, поток начинается с самого начала. Назвали эту архитекуру Flux.



Это в реальности крутая штука… но по картинке выше, наверное, не заметно.

После того, как разберетесь с Flux, картинка станет понятнее. Проблема в том, что если взяться за изучение документации Flux с нуля, такая иллюстрация не помогает понять суть. Мне она не помогла. Зато помог другой подход: я начал думать о системе как о наборе персонажей, которые работают вместе чтобы добиться результата. Так что вот, знакомьтесь, это мои вымышленные друзья.

Знакомьтесь


Сначала представлю всех по-быстрому.

Создатель действия (action'а)

Первый персонаж — создатель действия. Он генерирует действия (экшены), и через него проходят все изменения и взаимодействия с системой. Хотите изменить состояние приложения или хотите изменить вид — нужно создать действие.



Я представляю его как оператора на телеграфе. Вы знаете, какое сообщение нужно послать, а создатель действия форматирует его таким образом, чтобы другие части системы поняли.

У действия есть тип и груз. Тип это один из заданных в системе типов (обычно, это список констант). Например, MESSAGE_CREATE или MESSAGE_READ.

Получается, часть системы знает о всех возможных действиях. У этого факта есть полезный побочный эффект. Новый разработчик может открыть файлы создателя действий и увидеть весь API — все возможные изменения состояния в системе.

Созданное действие передается диспетчеру.

Диспетчер

Грубо говоря, диспетчер это большой регистр функций обратного вызова (коллбэков, callbacks). Что-то вроде оператора на телефонной станции. У него есть список всех хранилищ, которым нужно отправлять сообщения. Когда от создателя действия приходит новое действие, диспетчер передает его нужному хранилищу.



Он делает это синхронно, что упрощает ситуацию со множеством шариков в игре Pong, о которой я говорил выше. И если нужно задать зависимости между хранилищами (так, чтобы одно хранилище обновлялось перед другим), в диспетчере можно использовать waitFor().

Диспетчер Flux отличается от диспетчеров в других архитектурах. Действие отправляется всем зарегистрированным хранилищам вне зависимости от типа действия. Это означает, что хранилище не просто подписывается на определенные действия. Оно узнает обо всех действиях и фильтрует те, которые имеют для него смысл.

Хранилище

Хранилище содержит состояние приложения, и вся логика изменения состояния живет внутри.



Я представляю себе хранилище в виде дотошного бюрократа, контроллирующего больше, чем надо. Все изменения должны быть выполнены им лично. И нельзя напрямую запросить изменение состояния. У хранилища нет сеттеров (setters). Чтобы запросить изменение состояния, нужно следовать особой процедуре… нужно написать заявление на имя директора подать действие через канал создателя действия и диспетчера.

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

После изменения хранилище генерирует событие об изменении. Контроллер-вид (controller view) узнает об изменении состояния.

Controller view и view

Виды (views) отвечают за рендер состояния для пользователя, а также за прием ввода от пользователя.



Вид это presenter. Он не знает ни о чем в приложении, он знает лишь о данных, которые ему переданы, и как форматировать эти данные чтобы людям было понятно (с помощью HTML).

Контроллер-вид (controller view) это некий менеджер среднего звена или агент между хранилищем и видом. Хранилище сообщает ему об изменении состояния. Он получает новое состояние и передает его всем своим видам.

Как все они работают вместе


Организация

Вначале — организация. Инициализация приложения происходит только один раз.

1. Хранилища сообщают диспетчеру о том, что хотят получать уведомления о действиях.



2. Контроллер-вид запрашивает у хранилищ их последние состояния.

3. Когда хранилища отвечают, контроллер-виды передают эти состояния своим дочерним видам на рендер.



4. Контроллеры вида просят хранилища сообщать им об изменениях состояния.



Поток данных


Когда инициализация завершена, приложение готово принимать ввод от пользователя. Давайте сгенерируем действие, как будто пользователь произвел изменение.



1. Вид говорит создателю действий чтобы тот подготовил действие.



2. Создатель действия форматирует действие и посылает его диспетчеру.



3. Диспетчер посылает действие хранилищам по порядку. Каждое хранилище получает сообщение о всех действиях. Потом оно решает реагировать на действие или нет, и обновляет (или не обновляет) состояние соответственно.



4. После изменения состояния, хранилище оповещает контроллеры вида, которые подписаны на него.

5. Эти контроллеры вида просят у хранилища дать им обновленное состояние.



6. После того, как хранилище передаст свое состояние, контроллер вида попросит своих дочерние виды перерендериться в соответствии с новым состоянием.



Вот так я представляю себе работу Flux'а. Надеюсь, вам это тоже поможет!

Ресурсы

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


  1. ha2bj
    05.10.2015 15:53
    +4

    Ваши картинки еще больше запутали :)


  1. PQR
    05.10.2015 16:08
    +18

    Сам использую Flux подход и мне нравится, как он привносит понятную структуру и большую прозрачность в происходящее.

    Но что я не люблю в таких статьях, так это упрощение типа «посмотрите на MVC — всё влияет на всё, куча стрелочек во все стороны, а теперь посмотрите на Flux — все стрелочки one way!», типа такого:
    image

    Но давайте посмотрим на Flux объективно:
    1) у нас не один «прямоугольничек» View, их много — это целая иерархия React компонент
    2) каждый View (React компонент) может генерировать Action
    3) все Action проходят через единый Dispatcher, но если взглянуть на эту ситуацию как она есть на самом деле: каждый Action может быть обработан несколькими Store, каждый Store может обработать несколько Action — тут всё та же паутина стрелочек many-to-many между Action и Store
    4) Store могут зависеть от других Store через механизм waitFor — это и есть каскадные обновления, только завёрнутые в промисы
    5) Каждый Store может влиять на рендринг одного или нескольких View, а каждый View может зависеть от нескольких Store — опять паутина many-to-many стрелочек
    6) И, наконец, рендринг одних View влияет на другие View, ведь это иерархия React компонент

    Вот как это выглядит:

    А теперь попробуйте на картинке про MVC взять все стрелочки, которые нарисованы влево (от View к Mode) и нарисовать их по большой дуге по часовой стрелке, вставив по пути квадратики «Action» — получается Flux!
    image

    Иными словами: Flux — это такой правильно нарисованный MVC с переименованными терминами (Model -> Store, Controller -> Action Creator + Dispatcher).

    P.S. я не против Flux, я только за! Я против упрощений в попытках сравнить MVC и Flux, с которых начинается каждая первая статья про Flux.


    1. kahi4
      05.10.2015 16:36
      +5

      Я бы начал с того, что MVC тут в принципе неправильно нарисован. Стрелок от View к Model быть не может, модель обновляют только контроллеры.
      Модель изменяет другую модель только через агрегирование (в том числе — объект, владеющий обеими моделями). Ну и в зависимости от задач — паттернов проектирования на самые разные ситуации кучи.

      Объясните на пальцах в чем принципиальное новшество Flux кроме того, что он породил пару [лишних] абстракций? Однонаправленное движение событий? В MVC оно и так из коробки (пока кто-то не пытается накастылить и править модель прямо из вью, перемешивая все в кучу).

      UPD: стрелка от View к Mode есть, но она обозначает «запрос данных», а не их изменение. В случае подписки на изменение модели вьюшкой в автоматическом режиме стрелку можно опускать.


      1. kahi4
        05.10.2015 16:49
        +4

        К слову. Тут прикинул — Flux — это ведь попросту каскадированный (в том смысле, что есть диспетчер, а за ним store, который, в общем то, по факту поддиспетчер) шаблон проектирования «Фасад», допиленный напильником и переделанный под события?


    1. dmitriiabramov
      05.10.2015 18:26
      +2

      глядя на последние тенденции в архитектуре (в том числе то что на самом сайте flux) я обратил внимание на две вещи:

      1. чем больше приложение, тем меньше реакт компоненов имеют доступ к store. это или один (или несколько) controller views, которые агрегируют данные и передают дереву компонентов (дерево в свою очередь pure и не имеет сайд эффектов). Или же это использование higher order components (https://github.com/yahoo/fluxible-addons-react/blob/master/docs/api/connectToStores.md) которые абстрагируют дерево компонентов от stores, и приложение становится зависимо полностью от переданных props.

      2. чем больше приложение, тем меньше количество stores. Одна из проблем Flux это то что stores разделяют две ответственности: (1) Бизнес логика приложение (что случилось в приложение, что нужно сделать) и (2) управление данными (как модифицировать определенный объект). Если все состояние приложения поместить в один store, который внутри имеет сложную структуру но на поверхности имеет простой интерфейс (обработчики actions), то это упразднит необходимость в использовании waitFor. сам waitFor сигнализирует что имеется зависимость данных, которая искуственна разделена на несолкько stores. (om и redux например изначально хранили все состояние в одной неделимой структуре данных)

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


      1. timramone
        05.10.2015 23:22

        Мне тоже очень не нравится эта идея с waitFor. А вы не знаете примеров сравнительно большого open-source приложения с одним сложным (желательно immutable и полиморфным) стором? :)


        1. dmitriiabramov
          05.10.2015 23:38

          большие приложения реального мира обычно не опенсорсят :)
          у fluxible есть репозиторий с небольшими примерами (https://github.com/yahoo/flux-examples)
          они используют HOC и предоставляют изоморфность.


        1. rajdee
          07.10.2015 12:43

          Не знаю, подходит ли это под ваши требования, но существует довольно популярная Flux-реализация — redux, которая как раз оперирует только одним стором и пропагандирует иммутабельность.


          1. timramone
            07.10.2015 12:54

            Да я удивляюсь, что сам dmitriiabramov мне его не посоветовал)


            1. rajdee
              07.10.2015 14:14

              Так то Dan Abramov, если вы про создателя redux ;)


              1. timramone
                07.10.2015 14:34

                А я что-то подумал, что это один человек. Ну, бывает :)


      1. RusSuckOFF
        06.10.2015 15:35

        deleted


    1. MostovenkoAlexander
      06.10.2015 23:58

      плюсую, сам такого же мнения, не так все просто как нам его продают.


  1. hell0w0rd
    06.10.2015 00:21

    Я вот недавно столкнулся с простой проблемой. Допустим вы делаете админку. Получается у вас все сторы превращаются в гигантские статические переменные, причем некоторые еще понятны, UsersStore — ок, список пользователей. А вот где держать пользователя при переходе на /users/:id? В UserStore? Тогда каждый раз оно будет обновляться, в итоге это просто глобальная переменная


    1. dmitriiabramov
      06.10.2015 04:49

      я думаю что текущего пользователя держать отедльно не нужно. `/users/:id` это раут, и держать его нужно в RouteStore или чем-то подобном. когда браузер переходит по ссылке `/users/1`, RouteStore будет иметь значения `this.currentRoute = 'users'` и `this.currentParams = {id: 1}`.
      Имея id текущего юзера можно легко вытащить нужные данные из UsersStore.


      1. hell0w0rd
        06.10.2015 11:04

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


        1. dmitriiabramov
          06.10.2015 20:20

          это уже проблема не Flux а state synchronization. Тут свои правила и свои решения :)


  1. murzilka
    06.10.2015 10:03
    +1

    Почему вконтакте никогда не было бага с нотификациями?
    Есть ощущение, что успешно решается самими же придуманная проблема.


    1. wiygn
      06.10.2015 16:08
      +2

      У вк есть баги с нотификациями.

      Первый: если открыто несколько вкладок с вк, то уведомления в правом нижнем углу будут показывать количество сообщений умноженное на количество вкладок. Т.е. у вас открыто 3 вкладки с вк (аудио + диалоги + что-то еще), вам кто-то присылает два сообщения, но справа внизу у вас будет показывать не 2, а 6 входящих сообщений.

      Второй: не обновляются badges сообщений в левом боковом меню, если нажать на "+1", то сообщения покажет, причем уже прочитанными, а сама цифра пропадает только после рефреша страницы.

      Так что не надо тут про вк.


      1. murzilka
        06.10.2015 16:31

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


        1. NeXTs_od
          14.10.2015 16:38

          Вконтакте ещё так можно повторить багу с уведомлением о сообщении.
          Имеем 2 компьютера, на работе и дома. И там и там открыта вкладка вконтакта, один из компьютеров, допустим рабочий — в сне.
          Сидя дома мы получаем сообщение, открываем его, читаем.
          На следующий день приходим на работу — во вкладке висит уведомление о непрочитанном сообщении.


        1. NeXTs_od
          14.10.2015 21:47

          Забавно, я понял эту схему когда разобрался с флюксом. Когда только разбирался в нём — картинки только запутали)