[Перевод] Реализация реактивности и компонуемости во фронтенд-фреймворке без зависимостей

vzszpcv3n6ksavg4iol5e0zevsc.png


Реализация реактивности и компонуемости стандартными средствами таких фреймворков, как React, Vue и прочие, несёт собой ряд сложностей, включая необходимость настройки множества зависимостей. Но этой цели также можно достичь более простым путём, о чём и пойдёт речь в текущей статье.

Для начала небольшое уточнение. Под фреймворком я подразумеваю систему, которая позволяет избегать необходимости написания стандартного HTML и JS-кода вроде такого:


Вместо этого он даёт возможность писать магический HTML и JS-код, наподобие такого (Vue):



Или такого (React):

export default function Para() {
  const coolPara = 'Lorem ipsum';
  return 

{ coolPara }

; }


Преимущества здесь вполне понятны. Запоминать слова или фразы вроде document, innerText и getElementById трудно — очень уж много слогов.

Хотя, естественно, количество слогов здесь не самое главное.

Первая основная причина в том, что во втором и третьем примерах можно просто установить или обновить значение переменной coolPara, и разметка — т.е. элемент

 — обновится без необходимости явной установки её innerText.

Это называется реактивность — UI привязан к данным таким образом, что простое их изменение ведёт также и к его обновлению.

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

Стандартный HTML + JavaScript по умолчанию не обладает такой возможностью, поэтому следующий код не делает того, что по ощущениям должен:



  

Lorem ipsum.


Реактивность и компонуемость являются двумя основными преимуществами, которые нам предоставляют обычные фронтенд-фреймворки, такие как Vue, React и прочие.

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

Однако оказывается, что при использовании современных Web API реализовать эти две функциональные возможности не так уж трудно. И в большинстве случаев нам не потребуются обычные фреймворки со всеми своими сложностями.

Реактивность


Простым языком, реактивность объясняется как автоматическое обновление пользовательского интерфейса вслед за обновлением данных.

Первым делом нужно узнать, когда данные обновляются. К сожалению, это в возможности стандартных объектов не входит. Нельзя просто прикрепить слушателя ondataupdate для отслеживания событий обновления данных.

Зато в JavaScript как раз есть элемент, который позволит это сделать — Proxy.

▍ Объекты Proxy


Proxy позволяет создавать из стандартного объекта прокси-объект:

const user = { name: 'Lin' };
const proxy = new Proxy(user, {});


И этот прокси-объект способен прослушивать события изменения данных.

В примере выше у нас присутствует объект Proxy, но пока что, узнав об изменении name, он ничего не предпринимает.

Чтобы это исправить, нам необходим обработчик, представляющий объект, который сообщает прокси-объекту, что делать в случае изменения данных.

// Обработчик, прослушивающий операции присваивания данных
const handler = {
  set(user, value, property) {
    console.log(`${property} is being updated`);
    return Reflect.set(user, value, property);
  },
};

// Создание proxy с обработчиком
const user = { name: 'Lin' };
const proxy = new Proxy(user, handler);


Теперь при каждом обновлении name с помощью прокси-объекта мы будем получать сообщение: name is being updated.

Если вы думаете: «Да что здесь такого, я мог проделать это с помощью старого-доброго сеттера». Объясню в чём тут суть:

  • Метод proxy обобщается, и обработчики можно использовать повторно. Это означает, что…
  • Любое значение, которое вы устанавливаете на проксируемый объект, можно рекурсивно конвертировать в прокси, а значит…
  • Теперь у вас есть тот самый магический объект с возможностью реагировать на обновление данных, вне зависимости от степени их вложенности.


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

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

▍ Обновление UI


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

Это мы сделаем с помощью атрибутов data-*, функционала, который позволяет устанавливать для элемента произвольные значения:


Приятная особенность атрибутов data-* в том, что теперь мы можем найти все подходящие элементы с помощью:

document.querySelectorAll('[data-mark="name"]');


Далее мы просто устанавливаем innerText всех этих элементов:

const handler = {
  set(user, value, property) {
    const query = `[data-mark="${property}"]`;
    const elements = document.querySelectorAll(query);

    for (const el of elements) {
      el.innerText = value;
    }

    return Reflect.set(user, value, property);
  },
};

// Стандартный объект опускается, так как не нужен.
const user = new Proxy({ name: 'Lin' }, handler);


В этом и состоит суть реактивности.

Ввиду общей специфики нашего handler, для любого установленного свойства user будут обновляться все подходящие элементы UI.

Вот такими мощными возможностями обладает Proxy — за счёт применения некоторой смекалки он способен предоставить нам магические реактивные объекты при полном отсутствии зависимостей.

Теперь перейдём ко второй важной концепции.

Компонуемость


Как оказывается, в браузерах уже есть для этого отдельная функция — Web-компоненты. Правда, ей мало кто пользуется, потому что это доставляет определённые сложности (а также, потому что обычно большинство разработчиков с самого начала проекта задействуют привычные фреймворки).

Для реализации компонуемости сначала необходимо определить компоненты.

▍ Определение компонентов с помощью template и slot


Теги