Electron + microfrontends

от автора

Недавно на проекте столкнулся с необычной задачей — сделать из готового React веб-приложения десктопную версию на Electron. Что же тут необычного? А то, что наше веб-приложение построено на микрофронтенд архитектуре и располагается в трёх отдельных репозиториях. А общение между микрофронтендами происходит в runtime через HTTP. И тут начинаются сложности, так как для создания дистрибутива, Electron’у нужен доступ к исходникам всего приложения. Хотя Electron легко подружить с Webpack, как это сделать с плагином Module Federation на первый взгляд не понятно.

Поиск готового решения в интернете ничего не дал, кроме повисших в воздухе вопросов на Stack Overflow. Пришлось придумать своё решение, которое я и опишу здесь.

Стек проекта типовой (React, Webpack Module Federation, Electron, Electron-forge), поэтому не буду подробно расписывать конфиги, лишь опишу ключевые моменты.


Developer build

Начнём с локального запуска десктопного приложения в develop режиме. Здесь всё просто — нужно только изменить скрипт запуска. Хитрость в том, чтобы параллельно запустить корневое приложение в дев режиме и electron-forge.

// package.json // Скрипт ожидает запуск корневого приложения в режиме разработки и запускает Electron.  "scripts": {   ...   "desktop:start": "concurrently \"yarn start\" \"wait-on tcp:3003 && electron-forge start\"" }

А что же с ремоутами? Их придётся стартовать вручную в отдельных терминалах. Это простое решение подходит только для режима разработки, с продуктовой сборкой немного сложнее.


Production build

Здесь добавляется немного ручной работы, так как на момент упаковки electron-forge package в корневом репозитории должны лежать все необходимые бандлы ремоутов. Как же получить бандлы? Пока я не нашёл другого решения, кроме как тащить руками. Для этого идём в ремоут приложение, делаем прод сборку и копируем /dist в корень хост приложения.

Далее нужно включить бандл ремоута в дистрибутив десктопного приложения. Для этого указываем forge где лежат эти ресурсы:

// forge.config.js  module.exports = {   packagerConfig: {     extraResource: ['./your-mf-module']   } }

Далее нужно внедрить remoteEntry.js скрипт в index.html рута и инициализировать микрофронтовый модуль. Для этого в preload.js, где есть доступ к node api, опишем функцию внедрения скрипта и положим её в глобальный window:

// preload.js  contextBridge.exposeInMainWorld('electron', {   initMf: async () => {     const localAppPath = await ipcRenderer.invoke('get-local-app-data');     const pathToScript = path.join(localAppPath, 'App-name', 'app-1.0.0', 'resources', 'your-mf-module', 'dist', 'remoteEntry.js');     const script = document.createElement('script');      script.src = pathToScript;     document.head.appendChild(script);   } });

Тут есть одна сложность — нужно определить путь до директории локальной установки приложений (LOCALAPPDATA). Используем ipcRenderer.invoke() чтобы получить данные из main process. Также опишем соответствующий хендлер в main.js:

// main.js  const createWindow = () => {   const mainWindow = new BrowserWindow({...});    ipcMain.handle('get-local-app-data', () => process.env.LOCALAPPDATA); };

Вызов функции initMf происходит в renderer.js, где доступен объект window:

// renderer.js  window.electron.initMf();

Таким образом мы добавляем скрипты ремоут модулей при старте приложения. Далее их нужно инициализировать как webpack модули. В документации Webpack есть пример (https://webpack.js.org/concepts/module-federation/) такой функции, используем её:

function loadComponent(scope, module) {   return async () => {     // Initializes the shared scope. Fills it with known provided modules from this build and all remotes     await __webpack_init_sharing__('default');      const container = window[scope]; // or get the container somewhere else      // Initialize the container, it may provide shared modules     await container.init(__webpack_share_scopes__.default);      const factory = await window[scope].get(module);     const Module = factory();      return Module;   }; }

При вызове эта функция находит модуль в window, инициализирует и возвращает его. Полученный модуль импортируем в корневой App.tsx через React.lazy().

Вот собственно и всё решение. Теперь можно без проблем упаковать десктопное приложение и собрать дистрибутив.


Заключение

Возможно, это решение не самое изящное, но кажется единственная альтернатива ему — глобальная перестройка архитектуры всего приложения в монорепозиторий. А в моём решении нужны только доработки в хост приложении и никаких изменений в ремоутах. А в ремоутах как раз и происходит вся разработка, и для обновления десктопной версии приложения нужен только новый бандл изменённого ремоута — и свежий дистрибутив готов.


ссылка на оригинал статьи https://habr.com/ru/articles/934068/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *