Недавно я начал работать с Lightning Component Framework (последний интерфейсный набор инструментов Salesforce). Как Javascript - и особенно React + Redux - разработчик в глубине души, конечно, одним из первых вопросов, которые возникли у меня при входе в это новое пространство, было то, как оно поддается архитектуре flux.

По умолчанию Lightning поставляется с огромным количеством полезных функций, которые можно уникальным образом скомпоновать для создания однонаправленного потока данных, но в нем нет никаких конкретных предложений о том, как именно настроить что-то подобное. Возможно, это потому, что это действительно не так сложно, или, может быть, они хотят оставить это открытым для разработчиков, потому что тонкие различия между подходами могут повлиять на большие изменения, которые трудно обнаружить, если их не заставлять учитывать (oof) ; или, может быть, это просто не слишком большая проблема для Salesforce, по сравнению, например, с более широкой идеей простого повторного использования, которая, кажется, была всеобъемлющей темой фреймворка с самого начала.

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

Краткая предыстория нескольких важных функций Lightning

Чтобы охватить все мои основы, вот некоторые вещи, о которых вы обязательно должны знать, прежде чем пытаться расширить то, на что уже способно Lighting:

Атрибуты. Нельзя сказать достаточно о важности и полезности атрибутов в компонентах Lightning. Это включает в себя традиционный HTML-вид атрибутов, с которыми все веб-разработчики знакомы, а также атрибуты <aura:attribute ...>, которые устанавливаются внутри тела на уровне компонента (ну, технически не в {!v.body}, а между тегами компонентов на верхнем уровне ) и, конечно, может быть достигнута / установлена ​​через родительские компоненты традиционным способом. Атрибуты позволяют нам связывать значения между родительскими / дочерними компонентами, они могут содержать изменяющееся состояние на уровне отдельного компонента, и они могут быть динамически изменены или на них можно ссылаться в javascript. Эта последняя часть является мощной функцией из-за ее простоты (которая имеет огромное значение, когда вы говорите о создании API на основе существующих функций).

События: API событий Lightning - еще одна очень удобная функция фреймворка, и, вероятно, к ней у меня меньше всего жалоб (на самом деле у меня не так много в целом, если честно). Он четко различает два типа событий: компонент и приложение. Суть этой разницы в том, что события компонентов всплывают, как традиционные события DOM, к которым мы привыкли (такие как всемогущий «щелчок»), которые распространяются вверх по стеку (до тех пор, пока распространение не будет явно остановлено) и not получено одноуровневыми или дочерними компонентами. С другой стороны, события приложения транслируются по всему приложению; любой компонент, связанный в стеке приложения, имеет право на получение события, и все диспетчеры и подписчики полностью разделены (таким образом, нет порядка для того, кто получает событие, и ни один получатель не может что-либо изменить в этом событии после того, как оно было запущено) .

Компоненты. Это рабочие лошадки фреймворка Lightning. Они могут содержать или не содержать фактическую разметку HTML. По своей сути, компонент - это движок, который может иметь атрибуты (и другие вещи, такие как обработчики и методы, но пока мы не будем об этом говорить) и тело, которое представляет собой массив из нуля или более элементов / компонентов. Он делегирует JavaScript своему контроллеру (и, возможно, отдельный handler.js), а также может подключаться к определенному контроллеру Apex. Он знает, как обновлять собственное состояние при изменении значения связанного атрибута ({!v.myAttribute}), так что вам не придется. Просто подключите его, и пусть мощный молниеносный двигатель сделает все остальное.

Почему Flux?

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

Здесь проиллюстрирована самая большая опасность в нашем примере с часами: допустим, мы делегировали синхронизацию не одному атрибуту верхнего уровня, а, скорее, отдельным компонентам, что один из часов может, например, рассинхронизироваться; ни у него, ни у какого-либо другого компонента нет возможности узнать, что это произошло, и поэтому приложение продолжит работу, как будто все в порядке. Часы довольно точны, поэтому давайте сделаем эту проблему более реальной: у одного из компонентов есть кнопка паузы. Теперь мы должны уведомить все другие компоненты о том, что время остановилось, и мы до сих пор не знаем наверняка, что они даже синхронизированы. Если мы вернемся к тому, чтобы часы были на верхнем уровне, и просто привязать значение времени к каждому дочернему элементу, эта последняя проблема внезапно станет гораздо менее пугающей, потому что теперь нам нужно уведомить только один компонент об изменении, и мы может рассчитывать на то, что этот компонент обновит всех дочерних элементов, которым он небезразличен. И в этом прелесть потока: однонаправленный поток данных из единого источника правды.

Заявление об отказе от ответственности (-ish)

Я хочу завершить эту последнюю часть и добавить к следующему разделу предупреждение об использовании flux. Невероятно соблазнительно поместить все в состояние приложения, когда вы начинаете программировать с помощью этого шаблона. Кажется потрясающим, умным, эффективным и чистым просто собрать все это в одном месте, но на самом деле идея однонаправленного потока данных не в этом. Используйте поток только для обработки общего состояния приложения. То есть вещи, которые имеют решающее значение для успеха некоторых основных функций приложения (например, сохранение неотправленных данных формы при изменении маршрутов в SPA или, что более типично, обмен данными между компонентами), а не то, что только маленький виджет слева боковая панель заботится и никому другому. Не забивайте шоссе потоками ненужными деталями. Так вы создадите для себя гораздо больше работы, и ценность потока будет потеряна.

По возможности используйте атрибуты компонентов; У Lightning есть отличный способ обработки компонентов с отслеживанием состояния через атрибуты (даже проще, чем в React в базовых случаях), и этим следует воспользоваться. Это не означает, что у вас не может быть перекрывающихся иерархий потоков, если вам это действительно нужно; возможно, этот виджет очень сложный и поставляется с кучей дочерних компонентов, которые совместно используют данные - просто убедитесь, что шаблон оправдан. В противном случае, если он вам нужен только в одном месте, храните его в этом одном месте.

Пример в Lightning

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

В этом решении я создаю следующие части:

  • базовый компонент FluxContainer, который можно расширять, чтобы другие компоненты могли использовать его для управления состоянием своего приложения.
  • событие приложения FluxContainer_getState, которое будет транслироваться контейнером
  • событие компонента FluxContainer_setState, которое может быть запущено любым компонентом для обновления состояния
  • родительский компонент, расширяющий FluxContainer
  • два экземпляра дочернего компонента, который имеет кнопку и показывает количество нажатий кнопки (в любом случае)

FluxContainer.cmp

<aura:component exstensible="true">
  <aura:registerEvent name="getState"
                      type="e.c:FluxContainer_getState" />
  <aura:handler name="setState" 
                event="c:FluxContainer_setSTate" 
                action="{! c.setState }" />
</aura:component>

FluxContainer.controller.js

({
  setState: function (component, event, helper) {
    var update = event.getParam('update');

    if (
      update.containerName &&
      component.get('v.containerName') &&
      update.containerName == component.get('v.containerName')
    ) {
      helper.setState(
        component,
        update.containerName,
        update.state,
        update.callback
      );
    } else {
      console.error('There was a problem pairing containerNames.');
    }
  }
})

FluxContainer.helper.js

({
  setState : function (component, containerName, state, callback) {
    var res = {
      unavailable_attributes: []
    },
      updates = [];

    for (var param in state) {
      var attr = 'v.' + param;
      var val = state[param];

      if (component.get(attr)) {
        component.set(attr, val);
        updates.push({
          attribute: attr,
          value: val
        });
      } else {
        res.unavailable_attributes.push(param);
      }
    }

    // post the update as an application event
    if (updates.length > 0) {
      var updateEvent = 
          component.getEvent('e.c:FluxContainer_getState');
      updateEvent.setParams({
        containerName: containerName,
        state: updates
      });
      updateEvent.fire();
    }

    // call the optional callback
    callback && callback(res);
  }
})

FluxContainer_getState.evt

<aura:event type="APPLICATION" description="FluxContainer_getState">
    <aura:attribute name="update" type="Object" default="{}"/>
</aura:event>

FluxContainer_setState.evt

<aura:event type="COMPONENT" description="FluxContainer_setState">
    <aura:attribute name="newState" type="Object" default="{}"/>
</aura:event>

Parent.cmp

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

<aura:component description="parent"
                extends="c:FluxContainer">
  <aura:handler name="init" 
                value="{! this }"
                action="{! c.onInit }"

  <aura:attribute name="containerName" 
                  type="String" 
                  default="parentContainer"/>
  <aura:attribute name="someAttribute" 
                  type="Integer" 
                  default="0"/>
  
  <Child />
  <Child />
</aura:component>

Parent.controller.js

Добавьте функцию onInit, которая инициализирует его состояние и транслирует его.

({
  onInit : function (component, event, helper) {
    var init = component.getEvent('c:FluxContainer_getState');
    init.setParams({
      containerName: component.get('v.containerName'),
      state: [
        {
          attribute: 'count', 
          value: component.get('v.count')
        }
      ]
    });
    init.fire();
  }
})

Child.cmp

<aura:component>
  <aura:registerEvent name="addOne" 
                      type="c:FluxContainer_setState"/>
  <aura:handler name="update" 
                event="e.c:FluxContainer_getState" 
                action="{! c.onUpdate }"/>
  <aura:attribute name="count" 
                  type="Integer"
                  default="0"/>
  <button onclick="{! c.onButtonClick }">
    You clicked me or my twin {! v.count } times.
  </button>
</aura:component>

Child.controller.js

Функция onButtonClick отправляет новый счетчик (предыдущий счет плюс один) через событие FluxContainer_setState, которое принимает один параметр, «newState», объект только обновленных элементов (если бы у нас были другие свойства с отслеживанием состояния в контейнере, они не были бы затронутые этим, если мы явно не добавили их).

({
  onButtonClick : function (component, event, helper) {
    // dispatch the new count
    var update = component.getEvent('addOne');
    update.setParams({
      newState: {
        count: component.get('v.count') + 1
      }
    });
    update.fire();
  },

  onUpdate : function (component, event, helper) {
    var state = event.getParam('update').state;
    
    state.count && component.set('v.count', state.count);
  }
})

Этот дочерний компонент явно обрабатывает получение обновлений состояния через onUpdate, но, как и базовый компонент FluxContainer, мы могли бы абстрагировать эту функциональность до базового компонента, который любой дочерний компонент, подобный этому, мог бы расширить для автоматической обработки простых обновлений состояния. Я хотел показать, насколько гибкой может быть эта композиция; если ваш проект достаточно мал, возможно, вам не нужно ничего абстрагироваться, и, возможно, логика может остаться в вашем непосредственном контроллере (и / или помощнике).

Итак, краткое изложение вещей здесь выглядит следующим образом: контейнер верхнего уровня инициализируется с нулевым счетчиком кликов. При инициализации он передает это событие по всему приложению через событие getState. Два дочерних компонента подписываются на это событие и обновляют свои собственные атрибуты тогда и в любое другое время, когда событие getState запускается с этим атрибутом. При нажатии любой из дочерних кнопок они отправляют событие компонента setState с новым значением счетчика. Это значение принимается контейнером потока, обновляется в его собственном атрибуте, а затем повторно публикуется для всего приложения с помощью getState, чтобы другая кнопка (и технически исходная кнопка тоже) могла иметь последнее значение из состояния.

Я надеюсь, что этот пример освещает некоторые возможности компонентов Lightning и структуры потока. Гибкость фреймворка дает множество возможностей, и в действительности все сводится к тому, что нужно вашему проекту, чтобы добиться успеха и быть поддерживаемым. Обдумывайте все причины за и против сокращения состояния всякий раз, когда вы настраиваете каждую деталь. Точно так же рассмотрите все причины, по которым вы настроили именно так. Если вы хотите, чтобы все было беспроводным, используйте события, когда это возможно. Если ваш проект небольшой, рассмотрите возможность прямого связывания свойств и действий через атрибуты. Если у вас есть сложный компонент, вложенный где-то внутри более крупного компонента-контейнера, подумайте о том, чтобы сделать его также контейнером (здесь может быть полезно имя контейнера, чтобы разные контейнеры и дочерние элементы не путались с обновлениями из неправильной иерархии).

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