みなさん、こんにちは。私の名前はIvanで、フロントエンド開発者です。
マイクロフロントについての私の解説では、3つものいいねがあったので、マイクロフロントの導入の結果として私たちのストリームが満たされ、満たされるすべてのビッグウィッグを説明する記事を書くことにしました。
Habr(@ artemu78、@ dfuse、@ Katsuba)の人たちがすでにモジュールフェデレーションについて書いているという事実から始めましょう。したがって、私の記事はユニークで画期的なものではありません。むしろ、これらは、このテクノロジーを使用しようとしている人に役立つバンプ、松葉杖、自転車です。
原因
, , - , , - . , 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>. , , .
import React from 'react';
// ...
import Todo from 'widgets/Todo';
// ...
const queryClient = new QueryClient();
export const App = () => {
// ...
return (
<QueryClientProvider client={queryClient} contextSharing>
<Router>
<Layout sidebarContent={<Navigation />}>
<Switch>
{/* ... */}
<Route exact path='/'>
<Todo />
</Route>
{/* ... */}
</Switch>
</Layout>
</Router>
</QueryClientProvider>
);
};
, , , , , 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, . .
, , . , , . , , .
, , , . , shell- , Module Federation . , , , .
, , , , , , .
React-
react-router, , useLocation, , .
Apollo, , ApolloClient shell-. useQuery, useLocation.
, , npm- , shell-, .
UI- shell-
, , shell- . , :
UI- npm- shared-
"" ModuleFederationPlugin
, , , . Module Federation , npm.
TypeScript, , Module Federation , . - , . , .d.ts , - .
emp-tune-dts-plugin, , .
, Webpack 5 Module Federation , , - . , , , .
, , . - , .
, , , , , Module Federation.
Webpack5ドックのモジュールフェデレーションドキュメント