Привет, Хабр! Меня зовут Дмитрий Переверза, я 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 виджетов не только упрощает разработку и тестирование, но и повышает общую стабильность приложения за счет уменьшения вероятности конфликтов и неожиданных побочных эффектов. А какие подходы используете вы?