Веб-модули в Flutter-клиенте для FastEn и как мы обновляем их на 5-и платформах сразу
Mini-Apps в Server-Driven UI клиенте фреймворка FastEn. Веб-модули внутри Flutter-клиента, обмен данными через мост, авторизация статики через Nginx и обновления без выпуска новой версии приложения.

1. Введение. Ограничения Server-Driven UI
В основе некоторой части нашей заказной разработки и собственного SaaS-ERP Boost-X лежит FastEn — наш RAD-фреймворк, платформа для быстрой сборки корпоративных систем. Его ядро построено на концепции Server-Driven UI (SDUI): описание интерфейса живёт на бэкенде и приезжает на клиент не заранее, в составе обновления, а в момент, когда пользователь открывает нужный раздел (или при старте приложения). Тонкий кроссплатформенный клиент написан на Flutter, сервер под капотом — FastAPI.
Из единой кодовой базы мы раскатываем функционал сразу на 5 платформ: нативные мобильные приложения (iOS, Android), десктопные клиенты (Windows, macOS) и Web. По механике это похоже на обычный веб: клиент запрашивает «страницу», сервер отдаёт её актуальную версию. Обновили сервер — клиенты получили свежие версии экранов при следующем запросе, без установки новой версии приложения. Для 90% типовых B2B-задач SDUI сокращает Time-to-Market в разы.
Но рано или поздно эта схема упирается в конечность вариаций компонентов SDUI.
Что делать, если логисту в приложении нужна интерактивная карта с динамической кластеризацией адресов и кастомными пинами? Начальнику цеха — планировщик смен в виде диаграммы Ганта с Drag-and-Drop? Менеджеру B2B-портала — интерфейс для оформления акций?
Писать под каждую такую вариацию собственный компонент на клиенте и сервере — долго, а закладывать в компоненты десятки кастомизирующих свойств «на все случаи жизни» — ещё дольше. Здесь классический SDUI пасует. А описывать логику сложных жестов, перерисовку канваса или картографию перекидыванием JSON-схем между клиентом и сервером — тупик: сетевые лаги в таком интерактиве не контролируются в принципе.
Решать «в лоб» — писать сложный кастомный UI нативно или собирать набор Flutter-плагинов и добиваться их стабильной работы на всех 5 операционных системах — значит потерять и скорость кроссплатформенной разработки, и бюджет.
Резонный вопрос: а почему бы тогда не отказаться от нативного клиента вообще и не написать всю ERP как обычное веб-приложение (SPA)? Потому что это означает отказ от нативной производительности, плавности интерфейса, интеграции с ОС и железом (например, сканерами штрих-кодов), к которым привыкли пользователи мобилок и десктопов. Жертвовать этим для всей системы ради 10% кастомных экранов мы не захотели.
Решение: Mini-Apps как расширяемый UI-контрол
Нам был нужен управляемый «аварийный выход» — механизм, который решает три задачи: использовать готовые JS-библиотеки (React, Vue, Mapbox, редакторы) для нестандартного интерактива, доставлять такие модули без пересборки клиентов и модерации App Store / Google Play — и при этом сохранить контроль над доступом к коду и данным.
Так в платформе FastEn появился движок Mini-Apps — архитектура, встраивающая внешние веб-приложения внутрь нативного клиента. Не «глупые» веб-страницы в iframe, а полноправные модули бизнес-логики, которые обмениваются данными с нативными формами и подчиняются общим правилам безопасности.
Архитектура решает пять практических задач:
- 1 код — 5 платформ: веб-модуль пишется один раз и работает на iOS, Android, macOS, Windows и Web через наш универсальный мост.
- Декларативный in/out маппинг: бэкенд сам описывает, как данные из мини-аппа попадают в нативную форму, — без хардкода на клиентах.
- Delivery: новые фичи и хотфиксы доезжают до пользователей без ревью в сторах, за счёт кэш-контроля Nginx.
- Безопасность: двухуровневая схема с Nginx Gatekeeper и инъекцией токенов напрямую в память (In-Memory Token Injection) — статика и токены не утекают через URL.
- Сессии: истекшая авторизация обновляется в фоне, без «белых экранов».
2. Единый JS-API поверх разных веб-движков: универсальный «мост»
Веб-модулю внутри нативного приложения нужен надёжный двунаправленный канал связи с хостом — «Мост». И здесь типичная проблема кроссплатформенной разработки: у каждой платформы свой механизм связи с веб-движком.
Наш Flutter-клиент работает на 5 платформах:
- На iOS / macOS сообщения ходят через
window.webkit.messageHandlers. - На Android — через
JavascriptInterface. - На Windows (десктоп) под капотом
WebView2. - А в Web-версии приложения мини-апп открывается в
iframe, и общение идёт черезwindow.postMessage.
Заставлять фронтенд-разработчика мини-аппа (нашу команду или внешних партнёров) поддерживать все четыре протокола в своём JS/TS-коде — значит размазать эту сложность по каждому модулю и плодить технический долг.
Инъекция полифилов на уровне хост-приложения
Логику определения платформы и выбора транспорта мы перенесли в нативный контроллер. Перед загрузкой страницы мини-аппа Flutter-клиент встраивает в неё “UserScript”, который создаёт единый глобальный объект window.FastEnBridge и транслирует его вызовы в нативный интерфейс текущей ОС.
Принцип «тонкого клиента» и SDK
Само мини-приложение при этом ничего не знает о среде, в которой запущено. Веб-разработчик берёт привычный стек (React, Vue, просто JS/TS), подключает наш легковесный NPM-пакет fast-en-bridge и вызывает bridge.sendToHost({ type: 'submit', payload: data }). Мост сам сериализует данные (это особенно критично для строгих WebKit-окружений на iOS/macOS) и доставляет их нативному клиенту.
В результате UI-модуль пишется один раз (Write Once, Run Everywhere) и работает во всех наших нативных и десктопных клиентах.
Осталось разобраться, что нативному клиенту делать с данными, которые прилетают из мини-аппа.
3. In/out мэппинг данных: мини-апп ведёт себя как обычное поле формы
Главная проблема классических WebView-интеграций — изоляция данных. Обычно веб-виджет живёт сам по себе: открылся, сделал свои запросы, закрылся. Синхронизация его состояния с экраном нативного приложения — это всегда отдельные костыли.
Нам же было нужно, чтобы мини-апп вёл себя как обычный нативный UI-контрол — как текстовое поле или выпадающий список. Взял данные из текущей формы пользователя (например, из черновика заказа), дал их изменить в своём кастомном интерфейсе, вернул результат обратно в форму.
FastEn — Server-Driven UI фреймворк, поэтому и эту задачу мы отдали серверу, таким образом исключив хардкод на клиентах. Логика передачи данных диктуется бэкендом через механизм in/out мэппинга.
Сценарий: карта в карточке заказа
Логист в десктопном приложении Boost-X открывает карточку заказа. В карточке есть кнопка «Указать на карте», по клику должен открыться мини-апп с картой.
Вместе со структурой формы бэкенд присылает реестр мини-аппов (объект mini_apps в JSON-ответе), где для этой кнопки описаны правила обмена данными:
// Пример конфигурации мини-аппа, приходящей от бэкенда FastEn
"mini_apps": {
"pickup_location_picker": {
"url": "https://mini-apps.my-domain.com/map-picker",
"input_mapping": {
"initial_lat": "order.pickup.lat",
"initial_lng": "order.pickup.lng"
},
"output_mapping": {
"latitude": "order.pickup.lat",
"full_address": "order.pickup.address"
}
}
}
-
Input Mapping (Сервер -> Нативный клиент -> Веб): Бэкенд декларирует правило, например
"initial_lat": "order.pickup.lat". Когда логист нажимает кнопку, Flutter-клиент берёт текущее значение из локального стейта формы по путиorder.pickup.latи упаковывает его в стартовый Payload. Через наш Универсальный Мост эти данные инжектируются в JS-контекст стартующего мини-аппа — карта сразу открывается на нужных координатах. -
Output Mapping (Веб -> Нативный клиент -> Сервер): Логист передвигает пин и нажимает «Подтвердить». Мини-апп понятия не имеет, как устроена форма заказа в Boost-X, — он просто отправляет в Мост абстрактный JSON:
{"latitude": 55.75, "full_address": "г. Москва, ..."}. Flutter-клиент перехватывает сообщение, сверяется с серверным правиломoutput_mapping("latitude" -> "order.pickup.lat","full_address" -> "order.pickup.address") и обновляет свои локальные поля. Пользователь сразу видит новый адрес и координаты в форме заказа, без перезагрузки экрана.
Что это даёт
Нативный клиент в этой схеме остаётся тонким: семантики данных он не знает, а просто перекладывает их по путям, которые указал бэкенд. Поменять логику мини-аппа, добавить поля на форму, перекроить маппинг — всё это делается только правками на сервере и мгновенно вступает в силу для пользователей всех пяти платформ.
Остался вопрос доставки: как обновлённый код самих мини-аппов попадает на устройства.
4. Как обновлять Mini-Apps без релиза клиента: ETag
Известная боль нативной разработки — цикл релизов. Нашли баг (хорошо, если не критический) — соберите билд, отправьте на модерацию в App Store и Google Play (от нескольких часов до пары дней), а потом надейтесь, что пользователи обновятся. С десктопом не сильно легче: новые инсталляторы или же своя система автообновления.
Для кастомных UI-модулей архитектура Mini-Apps снимает эту проблему: их жизненный цикл отвязан от релизного цикла приложения-хоста (Boost-X).
Есть нюанс: WebView и встроенные браузеры очень агрессивно кэшируют статику (HTML, JS, CSS). Оставить как есть — и пользователи будут открывать старую, закэшированную версию модуля.
Управление кэшем на уровне Nginx
Чтобы пользователи всегда получали свежую версию мини-аппа, а запуск при этом оставался быстрым, контроль кэширования мы перенесли на веб-сервер. Для локации /apps/ Nginx принудительно отдаёт Cache-Control: no-cache, must-revalidate.
В связке с ETag (хешем версии файла) механика получается такая:
- Пользователь открывает форму в Boost-X и вызывает мини-апп. WebView не качает файлы заново, а делает запрос к Nginx: «У меня версия X, она не изменилась?».
- Если обновлений не было, Nginx отвечает
304 Not Modified, и WebView достаёт тяжёлые скрипты и стили из своего кэша. Визуально запуск мгновенный. - Как только фронтенд-команда деплоит новую версию (просто перезаписывает файлы бандла на сервере), ETag меняется — и при следующем открытии любой пользователь получит
200 OKи свежий код.
Фронтенд-разработка: Как отлаживать Mini-Apps?
Может показаться, что разработка такого модуля — это мучение с постоянными деплоями. На деле мы заложили специальный Proxy Mode для локальной отладки. Если нативное приложение FastEn запущено в Debug-режиме, разработчик может переопределить URL мини-аппа на
http://localhost:3000. Фронтендер поднимает локальный dev-сервер (например, Vite) и видит изменения в коде мини-аппа прямо на экране приложения в реальном времени. Пересобирать нативный клиент ради проверки вёрстки не нужно.
Что это даёт
Новый функционал, изменения логики интерактивных компонентов и хотфиксы доезжают на все 5 платформ синхронно; Time-to-Market для таких правок сократился с дней и недель до минут. Не нужно менять пути к файлам, выдумывать версионирование в URL или выпускать минорные апдейты самого FastEn-клиента.
Правда, раздавая файлы приложения с сервера, мы сами себе создаём вопрос, который первым задаст любая служба безопасности: а как сделать так, чтобы эту статику не мог скачать никто, кроме авторизованных пользователей?
5. Два уровня авторизации (для статики и для логики)
Веб-модули в корпоративном приложении — всегда под прицелом ИБ. Классических уязвимостей здесь две:
- Открытый контур: статические файлы (JS/HTML) лежат на публичном сервере — код можно выкачать, изучить бизнес-логику, найти точки входа в API.
- Утечка токенов: токен для API-запросов передаётся в веб-вью через URL-параметры (и оседает в логах роутеров) или хранится в уязвимом
LocalStorage.
Авторизация у нас разделена на два независимых слоя: доступ к файлам и доступ к данным.
Уровень 1: Транспорт (доступ к файлам через Nginx Gatekeeper)
Право просто скачать интерфейс мини-аппа должно быть только у авторизованных пользователей приложения. Нагружать Python-бэкенд раздачей статики не хотелось, поэтому шлюзом аутентификации стал сам Nginx — через модуль auth_request.
Прежде чем отдать ресурс из /apps/, Nginx делает запрос к FastAPI. Сессия активна (HTTP 200) — файл отдается, нет — 401 Unauthorized.
Мобильные WebView непредсказуемо работают с кастомными HTTP-заголовками при автоматической загрузке ресурсов, поэтому на этом уровне мы используем исключительно Cookies. Ещё до старта загрузки нативный клиент (Flutter) принудительно устанавливает Secure HttpOnly куку для домена мини-приложений.
# Упрощенный пример участка конфигурации Nginx
location /apps/ {
# 1. Запрос к внутреннему сервису авторизации
auth_request /internal/auth/verify;
# 2. Раздача статики только в случае успеха (HTTP 200)
alias /var/www/storage/mini-apps/;
# 3. Управление кэшем для мгновенных обновлений
add_header Cache-Control "no-cache, must-revalidate";
}
location = /internal/auth/verify {
internal;
proxy_pass http://fastapi_backend/api/v1/auth/verify-cookie;
proxy_pass_request_body off;
proxy_set_header Cookie $http_cookie; # Проброс Secure Cookie
}
Уровень 2: Прикладной (доступ к данным через JS-инъекцию)
Скачивание файлов защитили — теперь нужно безопасно передать мини-аппу токен для работы с API. От передачи чувствительных данных в URL и от хранения их на стороне веб-клиента мы отказались полностью.
Принцип такой: у мини-приложения нет собственного состояния авторизации. После того как страница загружена через Nginx, нативный клиент берёт API-токен пользователя и инжектирует его напрямую в оперативную память JS-движка через защищённый канал Универсального Моста (вызовом window.initFastEnApp(payload)).
Что в итоге по векторам атак
- Корпоративный контур закрыт: статику нельзя выкачать извне, мини-приложение не откроется в обычном браузере по украденной ссылке — без активной сессии в нативном клиенте Boost-X Nginx его просто не отдаст.
- Токены не светятся в истории браузера, URL-адресах и на диске устройства.
- При закрытии нативной шторки контекст мини-аппа уничтожается вместе с токенами в оперативной памяти.
При этом статику раздаёт «голый» Nginx — со всей его скоростью: проверка сессии стоит один внутренний подзапрос, а не полноценный проход через бэкенд на каждый файл.
Но загрузить страницу — половина дела. Нужно ещё, чтобы нативное приложение и веб-модуль стартовали синхронно — без «белого экрана» и потери данных.
6. Синхронный старт и Silent Refresh
Если встроенный веб-виджет выглядит и ведёт себя как чужой сайт, пользователи воспринимают его как отдельную систему — для корпоративного продукта это ухудшает UX и усложняет внедрение. Природу WebView выдают две вещи: «белый экран» при загрузке и внезапные обрывы сессии. Обе мы закрыли на уровне протокола.
Синхронный старт
При инициализации мини-аппа возникает классическая гонка: кто загрузится первым? Отправить начальные данные в веб-контекст до полной загрузки страницы — данные потеряются. «Угадывать» задержки таймерами — пользователи медленных устройств будут смотреть на пустые экраны.
Вместо угадывания — жёсткая стейт-машина:
- Flutter-клиент инициирует открытие мини-аппа, но перекрывает его нативным индикатором загрузки.
- Браузер качает статику. Мини-апп загружается «чистым» — он ещё не знает ни контекста, ни кто его открыл.
- Как только DOM-дерево готово принимать данные, JS-код мини-аппа отправляет через Универсальный Мост сигнал
system:ready. - Получив сигнал, нативный клиент инжектирует в веб-контекст стартовый Payload: данные формы, языковые настройки, параметры темы для синхронизации дизайна и API-токен прикладного уровня. Для этого вызывается глобальная функция
window.initFastEnApp(data). - Мини-апп рендерит UI с полученными данными — и только после этого нативный индикатор загрузки скрывается. Никаких «морганий» и пустых экранов: лоадер уходит, когда интерфейс уже инициализирован.
Тихое обновление токенов
Вторая проблема — устаревание токенов доступа. В классическом веб-приложении протухший токен (401 Unauthorized) означает переадресацию на страницу логина. Теперь представьте: логист полчаса заполняет форму заказа в Boost-X, открывает мини-апп с картой — и в этот момент сессия истекает. Экран логина внутри виджета и потерянный черновик ему не простят.
Поэтому мини-апп за авторизацию не отвечает вовсе. Если API-запрос внутри него возвращает 401, включается механизм тихого обновления:
- Мини-апп ставит все текущие и исходящие сетевые запросы на «паузу».
- Через Мост уходит сигнал
token_refreshхост-приложению. - Нативный Flutter-клиент в фоне обращается к серверу аутентификации, обновляет ключи (Refresh Token), переписывает Cookies и инжектирует новый Access-токен обратно в JS-контекст.
- Получив новый ключ, мини-апп подменяет его в заголовках и автоматически повторяет «упавшие» запросы.
Пользователь этого процесса не замечает: ни экранов логина, ни сброшенных данных.
7. Итоги: SDUI для 90% задач, защищённые веб-модули — для остальных 10%
Архитектура Mini-Apps позволила нам не выбирать между SDUI и вебом, а взять сильные стороны обоих. Для 90% задач работает Server-Driven UI: стандартные списки, таблицы и бизнес-формы генерируются сразу для 5 платформ (iOS, Android, Windows, macOS, Web) из единой кодовой базы. Для оставшихся 10% — карт, сложных визуализаторов, редакторов — подключаются защищённые веб-модули.
Что дали Универсальный Мост, декларативный маппинг и шлюз аутентификации Nginx в сумме:
- Независимость от сторов и релизных циклов. Логика сложных компонентов обновляется на сервере и сразу доставляется на все устройства.
- Безопасность. Исходный код изолирован, статика отдаётся только авторизованным сессиям, токены живут в защищённом хранилище.
- Цельный UX. Пользователь нативного клиента Boost-X не догадывается, что часть функционала отрисована веб-движком.
Дальше возникает отдельный вопрос: как сохранить бизнес-данные, когда пользователь нажимает «Сохранить» в мобильном приложении или мини-аппе? Ни одно событие не должно потеряться даже при падении серверов и обрывах сети — и при этом микросервис версионирования (истории данных) не должен тормозить основную базу.
Об этом — в одной из следующих статей: разберём под капотом Систему версионирования Boost-X, построенную на архитектурном паттерне Transactional Outbox.
Часто задаваемые вопросы (FAQ)
Отвечаем на главные архитектурные и технические вопросы о гибридной разработке на FastEn:
Как ускорить разработку B2B-системы сразу под iOS, Android, Windows, macOS и Web и не раздувать штат❓
Использовать комбинацию Server-Driven UI (SDUI) для 90% стандартных форм и архитектуру Mini-Apps для 10% сложного кастомного интерактива. Это позволяет удерживать единую кодовую базу на Flutter для всех 5 платформ, без отдельной реализации под каждую ОС.
Как обновлять бизнес-логику и чинить хотфиксы в мобильном приложении в обход модерации App Store и Google Play❓
Выносить сложный или часто меняющийся UI в архитектуру Mini-Apps. Изменения в коде веб-модуля становятся доступны пользователям с сервера после проверки актуальности через ETag — без выпуска апдейта нативного приложения и без ожидания проверки сторов.
Безопасна ли WebView-архитектура в Enterprise и как защитить статику и токены от утечек❓
За счёт архитектуры Zero-Trust и двухуровневой авторизации (Split Auth). Статические файлы закрываются шлюзом Nginx с проверкой сессии (auth_request), а чувствительные API-токены инжектируются напрямую в оперативную память JS-движка через нативный мост — не передавая токены через URL и не сохраняя их в LocalStorage.
Как избавить гибридное приложение на Flutter от белых экранов и вылетов на страницу авторизации❓
Использовать синхронный старт (протокол Handshake V2) и механизмы Silent Refresh. Нативный индикатор загрузки держится до полной готовности веб-модуля (сигнал system:ready), а при истечении токена нативный клиент обновляет сессию в фоне, не сбрасывая введённые пользователем данные.
Смогут ли наши внутренние веб-разработчики дорабатывать Mini-Apps без знания Flutter и языка Dart❓
Да. Нативный клиент FastEn предоставляет единый мост (fast-en-bridge), поэтому веб-модули пишутся на любом стандартном стеке (React, Vue, Vanilla JS) по принципу Write Once, Run Everywhere. Любая веб-студия или ваш внутренний IT-отдел сможет создавать новые фичи без погружения в мобильную разработку.
Проектируете архитектуру для сложного B2B/Enterprise продукта?
Если стоите перед выбором между нативной разработкой, PWA и кроссплатформенными решениями — запишитесь на экспресс-аудит. Архитекторы DEV-STACK изучат ваши вводные и честно скажут, где гибридный подход и Server-Driven UI сэкономят вам годы разработки и миллионы бюджета, а где объективно лучше классический натив.
Проверить проект
Запишитесь на экспресс-оценку. Наш архитектор изучит вводные и даст обратную связь. Это бесплатно.
Спасибо! Мы свяжемся с вами.