[JavaScript, ReactJS, TypeScript] Как готовить микрофронтенды в Webpack 5
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет, меня зовут Иван и я фронтенд-разработчик.На моём комментариипро микрофронтенды набралось целых три лайка, поэтому я решил написать статью с описанием всех шишек, что наш стрим набил и набивает в результате внедрения микрофронтендов.Начнём с того, что ребята с Хабра (@artemu78, @dfuse, @Katsuba) уже писали про Module Federation, так что, моя статья - это не что-то уникальное и прорывное. Скорее, это шишки, костыли и велосипеды, которые полезно знать тем, кто собирается использовать данную технологию.ПричинаПричина, по которой решено было внедрять микросервисный подход на фронте, довольно простая - много команд, а проект один, нужно было как-то разделить зоны ответственности и распараллелить разработку. Как раз в тот момент, мне на глаза попался доклад Павла Черторогова про Webpack 5 Module Federation. Честно, это перевернуло моё видение современных веб-приложений. Я очень вдохновился и начал изучать и крутить эту технологию, чтобы понять, можно ли применить это в нашем проекте. Оказалось, всё что нужно, это дописать несколько строк в конфиг Webpack, создать пару компонентов-хелперов, и... всё завелось.НастройкаИтак, что же нужно сделать, чтобы запустить микрофронтенды на базе сборки Webpack 5?Для начала, убедитесь, что используете Webpack пятой версии, потому что Module Federation там поддерживается из коробки.Настройка shell-приложенияТак как, до внедрения микрофронтендов, у нас уже было действующее приложение, решено было использовать его в качестве точки входа и оболочки для подключения других микрофронтендов. Для сборки использовался Webpack версии 4.4 и при обновлении до 5 версии возникли небольшие проблемы с некоторыми плагинами. К счастью, это решилось простым поднятием версий плагинов.Чтобы создать контейнер на базе сборки Webpack и при помощи этого контейнера иметь возможность импортировать ресурсы с удаленных хостов добавляем в Webpack-конфиг следующий код:
const webpack = require('webpack');
// ...
const { ModuleFederationPlugin } = webpack.container;
const deps = require('./package.json').dependencies;
module.exports = {
// ...
output: {
// ...
publicPath: 'auto', // ВАЖНО! Указывайте либо реальный publicPath, либо auto
},
module: {
// ...
},
plugins: [
// ...
new ModuleFederationPlugin({
name: 'shell',
filename: 'shell.js',
shared: {
react: { requiredVersion: deps.react },
'react-dom': { requiredVersion: deps['react-dom'] },
'react-query': {
requiredVersion: deps['react-query'],
},
},
remotes: {
widgets: `widgets@http://localhost:3002/widgets.js`,
},
}),
],
devServer: {
// ...
},
};
Теперь нам нужно забутстрапить точку входа в наше приложение, чтобы оно запускалось асинхронно, для этого создаем файл bootstrap.tsx и кладем туда содержимое файла index.tsx
// bootstrap.tsx
import React from 'react';
import { render } from 'react-dom';
import { App } from './App';
import { config } from './config';
import './index.scss';
config.init().then(() => {
render(<App />, document.getElementById('root'));
});
А в index.tsx вызываем этот самый bootstrap
import('./bootstrap');
В общем то всё, в таком виде уже можно импортировать ваши микрофронтенды - они указываются в объекте remotes в формате <name>@<адрес хоста>/<filename>. Но нам такая конфигурация не подходит, ведь на момент сборки приложения мы ещё не знаем откуда будем брать микрофронтенд, к счастью, есть готовое решение, поэтому возьмем код из примера для динамических хостов, так как наше приложение написано на React, то оформим хэлпер в виде React-компонента LazyService:
// LazyService.tsx
import React, { lazy, ReactNode, Suspense } from 'react';
import { useDynamicScript } from './useDynamicScript';
import { loadComponent } from './loadComponent';
import { Microservice } from './types';
import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';
interface ILazyServiceProps<T = Record<string, unknown>> {
microservice: Microservice<T>;
loadingMessage?: ReactNode;
errorMessage?: ReactNode;
}
export function LazyService<T = Record<string, unknown>>({
microservice,
loadingMessage,
errorMessage,
}: ILazyServiceProps<T>): JSX.Element {
const { ready, failed } = useDynamicScript(microservice.url);
const errorNode = errorMessage || <span>Failed to load dynamic script: {microservice.url}</span>;
if (failed) {
return <>{errorNode}</>;
}
const loadingNode = loadingMessage || <span>Loading dynamic script: {microservice.url}</span>;
if (!ready) {
return <>{loadingNode}</>;
}
const Component = lazy(loadComponent(microservice.scope, microservice.module));
return (
<ErrorBoundary>
<Suspense fallback={loadingNode}>
<Component {...(microservice.props || {})} />
</Suspense>
</ErrorBoundary>
);
}
Хук useDynamicScript нужен нам, чтобы в рантайме прикреплять загруженный скрипт к нашему html-документу.
// useDynamicScript.ts
import { useEffect, useState } from 'react';
export const useDynamicScript = (url?: string): { ready: boolean; failed: boolean } => {
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState(false);
useEffect(() => {
if (!url) {
return;
}
const script = document.createElement('script');
script.src = url;
script.type = 'text/javascript';
script.async = true;
setReady(false);
setFailed(false);
script.onload = (): void => {
console.log(`Dynamic Script Loaded: ${url}`);
setReady(true);
};
script.onerror = (): void => {
console.error(`Dynamic Script Error: ${url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(script);
return (): void => {
console.log(`Dynamic Script Removed: ${url}`);
document.head.removeChild(script);
};
}, [url]);
return {
ready,
failed,
};
};
loadComponent это обращение к Webpack-контейнеру, по сути - обычный динамический импорт.
// loadComponent.ts
export function loadComponent(scope, module) {
return async () => {
// Initializes the share scope. This 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;
};
}
Ну и напоследок опишем тип для нашего микросервиса, дженерик нужен для того, чтобы правильно работала типизация пропсов.
// types.ts
export type Microservice<T = Record<string, unknown>> = {
url: string;
scope: string;
module: string;
props?: T;
};
- url - имя хоста + имя контейнера (например, http://localhost:3002/widgets.js), с которого мы хотим подтянуть модуль
- scope - параметр name, который мы укажем в удаленном конфиге ModuleFederationPlugin
- module - имя модуля, который мы хотим подтянуть
- props - опциональный параметр, если вдруг наш микросервис требует пропсы, нужно их типизировать
Вызов компонента LazyService происходит следующим образом:
import React, { FC, useState } from 'react';
import { LazyService } from '../../components/LazyService';
import { Microservice } from '../../components/LazyService/types';
import { Loader } from '../../components/Loader';
import { Toggle } from '../../components/Toggle';
import { config } from '../../config';
import styles from './styles.module.scss';
export const Video: FC = () => {
const [microservice, setMicroservice] = useState<Microservice>({
url: config.microservices.widgets.url,
scope: 'widgets',
module: './Zack',
});
const toggleMicroservice = () => {
if (microservice.module === './Zack') {
setMicroservice({ ...microservice, module: './Jack' });
}
if (microservice.module === './Jack') {
setMicroservice({ ...microservice, module: './Zack' });
}
};
return (
<>
<div className={styles.ToggleContainer}>
<Toggle onClick={toggleMicroservice} />
</div>
<LazyService microservice={microservice} loadingMessage={<Loader />} />
</>
);
};
В общем то, по коду видно, что мы можем динамически переключать наши модули, а основной url хранить, например, в конфиге.Так, с shell-приложением вроде разобрались, теперь нужно откуда-то брать наши модули.Настройка микрофронтендаДля начала проделываем все те же манипуляции что и в shell-приложении и убеждаемся, что версия Webpack => 5Настраиваем ModuleFederationPlugin, но уже со своими параметрами, эти параметры указываем при подключении модуля в основное приложение.
// ...
new ModuleFederationPlugin({
name: 'widgets',
filename: 'widgets.js',
shared: {
react: { requiredVersion: deps.react },
'react-dom': { requiredVersion: deps['react-dom'] },
'react-query': {
requiredVersion: deps['react-query'],
},
},
exposes: {
'./Todo': './src/App',
'./Gallery': './src/pages/Gallery/Gallery',
'./Zack': './src/pages/Zack/Zack',
'./Jack': './src/pages/Jack/Jack',
},
}),
// ...
В объекте exposes указываем те модули, которые мы ходим отдать наружу, точку входа в приложение так же нужно забутстрапить. Если в микрофронтенде нам не нужны модули с других хостов, то компонент LazyService тут не нужен.Вот и всё, получен работающий прототип микрофронтенда.Выглядит круто, работает тоже круто. Общие зависимости не грузятся повторно, версии библиотек рулятся плагином, можно динамически переключать модули, в общем, сказка. Если копать глубже, то это очень гибкая технология, можно использовать её не только с React и JavaScript, но и со всем, что переваривает Webpack, то есть теоретически можно подружить части приложения написанные на разных фреймворках, это конечно не очень хорошо, но сделать так можно. Можно собрать модули и положить на CDN, можно использовать контейнер как общую библиотеку компонентов для нескольких приложений. Возможностей реально много.ПроблемыКогда удалось запустить это в нашем проекте я был доволен, нет, очень доволен, но это длилось недолго, после того как началась реальная работа над микрофронтендами, начали всплывать наши любимые подводные камни, а теперь поговорим он них подробнее.Потеря контекстов в React-компонентахКак только понадобилось работать с контекстом библиотеки react-router, то возникли проблемы, при попытке использовать в микрофронтенде хук useLocation, например, приложение вылетало с ошибкой.
Ошибка при попытке обращения к контексту shell-приложения из микрофронтендаДля взаимодействия с бэкендом мы используем Apollo, и хотелось, чтобы ApolloClient объявлялся только единожды в shell-приложении. Но при попытке из микрофронтенда просто использовать хук useQuery, в рантайме приложение вылетало с такой же ошибкой как и для useLocation.Экспериментальным путём было выяснено, для того чтобы контексты правильно работали, нужно в микрофронтендах использовать версию npm-пакета не выше, чем в shell-приложение, так что за этим нужно внимательно следить.Дублирование UI-компонентов в shell-приложении и микрофронтендеТак как разработка ведётся разными командами, есть шанс, что разработчики напишут компоненты с одинаковым функционалом и в shell-приложении и в микрофронтенде. Чтобы этого избежать, есть несколько решений:
- Выносить UI-компоненты в отдельный npm-пакет и использовать его как shared-модуль
- "Делиться" компонентами через ModuleFederationPlugin
В принципе, у обоих подходов есть свои плюсы, но мы выбрали первый, потому что так удобнее и прозрачнее управлять библиотекой компонентов. Да и саму технологию Module Federation хотелось использовать как механизм для построения микрофронтендов, а не аналог npm.ЗаключениеПока что выглядит так, что переход на Webpack 5 Module Federation решает проблему, которая стояла перед нашим стримом, а именно - разделение зоны ответственности и распараллеливание разработки. При этом, нет больших накладных расходов при разработке, а настройка довольно проста даже для тех, кто не знаком с этой технологией.Минусы у этого подхода конечно же есть, накладные расходы для развертывания зоопарка микрофронтендов будут значительно выше, чем для монолита. Если над вашим приложением работает одна-две команды и оно не такое большое, то наверное не стоит делить его на микросервисы.Но для нашей конкретной проблемы, это решение подошло хорошо, посмотрим, как оно покажет себя в будущем, технология развивается и уже появляются фреймворки и библиотеки, которые под капотом используют Module Federation.Полезные ссылкиРепозиторий из примераДокументация Module Federation в доках Webpack 5Примеры использования Module FederationПлейлист по Module Federation на YouTube
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Интерфейсы, Natural Language Processing, Голосовые интерфейсы] От дизайна до разработки: как делать качественные смартапы для виртуальных ассистентов Салют
- [JavaScript, ReactJS] Как мы решили проблемы с z-index
- [Разработка веб-сайтов, JavaScript, Совершенный код, HTML] Целительная сила JavaScript (перевод)
- [Angular] Добавляем PWA в Angular приложение
- [Angular, TypeScript] Обмен данными между компонентами Angular
- [Высокая производительность, Тестирование IT-систем, Анализ и проектирование систем, Разработка на Raspberry Pi] История про Гену, Чебурашку и тестирование производительности реактивного приложения, работающего на Raspberry Pi
- [Программирование, Управление проектами, Облачные сервисы, Микросервисы] О мифологии миграции монолита в облака
- [Управление разработкой, Управление проектами] Как работать в команде, которая пишет на 5 языках
- [Децентрализованные сети, JavaScript, Node.JS, Хранилища данных] Musiphone — децентрализованный музыкальный плеер
- [JavaScript, Проектирование и рефакторинг, ReactJS] Как мы сетапили монорепозиторий с SSR и SPA для Otus.ru
Теги для поиска: #_javascript, #_reactjs, #_typescript, #_webpack, #_webpack5, #_module_federation, #_microservices, #_frontend, #_react, #_reactjs, #_react.js, #_microfrontends, #_javascript, #_reactjs, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:58
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет, меня зовут Иван и я фронтенд-разработчик.На моём комментариипро микрофронтенды набралось целых три лайка, поэтому я решил написать статью с описанием всех шишек, что наш стрим набил и набивает в результате внедрения микрофронтендов.Начнём с того, что ребята с Хабра (@artemu78, @dfuse, @Katsuba) уже писали про Module Federation, так что, моя статья - это не что-то уникальное и прорывное. Скорее, это шишки, костыли и велосипеды, которые полезно знать тем, кто собирается использовать данную технологию.ПричинаПричина, по которой решено было внедрять микросервисный подход на фронте, довольно простая - много команд, а проект один, нужно было как-то разделить зоны ответственности и распараллелить разработку. Как раз в тот момент, мне на глаза попался доклад Павла Черторогова про Webpack 5 Module Federation. Честно, это перевернуло моё видение современных веб-приложений. Я очень вдохновился и начал изучать и крутить эту технологию, чтобы понять, можно ли применить это в нашем проекте. Оказалось, всё что нужно, это дописать несколько строк в конфиг Webpack, создать пару компонентов-хелперов, и... всё завелось.НастройкаИтак, что же нужно сделать, чтобы запустить микрофронтенды на базе сборки Webpack 5?Для начала, убедитесь, что используете Webpack пятой версии, потому что Module Federation там поддерживается из коробки.Настройка shell-приложенияТак как, до внедрения микрофронтендов, у нас уже было действующее приложение, решено было использовать его в качестве точки входа и оболочки для подключения других микрофронтендов. Для сборки использовался Webpack версии 4.4 и при обновлении до 5 версии возникли небольшие проблемы с некоторыми плагинами. К счастью, это решилось простым поднятием версий плагинов.Чтобы создать контейнер на базе сборки Webpack и при помощи этого контейнера иметь возможность импортировать ресурсы с удаленных хостов добавляем в Webpack-конфиг следующий код: const webpack = require('webpack');
// ... const { ModuleFederationPlugin } = webpack.container; const deps = require('./package.json').dependencies; module.exports = { // ... output: { // ... publicPath: 'auto', // ВАЖНО! Указывайте либо реальный publicPath, либо auto }, module: { // ... }, plugins: [ // ... new ModuleFederationPlugin({ name: 'shell', filename: 'shell.js', shared: { react: { requiredVersion: deps.react }, 'react-dom': { requiredVersion: deps['react-dom'] }, 'react-query': { requiredVersion: deps['react-query'], }, }, remotes: { widgets: `widgets@http://localhost:3002/widgets.js`, }, }), ], devServer: { // ... }, }; // bootstrap.tsx
import React from 'react'; import { render } from 'react-dom'; import { App } from './App'; import { config } from './config'; import './index.scss'; config.init().then(() => { render(<App />, document.getElementById('root')); }); import('./bootstrap');
// LazyService.tsx
import React, { lazy, ReactNode, Suspense } from 'react'; import { useDynamicScript } from './useDynamicScript'; import { loadComponent } from './loadComponent'; import { Microservice } from './types'; import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary'; interface ILazyServiceProps<T = Record<string, unknown>> { microservice: Microservice<T>; loadingMessage?: ReactNode; errorMessage?: ReactNode; } export function LazyService<T = Record<string, unknown>>({ microservice, loadingMessage, errorMessage, }: ILazyServiceProps<T>): JSX.Element { const { ready, failed } = useDynamicScript(microservice.url); const errorNode = errorMessage || <span>Failed to load dynamic script: {microservice.url}</span>; if (failed) { return <>{errorNode}</>; } const loadingNode = loadingMessage || <span>Loading dynamic script: {microservice.url}</span>; if (!ready) { return <>{loadingNode}</>; } const Component = lazy(loadComponent(microservice.scope, microservice.module)); return ( <ErrorBoundary> <Suspense fallback={loadingNode}> <Component {...(microservice.props || {})} /> </Suspense> </ErrorBoundary> ); } // useDynamicScript.ts
import { useEffect, useState } from 'react'; export const useDynamicScript = (url?: string): { ready: boolean; failed: boolean } => { const [ready, setReady] = useState(false); const [failed, setFailed] = useState(false); useEffect(() => { if (!url) { return; } const script = document.createElement('script'); script.src = url; script.type = 'text/javascript'; script.async = true; setReady(false); setFailed(false); script.onload = (): void => { console.log(`Dynamic Script Loaded: ${url}`); setReady(true); }; script.onerror = (): void => { console.error(`Dynamic Script Error: ${url}`); setReady(false); setFailed(true); }; document.head.appendChild(script); return (): void => { console.log(`Dynamic Script Removed: ${url}`); document.head.removeChild(script); }; }, [url]); return { ready, failed, }; }; // loadComponent.ts
export function loadComponent(scope, module) { return async () => { // Initializes the share scope. This 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; }; } // types.ts
export type Microservice<T = Record<string, unknown>> = { url: string; scope: string; module: string; props?: T; };
import React, { FC, useState } from 'react';
import { LazyService } from '../../components/LazyService'; import { Microservice } from '../../components/LazyService/types'; import { Loader } from '../../components/Loader'; import { Toggle } from '../../components/Toggle'; import { config } from '../../config'; import styles from './styles.module.scss'; export const Video: FC = () => { const [microservice, setMicroservice] = useState<Microservice>({ url: config.microservices.widgets.url, scope: 'widgets', module: './Zack', }); const toggleMicroservice = () => { if (microservice.module === './Zack') { setMicroservice({ ...microservice, module: './Jack' }); } if (microservice.module === './Jack') { setMicroservice({ ...microservice, module: './Zack' }); } }; return ( <> <div className={styles.ToggleContainer}> <Toggle onClick={toggleMicroservice} /> </div> <LazyService microservice={microservice} loadingMessage={<Loader />} /> </> ); }; // ...
new ModuleFederationPlugin({ name: 'widgets', filename: 'widgets.js', shared: { react: { requiredVersion: deps.react }, 'react-dom': { requiredVersion: deps['react-dom'] }, 'react-query': { requiredVersion: deps['react-query'], }, }, exposes: { './Todo': './src/App', './Gallery': './src/pages/Gallery/Gallery', './Zack': './src/pages/Zack/Zack', './Jack': './src/pages/Jack/Jack', }, }), // ... Ошибка при попытке обращения к контексту shell-приложения из микрофронтендаДля взаимодействия с бэкендом мы используем Apollo, и хотелось, чтобы ApolloClient объявлялся только единожды в shell-приложении. Но при попытке из микрофронтенда просто использовать хук useQuery, в рантайме приложение вылетало с такой же ошибкой как и для useLocation.Экспериментальным путём было выяснено, для того чтобы контексты правильно работали, нужно в микрофронтендах использовать версию npm-пакета не выше, чем в shell-приложение, так что за этим нужно внимательно следить.Дублирование UI-компонентов в shell-приложении и микрофронтендеТак как разработка ведётся разными командами, есть шанс, что разработчики напишут компоненты с одинаковым функционалом и в shell-приложении и в микрофронтенде. Чтобы этого избежать, есть несколько решений:
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:58
Часовой пояс: UTC + 5