- PVSM.RU - https://www.pvsm.ru -

Реализация реактивности и компонуемости стандартными средствами таких фреймворков, как React, Vue и прочие, несёт собой ряд сложностей, включая необходимость настройки множества зависимостей. Но этой цели также можно достичь более простым путём, о чём и пойдёт речь в текущей статье.
Для начала небольшое уточнение. Под фреймворком я подразумеваю систему, которая позволяет избегать необходимости написания стандартного HTML и JS-кода вроде такого:
<p id="cool-para"></p>
<script>
const coolPara = 'Lorem ipsum.';
const el = document.getElementById('cool-para');
el.innerText = coolPara;
</script>
Вместо этого он даёт возможность писать магический HTML и JS-код, наподобие такого (Vue [1]):
<script setup>
const coolPara = 'Lorem ipsum.';
</script>
<template>
<p>{{ coolPara }}</p>
</template>
Или такого (React [2]):
export default function Para() {
const coolPara = 'Lorem ipsum';
return <p>{ coolPara }</p>;
}
Преимущества здесь вполне понятны. Запоминать слова или фразы вроде document, innerText и getElementById трудно – очень уж много слогов.
Хотя, естественно, количество слогов здесь не самое главное.
Первая основная причина в том, что во втором и третьем примерах можно просто установить или обновить значение переменной coolPara, и разметка – т.е. элемент <p> – обновится без необходимости явной установки её innerText.
Это называется реактивность – UI привязан к данным таким образом, что простое их изменение ведёт также и к его обновлению.
Вторая основная причина – это возможность определять компонент и переиспользовать его без необходимости повторного определения в каждом месте, где он потребуется. Это называется компонуемость.
Стандартный HTML + JavaScript по умолчанию не обладает такой возможностью, поэтому следующий код не делает того, что по ощущениям должен:
<!— Определение компонента —>
<component name="cool-para">
<p>
<content />
</p>
</component>
<!— Использование компонента —>
<cool-para>Lorem ipsum.</cool-para>
Реактивность и компонуемость являются двумя основными преимуществами, которые нам предоставляют обычные фронтенд-фреймворки, такие как Vue, React и прочие.
Но эти абстракции даются не столь легко. Разработчику требуется предварительно загрузить множество специфичных для фреймворка компонентов, разобраться с их нестыковками, в результате которых что-то может работать непостижимым странным образом, и плюсом ко всему наладить кучу уязвимых для сбоев зависимостей.
Однако оказывается, что при использовании современных Web API реализовать эти две функциональные возможности не так уж трудно. И в большинстве случаев нам не потребуются обычные фреймворки со всеми своими сложностями.
Простым языком, реактивность объясняется как автоматическое обновление пользовательского интерфейса вслед за обновлением данных.
Первым делом нужно узнать, когда данные обновляются. К сожалению, это в возможности стандартных объектов не входит. Нельзя просто прикрепить слушателя ondataupdate для отслеживания событий обновления данных.
Зато в JavaScript как раз есть элемент, который позволит это сделать – Proxy [3].
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.
Если вы думаете: «Да что здесь такого, я мог проделать это с помощью старого-доброго сеттера [4]». Объясню в чём тут суть:
proxy обобщается, и обработчики можно использовать повторно. Это означает, что…
Помимо этого, вы можете обрабатывать [5] несколько других событий доступа, например, связанных со свойствами read [6], updated [7], deleted [8] и так далее.
Теперь, когда у вас есть возможность прослушивать операции, необходимо реализовать осмысленное на них реагирование.
Если помните, то второй частью реактивности выступает автоматическое обновление UI. Для этого нам нужно получить соответствующий элемент UI, требующий обновления. Но прежде необходимо отметить этот элемент как подходящий.
Это мы сделаем с помощью атрибутов data-* [9], функционала, который позволяет устанавливать для элемента произвольные значения:
<div>
<!-- Mark the h1 as appropriate for when "name" changes -->
<h1 data-mark="name"></h1>
</div>
Приятная особенность атрибутов 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-компоненты [10]. Правда, ей мало кто пользуется, потому что это доставляет определённые сложности (а также, потому что обычно большинство разработчиков с самого начала проекта задействуют привычные фреймворки).
Для реализации компонуемости сначала необходимо определить компоненты.
Теги <template> используются для хранения не отображаемой браузером разметки. К примеру, вы можете добавить к себе в HTML следующий её вариант:
<template>
<h1>Will not render!</h1>
</template>
И она отображаться не будет. Эти теги можно рассматривать как невидимые контейнеры для компонентов.
Следующим составляющим является элемент <slot>, который определяет место расположения содержимого в компоненте. Это позволяет повторно использовать компонент с другим содержимым, по сути, делая его компонуемым.
Например, вот элемент h1, окрашивающий свой текст в красный.
<template>
<h1 style="color: red">
<slot />
</h1>
</template>
Прежде, чем перейти к использованию наших компонентов, таких как красный h1 выше, их нужно зарегистрировать.
Для регистрации же компонента его необходимо проименовать, что можно сделать с помощью атрибута name:
<template name="red-h1">
<h1 style="color: red">
<slot />
</h1>
</template>
И теперь с помощью JS-кода можно получить компонент по его имени:
const template = document.getElementsByTagName('template')[0];
const componentName = template.getAttribute('name');
После чего, наконец, зарегистрировать его через customElements.define [11]:
customElements.define(
componentName,
class extends HTMLElement {
constructor() {
super();
const component = template.content.children[0].cloneNode(true);
this.attachShadow({ mode: 'open' }).appendChild(component);
}
}
);
В блоке выше происходит очень многое:
customElements.define с двумя аргументами;
В этом конструкторе класса мы используем копию шаблона red-h1 для установки теневого дерева DOM.
Теневая DOM элемента по умолчанию скрыта, в связи с чем на панели разработчика не отображается. Но здесь мы устанавливаем её режим как open.
Это позволяет проинспектировать элемент и увидеть, что красный h1 прикреплён к #shadow-root [14].
Вызов customElements.define позволит использовать определённый компонент как стандартный HTML-элемент.
<red-h1>This will render in red!</red-h1>
Теперь пора объединить две рассмотренные концепции.
К этому моменту мы проделали две вещи:
red-h1, который будет отображать своё содержимое как red h1.Теперь всё это можно объединить:
<div>
<red-h1 data-mark="name"></red-h1>
</div>
<script>
const user = new Proxy({}, handler);
user.name = 'Lin';
</script>
Вот мы и реализовали отображение наших данных кастомным компонентом, который также будет обновлять UI при их дальнейшем изменении.
Естественно, типичные фронтенд-фреймворки просто так этого не делают. В них есть специализированный синтаксис, такой как шаблоны [15] в Vue или JSX [16] в React, который позволяет писать код фронтенда в более сжатой форме.
Поскольку этот специализированный синтаксис не является стандартным JS или HTML, он не парсится браузерами, в связи с чем требует отдельных инструментов для своей компиляции в обычный JS, HTML и CSS. Поэтому больше никто и не пишет JavaScript [17].
Даже без специализированного синтаксиса вы можете реализовать очень многие возможности типичного фронтенд-фреймворка, добившись аналогичной лаконичности кода просто за счёт использования Proxy и WebComponents.
Приведённый в этой статье код очень упрощён, и для становления полноценным фреймворком требует доработки. Приведу ссылку на свой пример подобного проекта фреймворка под названием Strawberry [18].
В процессе его разработки я планирую придерживаться двух твёрдых требований:
А также следовать мягкому требованию по сохранению небольшого размера базы кода. На момент написания статьи этот фреймворк представляет собой всего один файл [19], содержащий менее 400 строк кода [20]. Посмотрим, что получится в итоге ✌️
Автор: Дмитрий Брайт
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/384993
Ссылки в тексте:
[1] Vue: https://vuejs.org/guide/scaling-up/sfc.html#introduction
[2] React: https://react.dev/learn/your-first-component#defining-a-component
[3] Proxy: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
[4] сеттера: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set
[5] обрабатывать: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy#handler_functions
[6] read: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get
[7] updated: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set
[8] deleted: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/deleteProperty
[9] атрибутов data-*: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
[10] Web-компоненты: https://developer.mozilla.org/en-US/docs/Web/Web_Components
[11] customElements.define: https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define
[12] диапазон ввода: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range
[13] видео: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video
[14] #shadow-root: https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
[15] шаблоны: https://vuejs.org/guide/essentials/template-syntax.html
[16] JSX: https://react.dev/learn/writing-markup-with-jsx
[17] больше никто и не пишет JavaScript: https://fly.io/blog/js-ecosystem-delightfully-wierd#nobody-writes-javascript-any-more
[18] Strawberry: https://18alan.space/strawberry
[19] один файл: https://github.com/18alantom/strawberry/blob/52cc4e3c88924d112559d0547c533c1fafa61140/index.ts
[20] менее 400 строк кода: https://github.com/AlDanial/cloc
[21] Image: http://ruvds.com/ru-rub?utm_source=habr&utm_medium=article&utm_campaign=Bright_Translate&utm_content=realizaciya_reaktivnosti_i_komponuemosti_vo_frontend-frejmvorke_bez_zavisimostej
[22] Источник: https://habr.com/ru/companies/ruvds/articles/737192/?utm_source=habrahabr&utm_medium=rss&utm_campaign=737192
Нажмите здесь для печати.