Мета-приложения и Symbiote.js

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Что такое "meta application"?

Определимся сразу, что мета-приложения и мета-компоненты - это еще не устоявшиеся в индустрии термины. Это скорее предложение, которое может быть принято или отвергнуто сообществом веб-разработчиков. Самое время объяснить, что конкретно мы имеем в виду.

Meta applications - это относительно независимые решения в основной структуре веб-приложения, такие как:

  • Виджеты

  • Микро-фронтенды

  • Элементы UI-библиотек

  • Ириложения-надстройки

  • и так далее...

Это приложения или компоненты, решающие свою выделенную задачу, или часть более общей задачи, которые могут быть простыми или довольно сложными сами по себе, и существуют относительно независимо от архитектур и принципов разработки хост-приложений. Упс, еще один новый термин. Ну с этим должно быть уже проще: host application - это “среда” для мета-приложения, окружение интеграции.

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

Формулируем задачу

Существует простой и распространенный подход к интеграции виджетов: мы даем указать, через созданный нами API, элемент-контейнер, в который и добавляем все необходимое. И все, вроде бы, хорошо, пока не выяснится, что клиент, в данном случае, речь идет о интеграторе - использует стили, которые глобально влияют на все элементы определенного типа на странице... И вот, вы должны позаботиться о инкапсуляции CSS, но таким образом, чтобы дизайн вашего решения можно было настроить снаружи каким-то вменяемым способом... А потом, клиенту потребуется использовать несколько виджетов, на одной странице, да еще и с разными настройками одновременно... А потом вы выясните, что цикл рендеринга хост-приложения влияет на то, как ваш виджет инициализируется, и вам нужно будет объяснить пользователю, как и когда вызывать методы вашего API для того, чтобы не возникало никаких "гонок" и прочих "cannot read property X of undefined". А еще, вам может понадобиться отобразить одну часть интерфейса в одном месте хост-приложения, а другую часть - в другом... В общем, никакие классические и консервативные подходы, достойных ответов, на эти вопросы, не дают.

Подытожим. Для создания виджета, нам необходимо:

  • определиться с "точкой подключения", решить каким образом ваш мета-компонент, в итоге, попадает в DOM. В идеале, интеграция должна быть максимально универсальной и поддерживаться как на уровне прямой манипуляции объектами DOM через JavaScript, так и на уровне шаблонов разметки и генерации документа на сервере.

  • разобраться с инкапсуляцией: решить что, а главное, как, мы прячем "под капот" а что оставляем доступным снаружи, в качестве API. Это касается как стилей, так и логики работы.

  • решить то, каким образом виджет может быть настроен, какие настройки могут быть общими, и как предоставить каждому экземпляру виджета на странице его персональные настройки. И желательно, чтобы это было возможно сделать в разных экосистемах: от динамических JS-приложений до статических HTML-документов.

  • сделать все вышеперечисленное максимально доступным для пользователей способом, не усложняя и не создавая избыточных абстракций над общедоступными возможностями платформы и базовыми паттернами веб-разработки.

  • постараться оставить решение максимально "легким", с вниманием отнестись к потреблению ресурсов.

Путь к решению

Есть луч света в темном царстве виджетостроения - это стандарты Custom Elements и Shadow DOM.

Главное, что дают нам кастомные элементы, в этом контексте, это встроенные методы жизненного цикла. Теперь мы можем точно знать, когда виджет появился в DOM и когда он был удален, независимо от того, кто именно, как и когда добавил его в документ и был ли он там изначально. Одной серьезной проблемой меньше - нам не нужно ничего инициализировать или удалять из памяти вручную и описывать необходимые действия в документации. Помимо этого, кастомные элементы позволяют реализовать доступ к своим API через ссылку на элемент в DOM, с учетом его положения в структуре документа и остального контекста, при этом, не используя никаких более сложных внешних абстракций.

Shadow DOM дает нам возможность надежно защитить наши стили и используемые в JS-коде селекторы от "протечек", как извне, так и наружу, что тоже очень полезно.

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

В принципе, мы можем использовать любую привычную нам библиотеку для разработки "тела" виджета, и потом завернуть результат в Custom Element c Shadow DOM. На этом этапе, стоит вспомнить о производительности и трафике: будет не очень хорошо, если мы используем что-то громоздкое - наши пользователи могут воспринять это негативно, если для них важна эффективность и общий объем загружаемого кода. Вы, наверное, догадываетесь к чему я клоню: React или Vue, как варианты, отпадают первыми - слишком много оверхеда (~ 40K и ~ 20K gzip/br соответственно). Про Angular я вообще молчу. Давайте обозначим рамки: у нас есть кандидаты, чей размер вписывается в ~3-5К gzip, вот на них и обратим свой пристальный взор.

Из довольно популярных, и при этом, "легких" вариантов, можно отметить Svelte и Preact. В экосистемах этих библиотек есть готовые решения для использования с Custom Elements, что добавляет плюсов в их копилку. Но, тут тоже есть свои нюансы, например, не для всех приемлема зависимость от компилятора в Svelte. Вообще все, что использует свои специфические компиляторы отпадает, если вы хотите иметь возможность гибко работать с кодом компонентов в хост-приложении напрямую (они должны уметь собираться как обычный JS). И реализация, с использованием сразу двух, принципиально разных компонентных моделей (уровень библиотеки и Custom Elements) - меня лично, очень смущает, как с архитектурной, так и с концептуальной точки зрения. Поясню: в случае, если мы создаем решение, сочетающее в себе некий набор элементов - каждый такой элемент придется "оборачивать" отдельно, и затем разбираться уже с двумя типами компонентов: внутренними и теми, которые мы отдаем для пользовательской интеграции. Это неудобно и, в определенный момент, может выйти нам боком. Идем дальше.

На этом этапе мы подходим к набору библиотек, специально предназначенных для работы со стандартом Custom Elements, у которых поддержка реализована на уровне генов, где компонент приложения = Custom Element.

Одна из самых популярных и подходящих среди них - это LitElement, от разработчиков из Google. В свое время, я делал выбор именно в ее пользу. Тогда, основные проблемы пришли со стороны поддержки Content Security Policy. Дело в том, что LitElement использует для подключения стилей экспериментальный интерфейс adoptedStyleSheets, который, на текущий момент, не поддерживается в Safari. И, в случае, если поддержки нет, LitElement создает в shadowRoot компонента, тег <style> куда добавляет CSS в виде текста, что, как вы уже, наверное, догадались, конфликтует с настройками CSP в общем случае... Увидеть проблему наглядно, можно сравнив результат работы LitElement на https://lit.dev/playground/ с помощью инструментов разработчика в Safari и Firefox/Chrome. Использовать стратегию nonce или hash, мы не можем, поскольку наше решение - встраиваемое. Говорить своим пользователям, о том, что им необходимо добавлять в настройки флаг unsafe-inline мы посчитаем дурным тоном, и начнем думать о том, как решить вопрос иначе. Вообще, стилизация внутри Shadow DOM - это интересная тема, которой я хочу посвятить отдельную статью. Если кратко: мы можем использовать свою альтернативную реализацию добавления стилей в LitElement и починить конфликт с CSP, или не использовать Shadow DOM вовсе. Но, мы же пришли за готовым решением, верно? Начинаем сомневаться в том, что LitElement - это правильный выбор.

Кстати, для тех, кто задастся вопросом, чего я так прицепился к этим CSP, я покажу вот этот график: https://trends.builtwith.com/docinfo/Content-Security-Policy.

С опытом, мы начинаем видеть и другие недостатки LitElement:

  • обработка шаблонов через механизм Tagged templates - это привязывает определения шаблона к контексту класса компонента и затрудняет манипуляции с шаблонами, даже на уровне примитивного разделения кода.

  • резолвинг модулей через node.js, вместо, поддерживаемых браузерами, относительных путей, что, в режиме разработки, привязывает нас к специальному серверу и не дает использовать компоненты "на лету" в по настоящему "сыром" виде. Да, мы знаем про import-map, как и про то, что это нигде, кроме движков, основанных на Chromium, нативно не поддерживаются.

  • нет встроенного решения для организации взаимодействий между разными частями мета-приложения: есть управление данными "внутри" мета-компонентов но нет "снаружи".

  • общее движение от близости к нативным API в сторону усложнения: с каждым обновлением документации ты превращаешься в "LitElement-разработчика", хотя желал простоты и дзена.

В любом случае, все эти проблемы, так или иначе, решаемы. И для каждой, скорее всего, найдется даже сразу несколько возможных направлений поиска решений. Но, тут всплывает резонный вопрос: если мы уже начали лепить надстройки и кастомизации для нашей базовой библиотеки, почему бы не перестать "бороться с ветряными мельницами", и не создать "уницикл", который будет ИЗНАЧАЛЬНО ПОЛНОСТЬЮ СООТВЕТСТВОВАТЬ нашим нуждам?

Решение

Итак, встречайте: Symbiote.js - библиотека, специально созданная для мета-приложений Конечно же, это свободное ПО, лицензия MIT.

Как следует из названия, Symbiote - это про симбиоз. Для того, чтобы симбиоз стал возможен, при разработке был применен принцип максимального приближения к веб-платформе и ее нативным API, при сохранении достаточного уровня удобства. Symbiote.js - изначально предназначен для создания приложений со слабосвязанной архитектурой, которая упрощает интеграцию в широкий набор сред и окружений. Symbiote.js дает вам высокий уровень свободы, что, конечно же, подразумевает и высокий уровень ответственности. Несмотря на общую внешнюю схожесть (динамические привязки данных в шаблонах, состояния, жизненный цикл), Symbiote.js довольно сильно отличается от своих более известных коллег концептуально. И все дело именно в этих отличиях:

  • Шаблоны - это HTML. Буквально, шаблоном в Symbiote.js считается то, что браузер может сам преобразовать в объектную модель без ошибок и дополнительных действий с нашей стороны. Весь синтаксис шаблонов основан на HTML-тегах и их атрибутах, доступных для пост-обработки через самый обычный DOM API. Таким образом, в отличие от JSX, или даже lit-html, шаблоны могут представлять из себя обычные шаблонные литералы (строки) или отдельные HTML-файлы (если вы используете HTML-лоадер для вашего сборщика).

  • Данные определяются контекстом. В Symbiote.js, из коробки, есть поддержка как работы с локальными данными компонента, так и с данными из общедоступных контекстов: абстрактных (named) или сформированных с учетом положения компонента в DOM-дереве хост-приложения. Инициализация компонентов происходит после того, как они попадают в DOM, и, образно говоря, первым делом Symbiote-компонент задает вопросы: "так, куда я попал?" и "кто вокруг меня?"

  • Pub/sub - работа с данными реализована через простейший, как для понимания, так и в использовании, паттерн.

  • Shadow DOM - это опция, выключенная по умолчанию. Теневой документ - отличный и очень мощный инструмент, дающий виджетостроителю многое. Но иногда, он-же является и источником проблем. В концепции Symbiote.js - Shadow DOM - это важная, но далеко не обязательная часть. Лично я, предпочитаю создавать теневой DOM только на "внешних рубежах", не усложняя стилизацию внутри компонентов и не добавляя лишней работы браузеру.

  • Синхронные динамические обновления DOM. Пакеты обновлений не нужно накапливать, чтобы потом синхронизировать их с DOM более эффективно. Симбиоту не требуется отдельный, и довольно затратный этап сравнения для внесения изменений, поскольку в нем нет Virtual DOM или каких-либо аналогов этого механизма. Для обработки участка DOM, во время инициализации шаблона, используется обычный DocumentFragment.

  • Этап сборки - не является необходимым. Вы можете писать свой код и сразу видеть результат в браузере, без установки каких-либо специфических зависимостей: компиляторов, специальных dev-серверов и прочего. Также, вы можете использовать и любой, привычный вам, стек или подход. Выбор за вами. Можно тестировать как все приложение, так и отдельные его компоненты, без дополнительных настроек в проекте и сайд-эффектов окружения.

  • Поддержка объектной модели документа. Symbiote.js не создает искусственных барьеров между DOM и вашим кодом в компонентах, а напротив, предоставляет прямой и удобный доступ.

  • Прогрессирующая сложность. Согласно концепции "Progressive Complexity", простые задачи должны иметь такое-же простое решение. А для решения задач сложных, не должно быть никаких концептуальных либо архитектурных ограничений. И в Symbiote.js все именно так.

  • "HTML as low-code". Симбиот стимулирует перенос взаимодействий компонентов с окружением на уровень HTML и CSS, туда, где за все отвечает браузер, а не какая-либо особенная, для конкретного стека, абстракция. Симбиот позволяет строить мощный и гибкий API на уровне самых базовых сущностей платформы.

  • "CSS Context Properties" - вы можете инициализировать компоненты с теми данными, которые сформированы CSS-контекстом в каждом конкретном месте общего документа. Этот контекст может как наследоваться так и переопределяться на различных уровнях согласно каскадной модели.

  • Технологический агностицизм в генах. Это касается экосистем, рантайма и внешних зависимостей, которых в Symbiote.js - нет.

Примеры кода

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

Для подсветки HTML и CSS синтаксиса внутри шаблонных литералов, вы можете использовать специальное расширение для вашей IDE, например это: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html

Простой пример, не требующий установки:

<script type="module">
  import { BaseComponent } from 'https://symbiotejs.github.io/symbiote.js/core/BaseComponent.js';

  class MyComponent extends BaseComponent {
    init$ = {
      count: 0,
      increment: () => {
        this.$.count++;
      },
    }
  }

  MyComponent.template = /*html*/ `
    <h2>{{count}}</h2>
    <button set="onclick: increment">Click me!</button>
  `;

  MyComponent.reg('my-component');
</script>

<my-component></my-component>

Этот код можно просто скопировать в HTML-файл и открыть в браузере.

Более сложный пример, где есть динамический рендеринг таблицы и общий Shadow DOM контейнер:

import { BaseComponent } from 'https://symbiotejs.github.io/symbiote.js/core/BaseComponent.js';

//// Dynamic list item component:
class TableRow extends BaseComponent {}

TableRow.template = /*html*/ `
  <td>{{rowNum}}</td>
  <td>Random number: {{randomNum}}</td>
  <td>{{date}}</td>
`;

TableRow.reg('table-row');

//// Dynamic list wrapper component:
class TableApp extends BaseComponent {
  
init$ = {
	tableData: [],
  buttonActionName: 'Generate',
  generateTableData: () => {
    this.$.buttonActionName = 'Update';
    let data = [];
    for (let i = 0; i < 1000; i++) {
      data.push({
        rowNum: i + 1,
        randomNum: Math.random() * 100,
        date: Date.now(),
      });
    }
    this.$.tableData = data;
  },
}

TableApp.shadowStyles = /*css*/ `
	table-row {     
		display: table-row;
 	}
 	td {     
 		border: 1px solid #f00;
 	}
`;

TableApp.template = /*html*/ `
  <button set="onclick: generateTableData">{{buttonActionName}} table data</button>

  <table
    repeat="tableData"
    repeat-item-tag="table-row">
  </table>
`;

TableApp.reg('table-app');

Пример с определением шаблона в разметке за пределами компонента:

<script type="module">
  import { BaseComponent } from 'https://symbiotejs.github.io/symbiote.js/core/BaseComponent.js';

  class MyComponent extends BaseComponent {

    // Enable external template usage:
    allowCustomTemplate = true;

    init$ = {
      title: 'Title',
      clicks: 0,
      onClick: () => {
        this.$.clicks++;
      },
    };
  }

  MyComponent.reg('my-component');
</script>

<template id="external-template">
  <h1>{{title}}</h1>
  <div>{{clicks}}</div>
  <button set -onclick="onClick">Click me!</button>
</template>

<my-component use-template="#external-template"></my-component>

Обратите внимание, что в первом и последнем примерах, обработчики нажатий на кнопку привязываются к шаблону по разному: Symbiote.js поддерживает два типа синтаксиса привязок, каждый из которых более удобен в определенных случаях. О том, почему это так, я более подробно расскажу в одном из следующих материалов.

Symbiote.js поддерживает TypeScript и может одинаково свободно использоваться как в TypeScript, так и в JavaScript проектах.

Заключение

На данный момент, Symbiote.js протестирован и хорошо себя зарекомендовал в достаточно сложных и разнообразных ситуациях. Но, конечно, это только начало пути, и мы находимся в самом начале формирования сообщества и экосистемы. Документация будет совершенствоваться, примеры будут пополняться, полезные инструменты разработки будут появляться.

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

В общем, ждем вас в/на GitHub, будем благодарны за "звездочки" и любую активность в обсуждениях. Всем - добра и мира.

Источник: https://habr.com/ru/post/682580/


Интересные статьи

Интересные статьи

Рано или поздно, каждый пэхапешник, пишущий на битриксе, начинает задумываться о том, как бы его улучшить, чтобы и всякие стандарты можно было соблюдать, и современные инструменты разработки использов...
Предыстория Когда-то у меня возникла необходимость проверять наличие неотправленных сообщений в «1С-Битрикс: Управление сайтом» (далее Битрикс) и получать уведомления об этом. Пробле...
Но если для интернет-магазина, разработанного 3–4 года назад «современные» ошибки вполне простительны потому что перед разработчиками «в те далекие времена» не стояло таких задач, то в магазинах, сдел...
1С Битрикс: Управление сайтом (БУС) - CMS №1 в России по версии портала “Рейтинг Рунета” за 2018 год. На рынке c 2003 года. За это время БУС не стоял на месте, обрастал новой функциональностью...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.