Давно не виделись хабровчане. Сегодня мы поговорим на избитую и всем уже изрядно надоевшую тему, но слегка под другим углом. Если вы уже работали с микрофронтендами в вебе, сама загрузка отдельной сборки внутри Capacitor вряд ли удивит. WebView исполняет JavaScript, а уже он загружает сборку через Module Federation и монтирует её как компонент.
Сложности начинаются, когда этому микрофронту нужен, например, доступ к камере или геолокация. Сразу возникает несколько вопросов:
-
Кто регистрирует плагины и запрашивает разрешения?
-
Как собственная маршрутизация микрофронта уживается с системной кнопкой “Назад” и диплинками?
-
Что можно обновлять через ОТА обновления, а что нет?
Разберём это на примере воображаемого(но реалистичного) Capacitor-приложения: страница профиля выступает оболочкой, а адреса доставки загружаются отдельным микрофронтендом.
Об авторе: Илья Скаков — мобильный разработчик, специализируется на гибридных приложениях на Capacitor и React Native.
Оглавление:
Термины и границы ответственности
|
Понятие |
В этой статье |
|---|---|
|
Capacitor |
Стартовая веб-часть приложения в WebView. Владеет маршрутизацией верхнего уровня, регистрацией плагинов и контрактами с MFE. |
|
MFE |
Независимо собираемая веб-функция, которую оболочка загружает и монтирует в своей области. В примерах — Delivery MFE. |
|
Контракт |
Типизированный набор методов и данных, через который MFE запрашивает возможности устройства и навигацию. |
|
Диплинк |
Ссылка, которая открывает конкретный экран приложения: |
Два слоя приложения
У обычного Capacitor приложения есть собственно свой бандл и нативные проекты Android/iOS. С микрофронтендами бандл делится ещё на две части:
Capacitor-приложение│├─ Capacitor│ ├─ стартовый бандл, лежащий в приложении│ ├─ Capacitor runtime и плагины│ ├─ авторизация, верхнеуровневый роутер│ ├─ загрузчик MFE│ └─ контракт│└─ Delivery MFE ├─ собственный бандл и доменная логика ├─ собственная маршрутизация └─ вызовы разрешённых плагинов через контракт
Capacitor здесь — стартовая веб-часть приложения. Он открывается первым, знает о текущем пользователе, создаёт роутер и регистрирует плагины.
Delivery MFE можно собирать и выпускать отдельной командой. На устройстве он работает в том же WebView, что и Capacitor.
Из схемы следуют два правила:
-
MFE не регистрирует плагины Capacitor самостоятельно.
-
Для обращения к нативным возможностям MFE использует методы, которые Capacitor передал через контракт.
Кто владеет оболочкой Capacitor
Можно импортировать @capacitor/core прямо в MFE и вызвать registerPlugin():
Пример зависимости, которую не стоит создавать в MFE
import { registerPlugin } from '@capacitor/core'const Camera = registerPlugin('Camera')
Если плагин уже есть в приложении, вызов может сработать. Но MFE теперь знает имя нативного плагина и зависит от его API.
При изменении плагина придётся согласовывать релизы Capacitor и MFE. Придётся также определить, кто показывает объясняющий экран перед запросом разрешения, фиксирует отказ и поддерживает старые версии приложения.
Эти решения я бы оставил команде Capacitor:
-
он подключает Android/iOS плагины;
-
он управляет разрешениями в
AndroidManifest.xmlиInfo.plist; -
он определяет пользовательскую политику запроса разрешений;
-
он сохраняет единое поведение на Android, iOS и web;
-
он может запретить ту или иную возможность для MFE фиче флагом контракта или по состоянию авторизации.
Нативный код плагина попадает в установленное приложение при сборке APK/AAB/IPA. Публикацией MFE его не добавить.
Контракт между оболочкой Capacitor и MFE
В нашем варианте MFE получает обычный объект с методами, которые нужны доставке: узнать координаты, дать тактильный отклик и отправить событие аналитики. Такой объект — не «обёртка над всеми плагинами», а минимальная публичная поверхность для конкретной функции.
// contracts/delivery-platform.tsexport type DeliveryLocation = { latitude: number longitude: number}export type DeliveryPlatform = { location: { getCurrent(): Promise<DeliveryLocation> } feedback: { success(): Promise<void> } analytics: { track(name: string, properties?: Record<string, string>): Promise<void> }}
Capacitor реализует интерфейс через плагин или собственный адаптер:
// mobile-shell/src/platform.tsimport { registerPlugin } from '@capacitor/core'import type { DeliveryPlatform } from '@app/contracts/delivery-platform'type AppHostPlugin = { getCurrentLocation(): Promise<{ latitude: number; longitude: number }> feedbackSuccess(): Promise<void> track(options: { name: string properties?: Record<string, string> }): Promise<void>}const AppHost = registerPlugin<AppHostPlugin>('AppHost')export const deliveryPlatform: DeliveryPlatform = { location: { getCurrent: () => AppHost.getCurrentLocation(), }, feedback: { success: () => AppHost.feedbackSuccess(), }, analytics: { track: (name, properties) => AppHost.track({ name, properties }), },}
MFE получает его через пропсы:
export const DeliveryApp = ({ platform }: { platform: DeliveryPlatform }) => { const locate = async () => { const point = await platform.location.getCurrent() await platform.analytics.track('delivery_location_received') console.log(point) } return <button onClick={() => void locate()}>Определить адрес</button>}
Для разработчика MFE переданный объект выглядит как обычная npm-зависимость. Ему достаточно знать методы контракта; детали остаются в Capacitor.
Такой контракт удобно вынести в пакет монорепы или отдельную библиотеку. Там будут типы, схемы данных, коды ошибок и правила версий. @capacitor/*, React-компоненты и платформенный код останутся в приложениях.
packages/└─ app-contracts/ ├─ delivery-platform.ts ├─ navigation.ts └─ versions.ts
Как MFE вызывает нативную возможность
Названия методов лучше брать из задачи пользователя, а не из API плагина. Это позволяет заменить реализацию, не меняя код MFE.
Плохо:
platform.plugins.geolocation.requestPermissions()platform.plugins.geolocation.getCurrentPosition()
Хорошо:
platform.location.getCurrent()
За вызовом метода могут стоять @capacitor/geolocation, собственный плагин Capacitor, серверный резервный сценарий или браузерный navigator.geolocation. MFE в любом случае получает координаты через один API.
Также можно описать и другие действия:
|
Нужно MFE |
Контракт |
Чем реализует оболочка |
|---|---|---|
|
Сделать фото документа |
|
плагин Camera и продуктовый сценарий |
|
Выбрать файл |
|
плагин File Picker |
|
Прочитать QR |
|
плагин Barcode Scanner |
|
Открыть системные настройки |
|
нативный адаптер |
|
Подтвердить действие вибрацией |
|
плагин Haptics |
|
Открыть другой раздел приложения |
|
роутер оболочки |
MFE не зависит от конкретной реализации на Android/iOS. Чтобы запуститься в браузере, ему можно передать другую реализацию контракта:
const browserPlatform: DeliveryPlatform = { location: { getCurrent: async () => ({ latitude: 55.7558, longitude: 37.6173 }), }, feedback: { success: async () => undefined }, analytics: { track: async () => undefined },}
Разрешения и ответственность
Когда MFE вызывает метод геолокации, системный диалог показывает приложение, а не React-компонент. MFE сообщает, что пользователю нужна текущая точка для доставки.
Запрос проходит через Capacitor:
Delivery MFE → вызывает platform.location.getCurrent()Capacitor → проверяет авторизацию, фиче-флаги и текущее состояние → при необходимости объясняет, зачем нужно разрешение → вызывает плагин → возвращает координаты или доменную ошибку
Ошибки тоже лучше описать в контракте, чтобы MFE не разбирал сообщения Android/iOS:
export type LocationErrorCode = | 'permission-denied' | 'permission-permanently-denied' | 'service-disabled' | 'unavailable'export type LocationError = { code: LocationErrorCode}
Тогда MFE сможет показать понятное состояние после отказа:
try { const location = await platform.location.getCurrent() setLocation(location)} catch (error) { if ( typeof error === 'object' && error !== null && 'code' in error && error.code === 'permission-denied' ) { setMessage('Разрешите доступ к геолокации, чтобы выбрать адрес.') } else { setMessage('Не удалось определить местоположение.') }}
Команда Capacitor подключает плагин, добавляет разрешения в AndroidManifest.xml и Info.plist и определяет политику запроса. Команда MFE выбирает момент в своём сценарии и показывает результат или отказ.
Роутинг: где заканчивается Capacitor и начинается MFE
У Capacitor и MFE могут быть свои роутеры. Если оба независимо меняют URL и обрабатывают кнопку «Назад», пользователь легко окажется не на том экране.
Я бы разделил маршруты по областям:
Роутер Capacitor├─ /home├─ /profile└─ /delivery/* → монтирует Delivery MFEDelivery роутер├─ /addresses → список адресов├─ /addresses/new → создание адреса└─ /addresses/:id/edit → редактирование адреса
Capacitor выбирает крупную область и при необходимости загружает MFE. Внутри доставки маршрутами занимается MFE.
Передать путь MFE можно двумя способами.
Вариант 1: общая история браузера
Capacitor и MFE используют window.history, заранее договорившись о маршрутах и полях состояния. Для небольшого приложения этого достаточно. Нужно только следить, чтобы MFE не записал состояние, которое Capacitor не умеет обрабатывать.
window.history.pushState( { shellRoute: 'delivery', remoteRoute: '/addresses/new' }, '', '/delivery/addresses/new',)
Вариант 2: Capacitor передаёт маршрут через контракт
Capacitor хранит текущий маршрут и передаёт в MFE путь вместе с функцией для перехода:
type DeliveryNavigation = { path: string navigate(path: string, options?: { replace?: boolean }): void}
<DeliveryRemote platform={deliveryPlatform} navigation={deliveryNavigation}/>
Так Capacitor может логировать переходы, проверять доступ, собирать URL и обрабатывать нажатие кнопки «Назад» в одном месте. MFE при этом может использовать React Router для своих экранов, не меняя глобальную историю напрямую.
Если MFE несколько, я бы выбрал второй вариант.
Диплинки
Capacitor разбирает URL и понимает, какому MFE передать маршрут. У MFE нет причины самостоятельно подписываться на системный диплинк:
myapp://delivery/addresses/42/edit → оболочка: область delivery → оболочка загружает Delivery MFE → оболочка передаёт путь /addresses/42/edit → Delivery MFE открывает экран редактирования
При таком переходе Capacitor должен:
-
проверить, доступна ли фича и авторизован ли пользователь;
-
сохранить намерение перехода, пока MFE ещё загружается;
-
передать MFE только путь, за который оно отвечает.
Если MFE ещё загружается, событие ему отправлять рано: он его не получит. Оболочка может сохранить ожидающий маршрут в своём состоянии и передать его при первом рендере MFE.
Кнопка «Назад» и история браузера
Нажатие системной кнопки «Назад» на Android нужно согласовать с историей WebView. Capacitor сначала проверяет, есть ли куда вернуться внутри текущей области, и только затем покидает её.
Например, пользователь возвращается так:
Редактирование адреса → список адресов → предыдущий маршрут оболочки → выход из приложения
Контракт навигации позволяет оболочке знать маршрут MFE и передать ему событие back. Если MFE вернулся на предыдущий экран, он отвечает handled: true. Иначе оболочка продолжает собственную навигацию. MFE предоставляет обработчик при монтировании, а вызывает его оболочка.
type DeliveryRemote = { onBack(): Promise<{ handled: boolean }>}
const result = await deliveryRemote.onBack()if (!result.handled) { shellRouter.back()}
Загрузка и обновление MFE
Бандл Capacitor и бандл MFE собираются и публикуются отдельно:
Capacitor → обновляется через публикацию в store или <abbr title="over-the-air, обновление веб-части без публикации новой нативной сборки">OTA</abbr>-обновлениеDelivery MFE → публикуется на CDN как remoteEntry.js
Для нового MFE не нужен APK/IPA. Ему доступны только возможности, уже поставленные с установленной версией приложения и контрактом.
На стороне Capacitor понадобятся:
-
экран загрузки;
-
резервный сценарий для сетевой ошибки или несовместимой версии;
-
кэш последней проверенной версии, если функция должна работать без сети;
-
откат к предыдущей рабочей версии.
Продумайте и кэширование. Если remoteEntry.js публикуется по одному и тому же URL, кэш может сохранить старые чанки после обновления. Версионированный релиз помогает избежать такой проблемы:
https://cdn.example.com/delivery/3.8.0/remoteEntry.jshttps://cdn.example.com/delivery/3.8.0/assets/...
Актуальный разрешённый релиз оболочка узнаёт из манифеста:
{ "name": "delivery", "version": "3.8.0", "entry": "https://cdn.example.com/delivery/3.8.0/remoteEntry.js", "minimumShellVersion": "1.4.0"}
Для манифеста тоже нужны HTTPS, список разрешённых доменов и, в зависимости от модели угроз, подпись или проверка целостности.
Почему одного URL remoteEntry.js недостаточно
После обновления entry-файла браузерный кэш может сохранить старые чанки. Поэтому версия должна входить и в URL remoteEntry.js, и в URL его ассетов; оболочка должна хранить последнюю проверенную версию и уметь откатиться к ней.
Совместимость версий
Capacitor и MFE выпускаются независимо, а у пользователей могут оказаться разные сочетания их версий. Совместимость придётся описать в контракте.
Допустим, Delivery MFE начал вызывать platform.location.openSettings(), но у части пользователей стоит старая оболочка без этого метода. Тут есть три варианта:
-
MFE требует
minimumShellVersionи не загружается на старой оболочке; -
функция опциональна, а MFE показывает резервный сценарий;
-
оболочка сначала выпускает новый метод, сохраняет его совместимым, и только потом MFE начинает им пользоваться.
Я бы начал с третьего варианта: выпустил новый метод в оболочке без удаления старых, включил функцию в MFE через фиче-флаг и дождался обновления приложения у пользователей. После этого метод можно сделать обязательным.
Версию контракта стоит передавать явно:
type PlatformInfo = { shellVersion: string contractVersion: 3}
TypeScript проверит типы во время сборки. Совместимость установленного Capacitor и опубликованного MFE нужно проверять при загрузке.
Итог
Module Federation работает внутри Capacitor так же, как в браузере. Специальные хитрые решения нужны, когда MFE обращается к возможностям устройства или участвует в системной навигации.
Если вы уже делили Capacitor-приложение на MFE, расскажите в комментариях, как согласовали навигацию и версии контрактов между командами.
В следующей части разберем безопасность Capacitor приложений. На этом у меня все. Пишите любые интересующие вас вопросы в комментарии и в личку VK | Telegram.
Ссылки:
ссылка на оригинал статьи https://habr.com/ru/articles/1089150/