Flux для глупых людей

http://blog.andrewray.me/flux-for-stupid-people/
  • Перевод
  • Tutorial
Пытаясь разобраться с библиотекой от Facebook ReactJS и продвигаемой той же компанией архитектурой «Flux», наткнулся на просторах интернета на две занимательные статьи: «ReactJS For Stupid People» и «Flux For Stupid People». Чуть раньше я поделился с хабравчанами переводом первой статьи, настала очередь второй. Итак, поехали.

Flux для глупых людей


TL;DR Мне, как глупому человеку, как раз не хватало этой статьи, когда я пытался разобраться с Flux. Это было не просто: хорошей документации нет и многие ее части перемещаются.

Это продолжение статьи «ReactJS For Stupid People».

Должен ли я использовать Flux?


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

Если ваше приложение просто набор статичных представлений и вы не сохраняете и не обновляете данные, тогда нет. Flux не даст вам какой-либо выгоды.

Почему Flux?


Юмор в том, что Flux — это не самая простая идея. Так зачем же все усложнять?

90% iOS приложений — это данные в табличном виде. Инструменты для iOS имеют четко определенные представления и модель данных, которые упрощают разработку приложений.

Для frontend’a (HTML, JavaScript, CSS) у нас такого нет. Вместо этого у нас есть большая проблема: никто не знает, как структурировать frontend приложение. Я работал в этой сфере в течении многих лет и «лучшие практики” никогда нас этому не учили. Вместо этого нас „учили“ библиотеки. jQuery? Angular? Backbone? Настоящая проблема — поток данных — до сих пор ускользает от нас.

Что такое Flux?


Flux — это термин, придуманный для обозначения однонаправленного потока данных с очень специфичными событиями и слушателями. Нет Flux библиотек (прим. перев.: на данный момент их полно), но вам будет нужен Flux Dispatcher и любая JavaScript event-библиотека.

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

Не пытайтесь сравнивать Flux с MVC-архитектурой. Проведение параллелей только еще больше запутает вас.

Давайте нырнем поглубже! Я буду по порядку объяснять все концепции и сводить их к одной.

1. Ваши представления отправляют события


Dispatcher по своей сути является event-системой. Он траслирует события и регистрирует колбэки. Есть только один глобальный dispatcher. Вы можете использоватьdispatcher от Facebook. Он очень легко инициализируется:

var AppDispatcher = new Dispatcher();

Скажем, в вашем приложении есть кнопка “New Item”, которая добавляет новый элемент в список.

<button onClick={ this.createNewItem }>New Item</button> 

Что происходит при клике? Ваше представление отправляет специальное событие, которое содержит в себе название события и данные нового элемента:

createNewItem: function( evt ) {

    AppDispatcher.dispatch({
        eventName: 'new-item',
        newItem: { name: 'Marco' } // example data
    });

}


2. Ваше хранилище(store) реагирует на отправленные события


Как и „Flux“, „Store“ — это просто термин, придуманный Facebook. Для нашего приложения нам необходимы некоторый набор логики и данные для списка. Это и есть наше хранилище. Назовем его ListStore.

Хранилище — это синглтон, а это значит, что вам можно не объявлять его через оператор new.

// Global object representing list data and logic
var ListStore = {

   // Actual collection of model data
    items: [],

   // Accessor method we'll use later
    getAll: function() {
        return this.items;
    }

}

Ваше хранилище будет реагировать на посланное событие:

var ListStore = …

AppDispatcher.register( function( payload ) {

    switch( payload.eventName ) {

        case 'new-item':

           // We get to mutate data!
            ListStore.items.push( payload.newItem );
            break;

    }

    return true; // Needed for Flux promise resolution

});


Это традиционный подход к тому, как Flux вызвает колбэки. Объект payload содержит в себе название события и данные. А оператор switch решает какое действие выполнить.

Ключевая концепция: Хранилище — это не модель. Хранилище содержит модели.

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

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

Только хранилища регистрируют колбеки в dispatcher. Ваши представления никогда не должны вызвать AppDispatcher.register. Dispatcher только для отправки сообщений из представлений в хранилища. Ваши представления будут реагировать на другой вид событий.

3. Ваше хранилище посылает событие „Change“


Мы почти закончили. Сейчас наши данные точно меняются, осталось рассказать об этом миру.

Ваше хранилище посылает событие, но не использует dispatcher. Это может сбить с толку, но это „Flux way“. Давайте дадим нашему хранилищу возможность инициировать событие. Если вы используете MicroEvents.js, то это просто:

MicroEvent.mixin( ListStore ); 

Теперь инициализируем наше событие „change“:

    case 'new-item':

            ListStore.items.push( payload.newItem );

           // Tell the world we changed!
            ListStore.trigger( 'change' );

            break;

Ключевая концепция: Мы не передаем данные вместе с событием. Наше представление беспокоиться только о том, что что-то изменилось.

4. Ваше представление реагирует на событие „change“


Сейчас мы должны отобразить список. Наше представление полностью перерисуется. когда список изменится. Это не опечатка.

Во-первых, давайте подпишемся на событие „change“ из нашего ListStore сразу после создания компонента:

 componentDidMount: function() {  
    ListStore.bind( 'change', this.listChanged );
}

Для простоты мы просто вызовем forceUpdate, который вызовет перерисовку:

listChanged: function() {  
   // Since the list changed, trigger a new render.
    this.forceUpdate();
},

Не забываем удалять слушателя, когда компонент удаляется:

componentWillUnmount: function() {  
    ListStore.unbind( 'change', this.listChanged );
},

Что теперь? Давайте посмотрим на нашу функцию render, которую я намерено оставил напоследок:

render: function() {

   // Remember, ListStore is global!
   // There's no need to pass it around
    var items = ListStore.getAll();

   // Build list items markup by looping
   // over the entire list
    var itemHtml = items.map( function( listItem ) {

       // "key" is important, should be a unique
       // identifier for each list item
        return <li key={ listItem.id }>
            { listItem.name }
          </li>;

    });

    return <div>
        <ul>
            { itemHtml }
        </ul>

        <button onClick={ this.createNewItem }>New Item</button>
    </div>;
}


Мы пришли к полному циклу. Когда вы добавляете новый элемент, представление отправляет событие, хранилище подписано на это событие, хранилище изменяется, хранилище создает событие „change“ и представление, подписанное на событие „change“, перерисовывается.

Но тут есть одна проблема. Мы полностью перерисовываем представление каждый раз, когда список изменяется! Разве это не ужасно неэффективно?

Конечно, мы вызываем функцию render снова и, конечно, весь код в этой функции выполняется. Но React изменяет реальный DOM, если только результат вызова render будет отличатся от предыдущего. Ваша функция render, на самом деле, генерирует „виртуальный DOM“, который React сравнивает с предыдущим результатом вызова функции render. Если два виртуальных DOMа различаются, React изменит реальный DOM — и только в нужных местах.

Ключевая концепция: Когда хранилище изменяется, ваши представления не должны заботиться том, какое событие произошло: добавление, удаление или изменение. Они должны просто полностью перерисоваться. Алгоритм сравнения „вирутального DOM“ справится с тяжелыми расчетами и изменит реальный DOM. Это сделает вашу жизнь проще и уменьшит головную боль.

И еще: что вообще такое „Action Creator“?
Помните, когда мы нажимали нашу кнопку, мы отправляли специальное событие:

AppDispatcher.dispatch({  
    eventName: 'new-item',
    newItem: { name: 'Samantha' }
});

Это может привести к часто повторяющемуся коду, если много ваших представлений использует это событие. Плюс, все представления должны знать о формате. Это неправильно. Flux предлагает абстракцию, названную action creators, которая просто абстрагирует код выше в функцию.

ListActions = {

    add: function( item ) {
        AppDispatcher.dispatch({
            eventName: 'new-item',
            newItem: item
        });
    }

};

Теперь, ваше представление просто вызывает ListAction.add({name: “...”}) и не переживает о синтаксисе отправки сообщений.

Оставшиеся вопросы


Все, о чем говорит нам Flux, это как управлять потоком данных. Но он не отвечает на вопросы:
  • Как вам загружать данные на сервер и как их сохранять на сервере?
  • Как управлять связью между компонентами с общим родителем?
  • Какую event-библиотеку использовать? Имеет ли это значение?
  • Почему Facebook не включил все это в свою библиотеку?
  • Должен ли я использовать слой модели наподобие Backbone в качестве модели в нашем хранилище?


Ответ на все эти вопросы: развлекайтесь!

PS: Не используйте forceUpdate
Я использовал forseUpdate ради простоты. Правильное решение будет считать данные из хранилища и скопировать их в state компонента, а в функции render прочитать данные из state. Вы можете посмотреть, как это работает в этом примере.

Когда ваш компонент загружается, хранилище копирует данные в state. Когда хранилище изменяется, данные полностью переписываются. И это лучше, потому что внутри forceUpdate выполняется синхронно, а setState — более эффективный.

Вот и все!

В дополнение можете посмотреть Example Flux Applcation от Facebook.

Надеюсь, после прочтения этой статьи вам будет проще понять файловую структуру проекта.

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

Если этот пост помог вам понять Flux, то подписывайтесь на меня в твиттере.
Поделиться публикацией
Комментарии 35
    +1
    Спасибо, все структурировалось в моей голове.

    Вся соль, в функции Render и в том, что state — это не часть компонента, как мне кажется. Вот именно это место и полностью мне объяснило всю соль flux. Еще раз спасибо.
      0
      Но, насколько я понимаю, выносим за пределы компонента только то состояние, те данные, которые необходимо изменять на сервере.

      И да, PS в статье я прочитал. :)
        +1
        state все таки относится именно к компоненту, и именно state однозначно определяет как будет отрисован компонент.
        За пределы мы выносим не state, а данные, и в нужный момент просто копируем эти данные(из store) в state
          0
          Да, запутался я в терминах. И данные и state под одну гребенку запихал. Перечитал свой коммент, понял, что написано не то, что я хотел сказать. И да, Вы абсолютно правы.
            0
            Вы хотели сказать props вместо state.
              +2
              нет, я хотел сказать, именно, state. props это список параметров пришедших в компонент из вне(от родителя), а state набор параметров описывающий именно внутренее состояние компоненета
                0
                А вот и зря вы используете state. Flux создан для предотвращения непонятных изменений данных, и именно благодаря props мы можем декларативно объявлять компоненты и обновлять их основываясь на данных которые хранятся ВНЕ компонента. Тем самым имя куда более предсказуемые и чистые функции, нежели с каким-то магическим внутренним состоянием
                  0
                  state должен использоваться на контрольной вьюхе, а ниже спускаться только за счет пропсов. Хотя возможно вы просто запутали понятия вью, компонент и контрол-вью))
        0
        Кстати, я ошибаюсь, или подход с тем, что мы не следим за тем, как именно изменился State возможен только в ReactJS, за счет виртуального DOM'a?

        Точнее можно везде делать полную перерисовку, но только ReactJS позволяет это делать без просадки рендеринга.
          0
          Скорее так, React полностью «перерисовывает» только виртуальный DOM, а реальный DOM трогаем по минимуму.

          Выигрыш в скорости за счет того, что и новую версию виртуального DOM он сравнивает не реальным, а с предыдущей версией виртуального.
            +1
            Да, я понимаю. Я и хотел сказать, что только в ReactJs, на текущий момент, можно делать реализацию, которая не зависит от того, как именно изменился state. Точнее я хотел уточнить, только ReactJs позволяет так делать?
          +1
          Стоит ещё заглянуть в комментарии к оригинальной статье, там разработчики из фэйсбука отвечают на некоторые вопросы:
          blog.andrewray.me/flux-for-stupid-people/#comment-1819956843
            +4
            Да, комментарии стоящие.
            Позволю себе перевести один из них.

            Я работаю над Atlas в Facebook и использую React+Flux уже полтора года, так что могу ответить на некоторые вопросы:

            — Как отправлять данные на сервер и обратно?

            Отправка данных на сервер: Action Creator делает асинхронный запрос к серверу и, как только получен success или failure результат, посылает Action с соотвествующей информацией в Dispatcher, а затем обновляются и Store. Впрочем, Action Creator может послать Action не дожидаясь ответа от сервера, предполагая оптимистичный исход.

            Получение данных от сервера: когда вы запрашиваете данные из Store, он может начать загружать данные в бекграунде и проинформировать вас, как только они получены. Store может быть синхронным, в этом случае getBlah() возвращает null если данные не были загружены, или асинхронным, тогда getBlah() возвращает Promise.

            — Как управлять связью между компонентами не имеющими общего родителя?

            Используйте Actions и Stores — они глобальны и могут использоваться любыми компонентами.
              0
              Ещё один стоящий комментарий, на этот раз из facebook/flux репозитория на github:

              Пример чат-приложения я подготовил специально для конференции ForwardJS осенью 2014 года…

              … мы хотели показать вызов серверного API из Action Creators, как это предпочитает делать Jing (автор Flux). Однако, внутри FB многие обычно делают запросы к серверу непосредственно из Store. Я считаю оба эти способа корректыми и не отдаю предпочтения какому либо из них.

              Одно из преимуществ обращаться к серверу в Action Creator, а не в Store, это обработка ошибок.
            +1
            Сложилось впечатление, что фейсбуковцы переизобрели велосипед. Посмотрите на подход рендеринга Ember.js или динамический рендеринг Joosy. Он делает абсолютно то же самое, только прозрачно для пользователя: bind и dispatch магически происходят внутри фреймворков.
            Единственный принципиальный плюс в решении от FB — это рендеринг через React с виртуальным DOM, и тот под глубоким сомнением. Насколько реально широк тот кейс, когда данные меняются, но не меняется отображение? И реально ли такая оптимизация _заметна_ пользователю, или её можно заметить только профайлером?
              +2
              Из-за декларативности компонент/шаблонов React выигрыш огромен. Вам не нужно определять все состояния компонента и переходы между ними. При увеличении сложности это значительно упрощает поддержку.
                +2
                Чем шаблоны React декларативнее шаблонов Ember?

                Чтобы не быть голословным, я взял пример кода "A Stateful Component" с официального сайта React и реализовал его на Ember: emberjs.jsbin.com/figobe/edit?js,output Как видите, всё то же самое, только на два года раньше, чем React. ;)

                React я не пробовал, но полагаю, что двусторонний data-binding и самовычисляемые свойства (computed properties, аналог нативных `get` и `set`, но со встроенным кешированием и автоматическим пересчитыванием при изменении зависимых свойств) избавляют от необходимости писать массу вспомогательного кода, который приходится писать в React.

                Я уже не говорю про то, что если вы выбрали React, вам нужно самому позаботиться о Model, Controller, Router, Adapter и т. д… Код, соответствующий указанным слоям, есть в каждом приложении, просто не везде он вынесен в самостоятельные сущности. В лучем случае, вы надергаете зоопарк разнообразных библиотек и будете жирно смазывать их glue code'ом, чтобы заставить их работать друг с другом. В худшем, будете изобретать велосипеды.
                  +1
                  Посмотрел следующий пример с офсайта React:

                    render: function() {
                      var createItem = function(itemText) {
                        return <li>{itemText}</li>;
                      };
                      return <ul>{this.props.items.map(createItem)}</ul>;
                    }
                  


                  В Ember это выглядело бы так:

                  <ul>
                    {{#each item in items}}
                      <li>{{item}}</li>
                    {{/each}}
                  </ul>
                  


                  Как говорится, без комментариев.

                  Я уже не говорю о том, что объявлять функцию внутри функции — это не очень хороший стиль программирования. Не ожидал такое увидеть на титульной странице флагманского open source продукта Facebook. :P
                    0
                    У вас другая вера.
                      +1
                      >>объявлять функцию внутри функции — это не очень хороший стиль программирования
                      вы точно про JS говорите?
                        +2
                        Единственный разумный повод объявлять функцию внутри функции — это создать замыкание или ограничить scope. В данном случае не происходит ни того, ни другого.

                        Это:

                          render: function() {
                            var createItem = function(itemText) {
                              return <li>{itemText}</li>;
                            };
                            return <ul>{this.props.items.map(createItem)}</ul>;
                          }
                        


                        следовало записать так:

                          createItem: function(itemText) {
                            return <li>{itemText}</li>;
                          },
                          render: function() {
                            return <ul>{this.props.items.map(this.createItem)}</ul>;
                          }
                        


                        Я тут протестировал, разница в производительности — двукратная: jsperf.com/sibling-functions-vs-nested-functions

                        Не говоря уже о разнице в сложности структуры кода (и, соответственно, читаемости), а также наличии/отсутствии возможности юнит-тестирования функции createItem.
                          0
                          Ну как минимум замыканий достаточно для того, чтобы не говорить что объявлять функции внутри функций — не очень хороший стиль программирования.

                          Что же до того, как записан код в примере, предполагаю, что так сделано намеренно, ибо это позволяет держать под контролем (в поле зрения) доступные для рендера функции внутри одного скоупа, и не смешивать с остальными методами вьюшки, что улучшает «понимаемость»

                          А за тест спасибо, возьму на вооружение
                        0
                        Да, клево, выглядит короче. Но все портит магия эмбера. Когда пробрасываются контексты их родительских контроллеров\вьюх\роутеров, и понять откуда появилась конкретная переменная — нет никакой возможности. Даже если дебажить — цикл рендера эмбера превращает дебаг в ад.
                          0
                          В современном Ember вместо Views и Controllers используются Components, которые имеют сугубо изолированный scope.
                            0
                            Так этот современный эмбер уже вышел или нет? Так-то компоненты были и до 2й версии, и ими я хоть как-то мог осмысленно пользоваться среди ужаса и боли наследованных\перенаследованных скоупов
                              0
                              Единственное, чего еще нет, это возможности из route передавать управление сразу в component, минуя view и controller. Эта возможность запланирована на версию 1.12 (недавно вышли 1.10 и 1.11beta).

                              На данный момент для каждого route создается по view и controller. Но о них можно спокойно забыть и передавать данные сразу в компоненты через шаблон.
                          0
                          объявлять функцию внутри функции — это не очень хороший стиль программирования

                          Можно поподробней? Например:

                          def joinPaths(*args):
                              
                              def preparePathPart(part): # prepare path parts to be joined by join method
                                   # trim slashes from the end
                                   # etc
                          
                              def joinParts(paths, delimiter='/'): # another function inside of a function, join works only with prepared data
                                   return args.join(delimiter)
                          
                              return joinParts( map(args, preparePathPart) )
                          
                          >>> joinPaths('/etc/', 'init.d/', 'some.conf')
                          '/etc/init.d/some.conf'
                          


                          P.S. это просто синтетический пример на python-like псевдо-языке
                          Я не понимаю, какие конкретно могуть быть проблемы с тем, что я тут использовал аж 2 функции внутри joinPaths

                          Ровно то же самое я делаю не только на Python, но и на PHP/JS/Ruby/Go. Ни у меня, ни у моих коллег за долгие годы не было с этим никаких проблем. Более того, я считаю, что такая внутренняя декомпозиция хорошо структурирует функцию внутри самой функции. Неплохо было бы увидеть хорошее объяснение, почему это плохой стиль.
                            0
                            Кстати по коду может показаться, что вложенные ф-ии создаются при каждом запуске joinPaths, хотя в действительности этого не происходит (в случае с питоном), вложенные ф-ии уже «скомпилированы» до этого, к ним просто применяются разные «замкнутые» переменные, поэтому такой подход не должен повлиять на производительность (если кого это волнует).
                              0
                              За Python не скажу, а в JS разница в производительности 15-кратная: jsperf.com/externally-vs-internally-defined-function
                                0
                                Да, в JS видимо не так, зато можно сделать «замыкание», результат как бы тот же — приватные методы, но при этом не теряем производительности: jsperf, у меня оно показывает чуть лучше результат.
                              0
                              Ответил выше.
                        0
                        Хм. То есть React шлёт diff между предыдущим и новым виртуальным домом в реальный. А как часто проще сравнить виртуальный дом, чем напрямую поменять реальный. Почему было не сделать как в WPF, где есть INotifyCollectionChanged?
                          +4
                          Вся суть React в декларативности шаблонов. Компонент при каждом изменении полностью перерисовывается (но виртуально), затем React вычисляет минимально дешевую мутацию состояний из А в Б. В императивном подходе все мутации состояний нужно определять руками. При увеличесии сложности компонента возрастает сложности определения всех состояний и переходов. Соответсвенно возрастает риск ошибки и сложность понимания.
                          0
                          Ваше хранилище будет реагировать на посланное событие:

                          и далее
                          ListStore.items.push( payload.newItem );

                          мне одному кажется, что тут хранилище никак не реагирует на сообщение, а как раз диспатчер?
                          т.е. разве не правильнее будет сделать как-то так:
                          ListStore.items_push( payload.newItem );

                          т.о. не диспатчер будет менять какое свойство в хранилище, а диспатчер будет посылать сообщение хранилищу, чтобы оно изменило себя.
                            0
                            Напоминает Windows с его перерисовкой только обновляемых частей окна.

                            Те же события.

                            Причём подобную событийную модель я давно использую в своих расширениях для браузеров.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое