Вадим Малютин
телеграм: t.me/mfpjke
почта: mltn.dev@yandex.ru
сайт: mltn.dev
Я Senior Software Engineer в Grid Dynamics
Со-основатель первой виртуальной примерки одежды в вебе clo-z.app
Со-основатель IT-сообщества VectorWay
Со-автор приложения для тренировки глаз, написанный на Flutter
Сертифицированный разработчик AWS – 853/1000 score
Previous experience:
Yandex
Bekitzur
До доклада
После доклада
О проекте
Следует
Объявляем remote
new ModuleFederationPlugin({
name: "watch",
filename: "remoteEntry.js",
library: { type: "var", name: "watch" },
exposes: {
"./page": "./src/components/page",
}
})
Показываем откуда раздаем статику
output: {
path: path.resolve(process.cwd(), "dist"),
filename: "[name].[contenthash].js",
publicPath: "http://localhost:3003/",
}
Объявляем хост
new ModuleFederationPlugin({
name: "app_host",
remotes: {
watch: "watch",
}
})
Добавляем скрипты
new HtmlWebpackTagsPlugin({
tags: ["http://localhost:3003/remoteEntry.js"],
// нужно, чтобы скрипты загрузились до app_host
append: false,
publicPath: false,
}),
const WatchPage = lazy(() => import("watch/page"));
...
render() {
return (
<Switch>
<Route path="/watch">
<Suspense fallback="Loading...">
<WatchPage />
</Suspense>
</Route>
</Switch>
)
}
Сам добавлю скрипты
new HtmlWebpackTagsPlugin({
tags: ["http://localhost:3003/remoteEntry.js"],
// нужно, чтобы скрипты загрузились до app_host
append: false,
publicPath: false,
}),
new ModuleFederationPlugin({
name: "app_host",
remotes: {
watch: "watch",
}
})
Module federation добавляет скрипты
new ModuleFederationPlugin({
name: "app_host",
remotes: {
watch: "watch@http://localhost:3003/remoteEntry.js",
}
})
<head>
<script
src="http://localhost:3003/remoteEntry.js"
defer
></script>
<script
src="http://localhost:3001/app.ed5a105fae04a74d5e4b.js"
defer
></script>
</head>
Какой способ использовать?
app-host/webpack.config.ts
new HtmlWebpackTagsPlugin({
tags: ["http://localhost:3003/remoteEntry.js", ...],
// нужно, чтобы скрипты загрузились до app_host
append: false,
publicPath: false,
}),
Какой способ использовать?
react-dom.production.min.js:216 Error: Container missing
while loading "./page" from 6448
at n (remotes loading:39)
at remotes loading:60
at u (remotes loading:54)
at remotes loading:68
at Array.forEach ()
at Object.f.f.remotes (remotes loading:31)
at ensure chunk:6
at Array.reduce ()
at Function.f.e (ensure chunk:5)
at index.tsx:8
optimization: {
splitChunks: {
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: "react",
chunks: "all",
},
commons: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
priority: -20,
},
},
},
}
watch/webpack.config.ts
output: {
path: path.resolve(process.cwd(), "dist"),
filename: "[name].[contenthash].js",
publicPath: "http://localhost:3002/",
},
app-host/webpack.config.ts
plugins: [
new ModuleFederationPlugin({
name: "app_host",
filename: "remoteEntry.js",
library: { type: "var", name: "app_host" },
remotes: {
watch: "watch@http://localhost:3003/remoteEntry.js",
}
})
]
Наименование в remotes
remotes: {
"mf-app": "mf-app@http://localhost:3003/remoteEntry.js",
"mf-app": "mf_app@http://localhost:3003/remoteEntry.js",
"mf_app": "mf_app@http://localhost:3003/remoteEntry.js",
"mfApp": "mfApp@@http://localhost:3003/remoteEntry.js"
}
watch/webpack.config.ts
new ModuleFederationPlugin({
name: "watch",
filename: "remoteEntry.js",
library: { type: "var", name: "watch" },
exposes: {
"./page": "./src/components/page",
}
})
watch/webpack.config.ts
output: {
path: path.resolve(process.cwd(), "dist"),
filename: "[name].[contenthash].js",
publicPath: "http://localhost:3002/",
},
app-host/webpack.config.ts
new ModuleFederationPlugin({
name: "app_host",
remotes: {
watch: "watch@http://localhost:3003/remoteEntry.js",
}
})
app-host/app.tsx
import React, { lazy, Suspense } from 'react'
const WatchPage = lazy(() => import('watch/page'))
...
render() {
return (
<Suspense fallback="Loading...">
<WatchPage />
</Suspense>
)
}
Обертка для React.lazy
/**
* @param factory – () => import('microfront/component')
*/
export function preloadLazy(factory) {
const Component = lazy(factory)
Component.preload = factory
return Component
}
Используем обертку
const WatchPage = preloadLazy(() => import('watch/page'))
const App = () => {
const [animate, setAnimate] = useState(false)
useLayoutEffect(() => {
WatchPage.preload().then(() => setAnimate(true))
}, [])
return (
<Suspense fallback="...">
<Animation in={animate}>
<WatchPage />
</Animation>
</Suspense>
)
}
new ModuleFederationPlugin({
...
shared: {
react: {
eager: true, # используем общий модуль при начальной загрузке
singleton: true, # используем только один экземпляр
requiredVersion: dependencies.react,
}
}
})
Создаем контекст shared/contexts/index.ts
import { createContext, useContext } from 'react'
export const UserContext = createContext({ id: '', name: '' })
export const useUser = () => useContext(UserContext)
Добавляем в app-host/webpack.config.ts
shared: {
'@contexts': {
singleton: true,
import: path.resolve('./shared/contexts')
}
}
Добавляем React дерево
import { UserContext } from '@contexts'
...
<UserContext.Provider value={{ id: '1234', name: 'Viktor' }}>
<WatchPage />
</UserContext.Provider>
Используем в микрофронтенде
import { useUser } from '@contexts'
export const WatchPage = () => {
const user = useUser()
return <div>{user.name}</div>
}
У нас свой cloud storage
А у вас?
/bucket
├── app-host
│ ├── 888.330d58fad93b1eb0dfa5.js
│ ├── app.d4c4be8e2eb88907410a.js
│ ├── index.html
│ └── react.39acefad56af0f11d471.js
└── watch
├── 888.3b49de53a0194486d9db.js
├── 938.85ead56334c64eec9261.js
├── app.c47cc95c56185a4b15b4.js
├── index.html
├── react.0ee026e74f05c2e958a1.js
└── remoteEntry.js
/bucket
├── app-host
│ ├── 2.0.0
│ │ ├── index.html
│ │ ├── 888.3b49de53a0194486d9db.js
│ └── 2.0.1
│ ├── index.html
│ ├── 938.85ead56334c64eec9261.js
└── watch
├── 2.0.0
│ ├── index.html
│ ├── 938.85ead56334c64eec9261.js
└── 2.0.1
├── index.html
├── 938.85ead56334c64eec9261.js
└── remoteEntry.js
С pre-sign url
https://hub.docker.com/r/openresty/openresty/
API
server {
listen 3001;
...
location = /graphql {
proxy_pass 'http://${BACKEND_URL}:8000/grahpql';
}
}
Bundles
server {
listen 3001;
...
location / {
try_url $uri @proxy;
}
}
server {
listen 3002;
...
}
server {
listen 3003;
...
}
Bundles
location @proxy {
set $bucket ${BUCKET};
set $key $1;
set $signature '';
set $access_key ${ACCESS_KEY};
set $secret_key ${SECRET_KEY};
set_by_lua 'return ngx.cookie_time(ngx.time())';
set $string_to_sign
'$request_method\n\n\n\nx-amz-date:$now\n/$bucket/$key';
set_hmac_sha1 $signature $secret_key $string_to_sign;
set_encode_base64 $access_key $signature;
error_page 404 = @index_proxy;
}
Routing
location @index_proxy {
set $bucket ${BUCKET};
set $key 'app-host/index.html';
set $signature '';
set $access_key ${ACCESS_KEY};
set $secret_key ${SECRET_KEY};
set_by_lua 'return ngx.cookie_time(ngx.time())';
set $string_to_sign
'$request_method\n\n\n\nx-amz-date:$now\n/$bucket/$key';
set_hmac_sha1 $signature $secret_key $string_to_sign;
set_encode_base64 $access_key $signature;
}
На входе nginx.conf.template
server {
listen 3001;
...
location = /graphql {
proxy_pass 'http://${BACKEND_URL}:8000/grahpql';
}
}
envsubtr '${BACKEND_URL}' < nginx.conf.template \
> /etc/nginx/conf.d/default.conf
На выходе /etc/nginx/conf.d/default.conf
server {
listen 3001;
...
location = /graphql {
proxy_pass 'http://backend-app:8000/grahpql';
}
}
На выходе /etc/nginx/conf.d/default.conf
server {
listen 3001;
...
location = /graphql {
proxy_pass 'http://158.134.171.13:8000/grahpql';
}
}
server {
listen 3001;
...
location / {
try_url $uri @proxy;
}
}
server {
listen 3002;
...
}
server {
listen 3003;
...
}
Новый nginx.conf.template
server {
listen 3001;
...
location ~ ^/bundles/(.*) {
try_url $uri @proxy;
}
}
watch/webpack.config.prod.ts
output: {
path: path.resolve(process.cwd(), "dist"),
filename: "[name].[contenthash].js",
publicPath: "http://your-domain.com/bundles/watch"
}
app-host/webpack.config.prod.ts
remotes: {
watch:
"watch@https://domain.com/bundles/watch/remoteEntry.js"
}
/bucket
├── app-host
│ ├── 2.0.0
│ │ ├── index.html
│ │ ├── 888.3b49de53a0194486d9db.js
│ └── 2.0.1
│ ├── index.html
│ ├── 938.85ead56334c64eec9261.js
└── watch
├── 2.0.0
│ ├── index.html
│ ├── 938.85ead56334c64eec9261.js
└── 2.0.1
├── index.html
├── 938.85ead56334c64eec9261.js
└── remoteEntry.js
remotes: {
watch: `promise new Promise(resolve => {
...
resolve({
get: (request) => window.watch.get(request),
init: (arg) => {
try {
return window.watch.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
})
})
},
Вместо ... могло быть
fetch('http://version-api.com?mf=watch')
.then(watchVersion => {
const url = "http://domain.com/bundles/"
+ watchVersion + "/remoteEntry.js";
const script = document.createElement('script');
script.src = url;
script.defer = true;
script.onload = () => {
resolve(...)
}
document.head.appendChild(script);
});
Вадим Малютин
телеграм: t.me/mfpjke
чатик: t.me/microfrontends
почта: mltn.dev@yandex.ru
сайт: mltn.dev