Привет Хабр!

Меня зовут Алекс, и я автор фронтенд-библиотеки для создания UI-компонентов-агностиков - Symbiote.js. Я не единственный разработчик, но главный контрибьютор и тот, кто отвечает за концепцию, развитие, документацию, деврел, DX все остальное. Мейнтейнер то есть. Всем этим я занимаюсь в свободное от другой работы время, на которой я фуллстек, R&D-инженер и техлид.

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

React

Свое первое приложение на React я делал много лет назад, когда тот еще не имел статуса лидирующего решения в отрасли и либы по дефолту. В те славные времена, я работал в команде стартапа, который создавал приложение для живого общения врачей с пациентами. Я пришел в проект на том этапе, когда базовые архитектурные решения уже были приняты, а технологии окончательно выбраны. Мое погружение в React было в жестком боевом режиме, когда, заодно с кодом, я прорабатывал и UX. Мои коллеги рассказывали мне какой React крутой концептуально, а мне не давала покоя одна мысль (ок, далеко не одна)...

Синтаксис JSX. Ведь это язык разметки. Для этого у нас ведь уже есть HTML. А еще, умные дяди и тети нас учили, что логику хорошо бы отделять от представления. А тут, все в кучу, и еще и с многоуровневыми взаимными вложениями... IDE глючит. Новый формат файлов, пока не очень хорошо поддерживается и подсветка синтаксиса постоянно ломается. Ломается и мозг, в сложных случаях. Да, да, я знаю, декомпозиция, но я-же работаю "в команде" а в команде, на декомпозицию могут быть разные взгляды.

А можно было получить, практически то-же самое, и обойтись простыми и ванильными HTML и JS?

Хорошо, держим этот вопрос в уме и идем дальше.

Virtual DOM. Да, нативный DOM браузера медленный, допустим. Это не совсем так, но допустим. Но разве, на этапе синхронизации виртуального DOM с настоящим браузерным, мы не используем тот-же самый DOM API, который, до этого, назвали медленным? То есть, мы родили новую сущность, которая не заменяет собой то, с чем мы "боролись", но теперь сама жрет память и другие ресурсы?

А можно было написать либу так, чтобы и с DOM работать эффективно, и новый ресурсоемкий огород ради этого не городить?

Это второй вопрос, который мы держим в уме.

Состояние. Есть локальное состояние компонента, с ним, вроде бы, все просто. А есть глобальное состояние приложения. Оно может быть довольно сложным, и само по себе, может быть подвергнуто декомпозиции: разделено на элементы, каждый из которых может иметь свои связи с остальными элементами.

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

Плюсом ко всему, в работе с данными важна такая вещь как "согласованность" - у вас не должно возникать ситуаций, когда связанные сегменты состояния опираются на разные версии данных в один момент времени.

Но сами данные, при этом, могут иметь совершенно разные источники правды и причины динамических обновлений: что-то прилетает с сервера, что-то зависит от действий пользователя, а что-то получается через какой-нибудь сторонний API. Тотальная асинхронность - одна из основных сложностей фронтенда, всякие там race conditions и прочее такое.

На тот момент, Redux, как частичное решение вышеописанных проблем, уже существовал. А всякие обертки, для уменьшения бойлерплейта - еще нет. Контекстов тоже, как вы понимаете, не было. И мне было больно на это смотреть.

Рождались очередные вопросы. Типа такого: если в библиотеку уже встроен базовый механизм реактивности, почему не сделать его абстрактным, чтобы он-же решал и проблемы глобального стейта, без лишних зависимостей и адаптеров к этим зависимостям?

Svelte

У Svelte, в свое время, был довольно агрессивный маркетинг. Нам говорили, что это "исчезающий фреймворк". То есть, мы придумали для вас еще один, новый формат файлов, который, при обработке специальным компилятором, превращается в... чистый JavaScript! Фантастика!

А разве нельзя, кхм... ну, сразу писать на чистом JavaScript? Ну чтобы не тащить в проект новый (очередной) компилятор, новый (очередной) тип файлов, новый (очередной) синтаксис? Что, совсем-совсем нельзя без "черных ящиков"?

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

Next.js

Как-то раз, React-разработчики поняли, что полностью динамический SPA-сайт - это не всегда очень хорошо. Проблемы с индексацией, скоростью загрузки ассетов и с первичной отрисовкой UI... Бэкендеры смеются и тычут пальцем. Обидно. И родилась идея: а давайте притащим React еще и на бэкенд! Будем делать SSR! А для динамической части страницы, будем применять гидрацию. Или даже гидратацию. Вставим в разметку специальные маркеры и плейсхолдеры и оживим их на фронте в самый нужный момент. Красиво?

А у меня, что? Правильно, очередные вопросы.

А зачем, собственно, React на сервере?

Для формирования всего документа и его частей (а для сервера это, тупо, строки), нам достаточно простых советских... шаблонных литералов. А для вызова определенного поведения, определенных участков документа на клиенте, нет ничего лучше стандарта Custom Elements: вставил такой в любое место разметки - а он сам активировался, посмотрел вокруг через DOM API и реализовал любой интерактив.

И опять вопрос: а можно было решить вопрос на уровне базовых возможностей платформы и самого языка? Зачем городить очередной "черный ящик", который порождает самые неожиданные сложности в самый неподходящий момент? Зачем, в очередной раз, изобретать то, что есть и так из коробки и бесплатно?

Да, я понимаю, есть еще роутинг, права, кэш, всякие другие ассеты, кроме HTML... Но React то здесь каким боком помогает? Никаким. Просто мы не хотим знать ничего кроме React.

Lit

Выше я упомянул Custom Elements. А где Custom Elements - там и Lit: главная библиотека для работы с веб-компонентами от Гугл. Хорошая штука, которая выросла из проекта Polymer. Когда-то, ребята из Гугл сказали "Use the Platform!", и это был свет. Заодно, они чуть пропатчили саму "Platform", чтобы этот "Use" не вызывал много "Pain". Я искренне благодарен им за это.

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

А я хочу спросить: а можно было без этого? Ну чтобы шаблоны формировать и хранить как угодно, включая возможность использовать и оживлять разметку самого материнского документа?

Искусство компромиссов

Инжиниринг - это искусство компромиссов. Грамотное инженерное решение - это взвешивание большого количества "ЗА" и "ПРОТИВ", причем как для общего, так и для частных случаев. Мы все люди, и иногда, склонны переоценивать или недооценивать какие-то "ЗА" и какие-то "ПРОТИВ". Часть факторов может ускользать от нашего взора, по различным причинам. Любую технологию можно критиковать. Любая технология и решение - могут быть хороши в своем контексте, который может быть не всем очевиден и не до конца понятен людям со стороны.

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

Моим ответом на эти вопросы стал Symbiote.js.

И да, так было можно.

P. S. Конечно же, я перечислил далеко не все вопросы и не все популярные технологии, которые эти вопросы вызывают. Надеюсь, у вас тоже есть вопросы, которыми вы поделитесь в комментариях.

Комментарии (16)


  1. tuxi
    27.09.2024 11:06

    Я так и не понял, в итоге Symbiote.js умеет в SSR или нет?


    1. i360u Автор
      27.09.2024 11:06
      +1

      1. tuxi
        27.09.2024 11:06

        Вопрос от нефронтендера и не совсем по теме статьи:

        При прочих равных, на какой из существующих сейчас технологий фронтенда легче всего сделать наиболее производительный SSR ?


  1. Gromilo
    27.09.2024 11:06
    +1

    А зачем, собственно, React на сервере?

    Я не настоящий фронтерндер, но понимаю так: один и тот же код выполняется и на клиенте и на сервере.

    Например, есть страница каталога. Её можно открыть сразу со всем содержимым, а можно по навигации перейти на неё и получить данные запросом к апи. А код хождения в апи один и тот же.


    1. i360u Автор
      27.09.2024 11:06
      +1

      А код хождения в апи один и тот же.

      Но React то здесь причем? Код хождения в API и рисование UI - это же вещи ортогональные. Вам никто не мешает использовать общий код и без React.


      1. Gromilo
        27.09.2024 11:06
        +1

        И средства отрисовки одни и те же, и стейт перетекает бесшовное. Т.е. ещё и шаблоны одинаковые.


        1. i360u Автор
          27.09.2024 11:06
          +2

          Да, шаблоны одинаковые. Почти. Но если вы хотите использовать HTML а не JSX, то не одинаковые. Я пишу о том, что JSX - это сущность, которую можно сократить без особого ущерба для дела. Но React-разработчикам, конечно, удобнее когда вокруг один сплошной React. С этим я не спорю.


          1. Gromilo
            27.09.2024 11:06

            Спасибо за разъяснения

            По мотивам:


  1. stanukih
    27.09.2024 11:06

    Нужно сравнение от @nin-jin


    1. i360u Автор
      27.09.2024 11:06
      +1

      О, да. Этот сравнит.


    1. Mr_FatCat
      27.09.2024 11:06
      +1

      Думаю, стоит найти спеца с уровнем повыше, для адекватного сравненя...


  1. fertilis
    27.09.2024 11:06

    Рутинг получается надо делать вызывая AppRouter.applyRoute() при клике?


    1. i360u Автор
      27.09.2024 11:06
      +1

      Либо так, либо просто обычный переход по нужному урлу. Работает просто через адресную строку.


  1. savostin
    27.09.2024 11:06

    Открыл, увидел

    <button ${{onclick: 'increment'}}>Click me!</button>

    закрыл. Функция по имени - детский сад какой-то.


    1. i360u Автор
      27.09.2024 11:06

      Я очень рад, что вы так быстро во всем разобрались. Однако, приведенный вами пример - это то, как работает функция-хелпер (ее использование не обязательно), которая преобразует объектную нотацию в следующий вид:

      <button bind="onclick: increment">Click me!</button>

      А это уже - просто HTML-строка, которая использует значения HTML-атрибутов для описания биндингов. Чем эта строка интересна? А тем, что она парсится нативным браузерным парсером и преобразуется в DOM без использования каких-либо дополнительных обработок. А также, может быть полностью независима от контекстов экземпляров компонентов. Может формироваться любым способом, хоть на сервере, хоть на клиенте. Ну, то есть, это бесплатный SSR, если вы понимаете о чем я говорю.

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


      1. savostin
        27.09.2024 11:06

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