- основной стек
- typescript
- react
- webpack/vite module federation
- основной стек библиотек
- tsc (для библиотек)
- tsup (сборщик для ui библиотек)
- storybook
(возможно) используем bit.dev для разработки библиотек компонентов- отказываемся от next.js - надо раскурить, как у него дела с mf сейчас. UPD: немного раскурил - стало лучше, но loadRemote НЕ РАБОТАЕТ
(возможно) используем фреймворк для mf - piral или tramvai.js
Module Federation
- vite
- webpack
next.js
- react
- react-dom
- react-router
- react-routed-dom
- styled-components / emotion
- @resource/ui-config
Для работы микрофронтов вместе нужно реализовать несколько библиотек
- @resource/configs - конфиги (eslint/prettier/standard version/stylelint/etc.)
- @resource/ui-app - конфиги сборки (vite, webpack, next)
- @resource/ui-config - общий Context между микрофронтами
Предоставляет
- компоненты, завязанные на общий Context (AuthProvider, LogsProvider, Router, etc.)
- инстансы общих сервисов (eventBusService, logsService, etc.)
- @resource/ui-base - общие компоненты (кнопочки и т.д.)
- @resource/api-schemas - api swagger схемы (+ ts) всех микросервисов
- @resource/api-schemas-* - api swagger схемы (+ ts) для каждого микросервиса
- @resource/fetchers - методы для генерации http/graphql/etc. клиентов (в основном - taxios)
- @resource/federated-types - typescript декларации каждого *-mf микрофронта
- (позже)
- @resource/ui-utils - утилитарные функции фронта
- @resource/generator - генерация файловой структруы микрофронтов через cli
@module-federation/enhancedvite-plugin-federation - сборка микрофронта- @module-federation/runtime - подключение микрофронтов
- @module-federation/native-federation-typescript - генерация типов микрофронтов
- @mui/material - UI kit
Мы используем “Стандартный подход”. Это значит, что каждому микрофронту выделяется свой кусок URL
Однако это не отменяет того, что микрофронты могут предоставлять дополнительные компоненты, которые могут использовать другие микрофронты.
Сервисы
- @resource/*-host-frontend - точка входа в ui приложение (host)
- @resource/resource-host-frontend - точка входа в ui приложение ресурс
- @resource/burnee-host-frontend - точка входа в ui приложение burnee
- @resource/sandbox-frontend - среда для отладки микрофронтов
- @resource/core-mf - предоставляет служебные сервисы (eventbus/etc.), авторизацию, ролевую систему, конфиги
- @resource/*-mf - микрофронт под конкретный домен/бизнес функцию
Взаимодействие
- Роутинг: предоставляет @resource/core-mf
- Запросы: через api-gw
- Обмен данными/событиями (между микрофронтами): eventbus
- Общие данные (user/configs/etc.): предоставляет @resource/core-mf
(полезное) Статья: https://holyjs.ru/talks/0beb987452b747b28fe522bf7ecdd384/
См. https://excalidraw.com/#json=MjcGDwkHuzjtgI50gw5-T,jaoLSrtlFK_Tfj7rmUVx1g
С шиной и api gateway
@TODO: Надо придумать, как нормально работать с конфигами
import { loadRemote, init } from '@module-federation/runtime';
// .env
// # CORE_MF_ENTRY=http://localhost:3000
// # CORE_MF_ENTRY=https://sut.web-beee.ru
// vite.config.ts
// proxy: { // (!) LOCAL ONLY (!)
// '/apps/@resource/core-mf/**': process.env.CORE_MF_ENTRY,
// '/apps/@resource/resource-mf/**': process.env.RESOURCE_MF_ENTRY,
// '/apps/@resource/burnee-mf/**': process.env.BURNEE_MF_ENTRY,
// }
// @TODO - На подумать
// let config = {}
// window.setConfig = (_config) => {
// config = _config
// }
init({
name: '@resoruce/host',
remotes: [
{
name: '@resource/core-mf',
entry: '/apps/@resource/core-mf/remoteEntry.js'
},
// ...
]
})
const CoreProvider = React.lazy(() => loadRemote('@resource/core-mf'))
const ResourceApp = React.lazy(() => loadRemote('@resource/resource-mf'))
const BurneeApp = React.lazy(() => loadRemote('@resource/burnee-mf'))
return <CoreProvider>
<Routes>
<Route path="/resource"> // config.resource.path
<React.Suspense>
<ResourceApp />
</React.Suspense>
</Route>
<Route path="/burnee">
<React.Suspense>
<BurneeApp />
</React.Suspense>
</Route>
</Routes>
</CoreProvider>
sandbox (standalone) режим
const CoreProvider = React.lazy(() => loadRemote('@resource/core-mf'))
ReactDOM.render(
<CoreProvider>
<App basePath={config.APP_BASE_URL} />
</CoreProvider>
, document.getElementById(APP_ID));Использование внутри Host
Использование компонентов из одних микрофронтов в других микрофронтах
├── public
│ ├── static
│ │ ├── **/*.*
│ ├── index.html
├── src
│ ├── index.ts
├── dist # ignored
├── node_modules # ignored
├── tsconfig.json
├── federation.config.json
├── vite.config.js # webpack.config.js
└── package.json
Обязательные
# npm
react
react-router
# custom
@resource/ui-app
@resource/ui-config
@resource/ui-base
@resource/ui-utils
@resource/api-schemas
@resource/federated-types
@resource/configs# Запускает процесс инициализации кодовой базы микрофронта для репозитория (https://github.com/yeoman/yo)
$ npx @resource/generator mf// federationRuntime.ts
import { name, version } from '../../package.json';
import { createFederationRuntime, resolveEntryPath } from '@resource/ui-config';
// настраиваем подключение микрофронтов
const federationRuntime = createFederationRuntime({
name, // '@resoruce/users-mf',
version,
remotes: [ // здесь указываем только те микрофронты, которые нам нужны
// @NOTE: Возможно можно вынести в библу общим списком (?)
{
name: '@resource/core-mf',
// (dev) proxy /apps/@resource/core-mf -> http://localhost:3001 (core-microfrontend)
entry: resolveEntryPath('/apps/@resource/core-mf/remoteEntry.js'),
},
],
});
export default federationRuntime;// App.tsx
import React from 'react';
import federationRuntime from '~/lib/federationRuntime';
const Users = React.lazy(() => federationRuntime.loadRemote('@resoruce/users-mf/Users'))
const App = () => {
return <React.Suspense fallback="Загрузка">
<Users />
</React.Suspense>
}// пример нахера это надо
// Button.tsx // @resource/core-mf
const Button = () => {
return <button />
}
export default Button;
// Card.tsx // @resource/users-mf
import { loadRemote} from '@module-federation/runtime';
const Button = React.lazy(() => loadRemote('@resource/core-mf/Button'))
const Card = () => {
return <div><Button /></div>
}
export default Card;
// App // @resource/resource-mf
import { init, loadRemote } from '@module-federation/runtime';
init(/* ... */) // init remotes: [@resource/users-mf, @resource/users-mf]
// (!) ERROR (!)
// 2й дочерный микрофронт не знает про init родителя -> loadRemote сфейлится
// и не должен (!), у каждого consumer'а свой init
const Card = React.lazy(() => loadRemote('@resource/users-mf/Card '))
const App = () => {
return <Card />
}// Например нам нужен AuthProvider + хук useAuth из другого микрофронта
// Button.tsx
// (!) Используем top level await (!)
const useAuth = await loadRemote('@resoruce/users-mf/AuthProvider').then(module => module.useAuth)
const Button = () => {
const auth = useAuth()
// ....
}// App.tsx
import React from 'react';
import federationRuntime from '~/lib/federationRuntime';
const Button = React.lazy(() => import('./Button'))
const AuthProvider = React.lazy(() => federationRuntime.loadRemote('@resoruce/users-mf/AuthProvider'))
const App = () => {
return <React.Suspense fallback="Загрузка">
<AuthProvider>
<Button />
</AuthProvider>
</React.Suspense>
}- микровиджетный подход в Тинькофф Обслуживание - https://holyjs.ru/talks/0beb987452b747b28fe522bf7ecdd384/
- Реализуем библиотеку @resource/configs с eslint/stylelint/prettier/tsconfig/etc. конфигами
- Реализуем библиотеку @resource/ui-app с vite конфигом
- Выпиливаем next.js
-
Конфиги заменяем импорт из @resource/configs
-
Сборку заменяем на @resource/ui-app
-
Роутер - react-router@6
- Берём и , не используем createBrowserRouter
- UPD: НАДО ИССЛЕДОВАТЬ С useRoutes
- Все роуты тащим в константу Routes.ts
// Routes.ts const APP_BASE_ROUTE = '/ui'; const APP_ROUTES = { HOME: `${APP_BASE_ROUTE}`, // можно использовать generateRoute USERS: `${APP_BASE_ROUTE}/users`, USER_PAGE: `${APP_BASE_ROUTE}/users/:id`, } as const; // обязательно, чтобы типы сохранились export { APP_ROUTES as Routes } // (См. после настройки типов (!)) // Пример использования в другом микрофронте import type { Routes } from '@resource/federated-types' import { generateRouting } from '@resource/ui-routing' const generatePath = generateRouting<Routes>() return <Link to={generatePath<Routes>('/ui/users/:id', { id: 123 })} /> // TS ERROR return <Link to={generatePath<Routes>('/ui/users1/123123' as Routes[string])} />
-
Настраиваем
- Формируем массив AppPages
import React from 'react'; import { Routes } from '~/constants/Routes'; interface AppPage { path: typeof Routes[keyof typeof Routes]; page: React.ElementType; exact?: boolean; // ... } const AppPages: AppPage[] = [ { path: Routes.HOME, page: () => import('../../pages/home/index.tsx'), // заготовка под lazy exact: true, }, // ... ] export { AppPages }
-
Инициализируем
import React from 'react'; import { Routes, Route } from 'react-router-dom'; interface AppRouterProps { appRoutes: typeof AppRoutes; } const AppRoutes = ({ appRoutes }: AppRouterProps) => { // где-то выше в AppProvider рисуем <Outlet />, в него пихаем <AppErrorBoundary> и прочее return ( <Routes> {appRoutes.map((appRoute) => ( <Route key={appRoute.path} exact={appRoute.exact} path={appRoute.path} element={appRoute.page} # ТУТ НАДО SUSPENSE (lazy) /> ))} // <PageError /> - компонент ошибки <Route path="*" element={<PageError statusCode={404} />} /> </Routes> ); }; export default AppRoutes;
-
Настраиваем lazy
- Берём и , не используем createBrowserRouter
-
Настраиваем proxy до других микрофронтов в dev режиме
- НЕ используем proxy в прод режиме, больше у фронтов не будет своего сервера (в дев режиме можно использовать)
-
- Выносим shared компоненты в библиотеку @resource/ui-base, убираем зависимости от next (если они есть)
- НУЖЕН АНАЛИЗ, ЧТО ТАЩИМ В БИБЛУ, А ЧТО НЕТ
- для сборки используем tsup
- Подготавливаем “модульность”
-
Api
- Выпиливаем глобальные fetch функции (инкапсулируем в Service классы)
- taxios инстансы запихиваем в ApiProvider, а сервисы - в ServicesProvider / FetchersProvider
- ApiProvider обязательно принимает basePath путь до API
- const { usersFetcher, projectsFetchers } = useFetchers()
-
CoreProvider, AppProvider
# Примерная (схематичная) файловая структура промежуточного перехода на Module Federation # Примеры компонентов могут быть разбиты н дополнительные файлы/папки - /exposed - Button.tsx // <AppProvider><Button /></ AppProvider> - /src - /components - /app - AppProvider.tsx # тут весь необходимый ВНУТРЕННИЙ Context обвес (ApiProvider, ServicesProvider, ThemeProvider, стор, модальки и прочее) - App.tsx # Рисует <Routes> (компоненты берёт из /src/pages) - /button - Button.tsx - /pages - ... # Страницы - CoreProvider.tsx # тут весь необходимый ВНЕШНИЙ Context обвес (BrowserRouter, UserProvider, AvilityProvider и прочее) (потом переедет в отдельный микрофронт) - bootstrap.tsx # Рисует <CoreProvider><AppProvider><App /></AppProvider></CoreProvider> # @FIXME: Возможно нужен standalone под каждый Exposed компонент - index.ts # импортирует bootstrap.tsx ("standalone режим")
-
Реализуем UserProvider, AbilityProivder, вытаскиваем эту логику из mobx
-
- @TODO
- Прикручиваем автотесты в либы
- vitest (unit, integration)
- react testing library (snapshot)
- playwright (e2e)
- Прикручиваем автотесты в проекты
- (всё, что сверху)
- msw (+ kubb faker) (mock)
- Реализуем Module Federation конфиг в @resource/ui-app
- Используем плагин https://github.com/originjs/vite-plugin-federation (в будущем перейдём на @module-federation/esbuild)
- Реализуем @resource/ui-config библиотеку
- Выносим все ВНЕШНИЕ Context’ы, которые предоставляет CoreProvider (AbilityContext, UserContext, etc.)
- Реализуем методы для работы с Module Federation (на базе @module-federation/runtime)
- createFederationRuntime
- resolveEntryPath
- Реализуем core-mf приложение
- Тащим конфиг из @resource/ui-app
- Тащим контексты из @resource/ui-config, реализуем CoreProvider логику
- Настраиваем Module Federation (createFederationRuntime)
- Экспортируем CoreProvider в exposed директорию (@resource/core-mf/CoreProvider)
- Настраиваем Module Federation на фронте (для начала resource и burnee)
- Тащим конфиг из @resource/ui-app
- Экспортируем App и другие компоненты в exposed директорию
- Подключаем CoreProvider из core-mf, выпиливаем собственную реализацию
- Для подключения используем createFederationRuntime
-
- proxy в дев режиме
- Реализуем host приложение (оно же sandbox?)
- Тащим конфиг из @resource/ui-app
- Подключаем CoreProvider из core-mf
- Настраиваем Routes, Подключаем микрофронты
-
- proxy в дев режиме
- Реализуем @resource/federated-types для работы с типами микрофронтов
- Реализуем @resource/api-schemas для работы с taxios схемами бэкенд сервисов
- Реализуем @resource/fetchers для генерации taxios инстансов
- Реализуем @resource/generator библиотеку с шаблонами генерации микрофронтов (и не только)