Привет, Хабр! Меня зовут Дмитрий Переверза, я Frontend Team Lead в компании Just AI. В рамках платформенного стрима мы занимаемся разработкой и развитием платформы для создания своих чат‑ботов. Cделать хорошего и полезного бота временами бывает сложно, поэтому для помощи разработчикам мы создаем инструменты, которые помогают ускорить разработку и упростить работу с ботами. В этой статье я расскажу, как реализовать изолированный UI, грамотно организовать код на примере виджета чата, и какие проблемы могут возникнуть в процессе разработки.
В мире разработки, как и в жизни, мы постоянно балансируем между сотрудничеством и самостоятельностью: каждый компонент системы, как человек в обществе, хочет быть особенным и уникальным, но при этом быть готовым к сотрудничеству с другими. Мы исследуем технический подход, который помогает применить эту философию на практике: как создать программный элемент, который будет самостоятелен, но в то же время будет хорошо взаимодействовать с остальной системой.
Все начинается с задачи
Сам кейс и эта статья появились благодаря простой задаче — разместить чат-виджет на сайте. Рассмотрим, как все устроено и с какими проблемами мы столкнулись.
Чат-виджет — это небольшой модуль, который загружается на страницу по определенному URL. При загрузке виджет инициализирует себя, используя некоторые переменные из глобального окружения страницы.
Проблема: если на странице будет несколько таких виджетов, они будут использовать одни и те же глобальные переменные. Это приведет к конфликтам и непредсказуемому поведению, потому что каждый виджет может перезаписывать значения, которые нужны другим виджетам.
Решение проблемы изоляции виджета
Для решения проблемы конфликтов глобальных переменных и обеспечения изоляции виджета мы используем простой и проверенный подход — загружаем виджет в отдельный iframe.
iFrame (Inline Frame, фрейм) — это код HTML, используемый для встраивания интерактивных медиа, сторонних страниц в ваш сайт. iFrame создает плавающий фрейм (отдельное окно html документа), который находится внутри обычного документа — он позволяет загружать в область заданных размеров любые другие независимые документы, видео и интерактивные медиафайлы на вашу страницу.
Фрейм может интегрировать контент в любом месте на вашей странице без необходимости включать их в структуру веб-макета, как традиционный элемент. Это позволяет виджету работать в собственном контексте, не влияя на глобальные переменные и код основной страницы. В iframe мы можем использовать свои собственные глобальные переменные, которые не будут влиять на переменные первой страницы.
Как это работает?

Создание отдельной страницы для виджета
Вместо того, чтобы внедрять весь код виджета непосредственно в основное приложение, мы создаем отдельную веб-страницу, которая содержит весь необходимый код виджета. На схеме эта страница обозначена как «Our Page for iframe».
2) Загрузка виджета через iframe
Основное приложение (Router) не включает код виджета напрямую, а просто отображает iframe, который загружает эту отдельную страницу по URL.
3) Изоляция глобальных переменных
Поскольку iframe создает отдельный контекст выполнения, все глобальные переменные, функции и состояния внутри виджета находятся в изоляции от основной страницы.
Это значит, что:
Глобальные переменные виджета не будут перезаписывать или конфликтовать с глобальными переменными основной страницы
Код виджета не сможет случайно изменить состояние или поведение основного приложения
Можно безопасно размещать несколько таких виджетов на одной странице, каждый в своем iframe, без риска конфликтов
Это решает проблему с перезаписью глобальных переменных, но создает другую: теперь мы не можем взаимодействовать с виджетом из главного окна.
Как восстановить обмен данными между iframe и главным окном?
Для этого используем механизм postMessage. Механизм позволяет отправлять сообщения между окнами, даже если они находятся на разных страницах. Это безопасный и контролируемый способ передачи данных между изолированными контекстам: от iframe к главному окну и наоборот.

Это уже рабочее решение, но тут появляется еще одна скрытая проблема связанная с организацией кода.
В текущей архитектуре у нас есть две большие области кода:
Код, отвечающий за инициализацию и работу виджета внутри iframe
Код, управляющий этим виджетом и взаимодействующий с ним из главного окна
Обе части тесно связаны: они должны «знать» структуру сообщений, поддерживать актуальные URL, синхронизировать состояния. Это увеличивает сложность поддержки и риск ошибок.
Как теперь организовать код?
Пример проблемы: Вы изменили URL страницы, которую загружает iframe, но забыли обновить этот же URL в роутере главного приложения. В результате виджет не загрузится или будет работать некорректно.
Сильно связанные части кода нужно держать вместе, чтобы избежать ошибок при изменении одной части кода.
Один из вариантов — вынести общие переменные и переиспользовать их в разных частях приложения или положить их в React Context, это позволит централизованно управлять состоянием и настройками. Но такой подход не всегда подходит, так как мы не всегда можем контролировать, где и как будут использоваться эти переменные.
Помимо этого, хотелось бы настраивать всю фичу в одном месте и не прыгать по всему приложению, боясь упустить что-то.

Описываем всю нашу фичу:

Добавляем buildIsolatedWidgetComponents под капотом, эта функция представляет из себя фабрику React-компонентов, которые создаются на основе переданных параметров. Такой подход позволяет централизованно описывать логику и структуру изолированного виджета, не размазывая код по всему проекту.
В нашем случае мы передаем URL страницы, которую нужно загрузить в iframe и дополнительный контент, который также нужно добавить.
На выходе мы имеем Compound Components — это паттерн, при котором несколько компонентов объединяются в один объект, чтобы их можно было использовать вместе, и они знали о контексте друг друга.
Это позволяет:
Инкапсулировать всю логику и состояние, связанные с виджетом;
В одном месте минимизировать ошибки, связанные с рассинхронизацией кода;
Упростить интеграцию и повторное использование виджета в разных частях приложения.

IWC.IsolatedWidget – обертка, которая вставляет iframe в документ и управляет его состоянием (например, открытием и закрытием). Этот компонент (обозначен как "1" на схеме 2) отвечает за создание и отображение изолированного виджета.

IWC.RouterSwitchWrapper – компонент для перехвата роута приложения. Он показывает нужную страницу внутри iframe, когда пользователь переходит по определенному URL. Этот компонент (обозначен как "2" на схеме 2) работает совместно с Router, определяя, какую страницу из Pages загрузить в iframe при определенном запросе.
Давайте посмотрим на код очень упрощенной версии такой фабрики:

Общие переменные мы теперь храним в замыкании, что позволяет нам получить к ним простой доступ.
Складываем все вышеперечисленное вместе: IWC.RouterSwitchWrapper перехватывает запрос к /llm-assistant-widget, а IWC.IsolatedWidget создает iframe и загружает туда содержимое этой страницы (на схеме «Our Page for iframe»).Таким образом мы получаем полностью изолированный виджет, работающий в своем собственном контексте, без сильной связи с остальным приложением и описанный в одном месте.
Использовать такой подход можно не только с виджетами в iframe, но и с другими сильно связными компонентами, части которых должны должны находиться в разных частях приложения.
А что получилось в итоге?
Использование фабрики компонентов для создания изолированного UI — это эффективный способ:
Снизить связность кода: Компоненты меньше зависят друг от друга, что упрощает поддержку и развитие проекта;
Повысить безопасность и надежность: Изоляция предотвращает конфликты глобальных переменных и неожиданные побочные эффекты;
Четко определить интерфейсы: Взаимодействие между частями системы становится прозрачным и управляемым;
Минимизировать риски ошибок: Любые изменения в одной части кода не затрагивают другие, что снижает вероятность багов.
Такой подход облегчает масштабирование и тестирование сложных UI-решений, делает код более понятным и предсказуемым, а внедрение новых фич — безопасным и быстрым. Это особенно актуально для команд, работающих над большими и развивающимися проектами.
Инкапсуляция UI виджетов не только упрощает разработку и тестирование, но и повышает общую стабильность приложения за счет уменьшения вероятности конфликтов и неожиданных побочных эффектов. А какие подходы используете вы?