Мікрофронтенди з ReactJS та Vite

Проблема

Уявіть, що ви працюєте над великим проектом з кількома модулями, кожен з яких обслуговується незалежними командами. Якщо це моноліт, тобто єдина кодова база, керувати таким додатком стане викликом. Час збірки збільшується з цієї причини. Можливість отримати злиття також зростає. Опанування новими інженерами також стає проблемою, оскільки індивідууму може бути важко пройти весь цей код. Як ви вирішите цю проблему?

Мікрофронтенди

Просто. Команди незалежно працюють над різними проектами/мікрофронтендами без необхідності турбуватися про всі ці проблеми, а в кінцевому підсумку об'єднують їх разом під час виконання.

Тепер вам не доведеться турбуватися про довший час збірки/деплойменту, оскільки команди незалежно розгортають свої мікрофронтенди (з їхніми власними CI/CD пайплайнами), оновлюючи лише певні частини додатка, а не весь.

Також, одна технологічна стек обмежує експерименти та гнучкість для команд, що було б випадком для монолітної кодової бази, але це вирішується з мікрофронтендами. Ви можете використовувати різні фреймворки, якщо хочете, оскільки в кінцевому підсумку кінцевий продукт - це JavaScript, тож не має значення, який фреймворк ви використовуєте для цих мікрофронтендів. Для цього прикладу ми будемо використовувати React.

Також відеоверсія цього туторіалу доступна на youtube. Перевірте її!

Налаштування проекту

У нас буде два мікрофронтенди на React, обидва в монорепозиторії. Я використовую pnpm, оскільки це ефективніше за npm і добре працює з монорепозиторіями, але ви все ще можете використовувати npm, якщо хочете. Давайте ініціалізуємо цей монорепозиторій і всередині створимо директорії для обох наших React додатків. Я збережу їх у папці packages.

mkdir micro-frontend-demo  
cd micro-frontend-demo  
pnpm init // ІНІЦІАЛІЗАЦІЯ МОНОРЕПО  

mkdir packages  
cd packages  
mkdir host  
mkdir remote

Терміни host та remote в контексті мікрофронтендів та модуля федерації описують взаємодію між двома додатками.

Основний додаток — це головний додаток, який діє як точка входу для користувача. Він інтегрує компоненти з віддалених додатків, ефективно “розміщуючи” ці мікрофронтенди. Наприклад, головний додаток може бути платформою для електронної комерції, яка імпортує функціональність, таку як кошик чи розділ рекомендацій продуктів з віддаленого мікрофронтенда.

Віддалений додаток — це окремий додаток/модуль, який надає певні компоненти, функціональність або сервіси. Ці модулі динамічно завантажуються головним додатком під час виконання. У прикладі електронної комерції розділ кошика або модуль пошуку продуктів можуть бути реалізовані як віддалений додаток, який головний додаток підтягує за потреби.

Усередині папки host давайте створимо додаток на React за допомогою vite та встановимо залежності. Нам також потрібно додаткове бібліотеку під назвою vite plugin federation. Це те, що ми будемо використовувати для реалізації модульної федерації в нашому додатку.

cd host  
pnpm create vite@latest . --template react  
pnpm install  
pnpm install @originjs/vite-plugin-federation

Давайте також оновимо файл vite.config для цього додатка. Я розгляну конфігурацію через хвилину.

//HOST  
import { defineConfig } from 'vite';  
import federation from '@originjs/vite-plugin-federation';  
import react from '@vitejs/plugin-react';  

export default defineConfig({  
 plugins: [  
 react(),  
 federation({  
 name: 'app',  
 remotes: {  
 remoteApp: 'http://localhost:5001/assets/remoteEntry.js',  
 },  
 shared: ['react', 'react-dom'],  
 }),  
 ],  
 build: {  
 modulePreload: false,  
 target: 'esnext',  
 minify: false,  
 cssCodeSplit: false,  
 },  
});

Налаштування для віддаленого додатка буде також досить схожим.
Всередині папки remote ми створимо додаток на React, встановимо залежності разом з vite-plugin-federation та оновимо конфігурацію.

cd host  
pnpm create vite@latest . --template react  
pnpm install  
pnpm install @originjs/vite-plugin-federation
//REMOTE  
import { defineConfig } from 'vite';  
import react from '@vitejs/plugin-react';  
import federation from '@originjs/vite-plugin-federation';  
export default defineConfig({  
 plugins: [  
 react(),  
 federation({  
 name: 'remote_app',  
 filename: 'remoteEntry.js',  
 exposes: {  
 './Button': './src/components/Button',  
 },  
 shared: ['react', 'react-dom'],  
 }),  
 ],  
 build: {  
 modulePreload: false,  
 target: 'esnext',  
 minify: false,  
 cssCodeSplit: false,  
 },  
});

Давайте розглянемо конфігурацію федерації по черзі.

  • name: ім’я основного та віддаленого додатка.
  • shared: Ділення бібліотеками між основним і віддаленими додатками для уникнення дублювання.
  • filename(remote): Вказує на файл, де буде створено манифест модульної федерації (remoteEntry.js). Цей файл є точкою входу для основного додатка, щоб споживати модулі з цього віддаленого.
  • exposes(remote): Вказує, що цей віддалений додаток надає для інших додатків. На даний момент ми надамо компонент кнопки.
  • remotes(host): Вказує віддалені додатки, які буде споживати основний додаток.

Тепер розглянемо варіанти конфігурації збірки, які є спільними як для віддаленого, так і для основного додатка.

  • modulePreload: Контролює, як Vite обробляє попереднє завантаження модулів перед їхнім використанням, щоб покращити продуктивність сторінки, зменшуючи час на завантаження залежних модулів.
  • target: Вказує на версію JavaScript (esnext) для сучасних браузерів.
  • minify: Вимикає мінімізацію, щоб полегшити налагодження.
  • cssCodeSplit: Об'єднує CSS у один файл, замість того щоб розділяти його.

Сегмент React

Компонент кнопки React всередині віддаленого додатка досить простий. Він має одну змінну стану і при натисканні кнопки збільшує цей стан на одиницю. Легко.

import './Button.css';  
import { useState } from 'react';  
export const Button = () => {  
 const [state, setState] = useState(0);  
 return (  

 setState((s) => s + 1)}>    Ae bsds: {state}        
    );   };   export default Button; 
//Button.css   
.shared-btn {    
 background-color: skyblue;    
 border: 1px solid white;    
 color: rgb(0, 0, 0);    
 padding: 15px 30px;    
 text-align: center;    
 text-decoration: none;    
 font-size: 18px;   
 } 

Тепер ми імпортуємо цю кнопку в компонент App.

import reactLogo from './assets/react.svg';  
import './App.css';  
import Button from 'remoteApp/Button';      
function App() {    
 return (    
Vite + React

 Edit src/App.jsx and save to test HMR  

Click on the Vite and React logos to learn more
    );  
}   
export default App;  

Також я встановив фіксовані порти для обох додатків. Отже, основний додаток буде працювати на порту 5000, а віддалений додаток на порту 5001. Ви можете оновити ці налаштування в об'єкті scripts у файлі package.json обох додатків.

"dev": "vite --port 5001 --strictPort", //remote   
"dev": "vite --port 5000 --strictPort", //host 

Проблема в режимі розробки та HMR

Проблема полягає в тому, що ви не можете запустити віддалений додаток у режимі розробки. Якщо ви ознайомитесь з документацією, ви зрозумієте це.

pic

Отже, давайте це виправимо.
Також для отримання функціональності, схожої на гаряче перезавантаження (HMR) — коли я оновлюю щось у віддаленому додатку, зміни відображаються в основному додатку — я також одночасно запускаю команду vite preview. Тепер скрипт збірки для віддаленого додатку виглядатиме так.

"build": "vite build --watch & vite preview --port 5001 --strictPort"

Примітка: Якщо цей скрипт не працює на Windows, ви можете використати сторонню бібліотеку, таку як concurrently, щоб досягти цього.

Навіть із цією реалізацією, з якоїсь причини, я не зміг отримати гаряче перезавантаження при змінах у проекті. Тому в одному з обговорень хтось поділився простим плагіном, який надсилає повідомлення про перезавантаження з віддаленого кінця, коли відбувається збірка, а плагін-слухач на стороні хоста слухає це повідомлення про перезавантаження.

Отже, у конфігурації віддаленого додатку, відразу після плагіна федерації, додаємо цей користувацький плагін.

import { defineConfig } from 'vite';  
import react from '@vitejs/plugin-react';  
import federation from '@originjs/vite-plugin-federation';  
export default defineConfig({  
 plugins: [  
 react(),  
 federation({  
 ...  
 }),  
 {  
 name: 'vite-plugin-notify-host-on-rebuild',  
 apply(config, { command }) {  
 return Boolean(command === 'build' && config.build?.watch);  
 },  
 async buildEnd(error) {  
 if (!error) {  
 try {  
 await fetch('http://localhost:5000/__fullReload');  
 } catch (e) {  
 console.log(e);  
 }  
 }  
 },  
 },  
 ],  
...  
});

А в конфігурації хоста ми будемо використовувати цей плагін.

import { defineConfig } from 'vite';  
import federation from '@originjs/vite-plugin-federation';  
import react from '@vitejs/plugin-react';  

export default defineConfig({  
 plugins: [  
 react(),  
 federation({  
 ...  
 }),  
 {  
 name: 'vite-plugin-reload-endpoint',  
 configureServer(server) {  
 server.middlewares.use((req, res, next) => {  
 if (req.url === '/__fullReload') {  
 server.hot.send({ type: 'full-reload' });  
 res.end('Full reload triggered');  
 } else {  
 next();  
 }  
 });  
 },  
 },  
 ],  
 ...  
});

Тепер, якщо ви зробите будь-які зміни у віддаленому додатку, ви побачите оновлення в реальному часі в основному додатку. Однак це технічно не є HMR. Ми служимо фактичним продакшн бандлом, тому це, безумовно, дасть відчутний вплив, коли додаток буде рости в розмірах, але хоча б у вас є щось, з чим можна працювати.

Спільний стан

Отже, ми маємо 2 React-додатки, які використовують модульну федерацію та працюють у налаштуванні мікрофронтендів. Але очевидне наступне питання — що щодо стану? Припустимо, я хочу оновити стан з одного мікрофронтенда в інший. Як ці 2 додатки можуть комунікувати? Ці 2 додатки працюють в ізольованих просторах, чи це взагалі можливо? Звісно, це можливо, і насправді це не так складно, як може здатися.

Давайте розглянемо простий приклад.
Створюю провайдер контексту React, який надає той самий стан лічильника.

//MyContext.jsx  
import { createContext, useContext, useState } from 'react';  
const MyContext = createContext(undefined);  
export const MyProvider = ({ children }) => {  
 const [count, setCount] = useState(0);  
 const increment = () => setCount((prev) => prev + 1);  
 return {children};  
};  
export const useSharedState = () => {  
 const context = useContext(MyContext);  
 if (!context) throw new Error('useSharedState must be used within a MyProvider');  
 return context;  
};

Тепер мені потрібно зробити цей провайдер доступним у модульній федерації.

federation({  
 name: 'remote_app',  
 filename: 'remoteEntry.js',  
 exposes: {  
 './Button': './src/components/Button',  
 './MyProvider': './src/MyContext', // ПРОВАЙДЕР ДОСТУПНИЙ  
 },  
 shared: ['react', 'react-dom'],  
}),

У додатку-хості обгорнемо провайдер на рівні кореня, але обов'язково обгортаємо тільки ті компоненти, яким потрібен цей стан, інакше це може призвести до непотрібних повторних рендерів.

//main.jsx всередині хоста  
import { StrictMode } from 'react';  
import { createRoot } from 'react-dom/client';  
import './index.css';  
import App from './App.jsx';  
import { MyProvider } from 'remoteApp/MyProvider';  

createRoot(document.getElementById('root')).render(  





);

Тепер всередині файлу App використаємо хук useSharedState з контексту.

import reactLogo from './assets/react.svg';  
import './App.css';  
import Button from 'remoteApp/Button';  
import { useSharedState } from 'remoteApp/MyProvider';   
function App() {  
 const { count, increment } = useSharedState(); // СПІЛЬНИЙ СТАН  
 return (  

    // СПІЛЬНИЙ СТАН    Increment: {count}    
Vite + React

 Edit src/App.jsx and save to test HMR  

Click on the Vite and React logos to learn more
    );   }   export default App; 

Всередині компонента кнопки віддаленого додатку також використаємо той самий спільний стан.

import './Button.css';  
import { useSharedState } from '../MyContext'; // СПІЛЬНИЙ СТАН  
export const Button = () => {  
    const { count, increment } = useSharedState(); // СПІЛЬНИЙ СТАН  
    return (    
    //СПІЛЬНИЙ СТАН    hello world: {count} //СПІЛЬНИЙ СТАН        
    );   };   export default Button; 

Стан повинен оновлюватися, не залежно від того, на яку кнопку я натискаю. Отже, він спільний для обох компонентів: віддаленого і цього додатку-хоста. Це так просто — ділитися станом. Також ще одна річ, не впевнений, чи згадував я це раніше, але додаток-хост також може надавати компоненти так само, як і віддалений додаток. Але в такому випадку вам також потрібно буде зібрати додаток-хост і попередньо переглянути його, оскільки він також матиме файл remoteEntry. Тому вам потрібно буде врахувати це, але так, хост може також надавати компоненти та сервіси так само, як і віддалений додаток. Якщо ви використовуєте інші менеджери стану, такі як redux або zustand, ви можете дотримуватися того ж процесу. Просто переконайтеся, що додали ці залежності до масиву shared (в конфігурації vite).

Висновок

І ось базова реалізація архітектури мікрофронтендів в React і vite.
Код для цього прикладу можна знайти в цьому github репозиторії.

Також відеоверсія цього посібника доступна на youtube. Обов'язково подивіться!

Якщо у вас є інші запитання чи пропозиції, обов'язково пишіть їх у коментарях або звертайтеся до мене через будь-яку з моїх соціальних мереж. Успіхів!

YouTube LinkedIn Twitter GitHub

Дякую, що є частиною спільноти

Перед тим як піти:

Перекладено з: Microfrontends with ReactJS & Vite

Leave a Reply

Your email address will not be published. Required fields are marked *