React 中的 远程组件 是指从远程源(如 CDN 或服务器)加载和执行的组件,而不是与你的应用程序捆绑在一起。这样可以:
- 运行时组件的动态加载
- 跨不同应用程序共享组件
- 更新组件,无需重新构建主应用程序
- 微前端架构实现
如何实现远程组件
在 React 中实现远程组件的方法有多种:
1.模块联合 (Webpack 5)
这是主流用法
使用者/消费者(host主应用程序):
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack');
module.exports = {
// ...other config
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
远程应用程序(provider提供者)
// webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/webpack');
module.exports = {
// ...other config
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
在主应用程序中使用:
import React, { Suspense } from 'react';
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
function App() {
return (
<div>
<h1>Host Application</h1>
<Suspense fallback="Loading Remote Component...">
<RemoteButton />
</Suspense>
</div>
);
}
2.使用 CDN 动态导入
从 CDN 加载组件:
import React, { useState, useEffect } from 'react';
function RemoteComponent({ url }) {
const [Component, setComponent] = useState(null);
useEffect(() => {
const loadComponent = async () => {
try {
const module = await import(/* webpackIgnore: true */ url);
setComponent(() => module.default);
} catch (error) {
console.error('Failed to load remote component:', error);
}
};
loadComponent();
}, [url]);
if (!Component) {
return <div>Loading...</div>;
}
return <Component />;
}
3.自定义远程组件加载器
创建更复杂的远程组件系统:
// RemoteComponentLoader.js
import React, { useState, useEffect, useRef } from 'react';
class RemoteComponentLoader {
constructor() {
this.cache = new Map();
}
async loadComponent(url, scope, module) {
const key = `${url}-${scope}-${module}`;
if (this.cache.has(key)) {
return this.cache.get(key);
}
try {
// Load the remote entry
await this.loadScript(url);
// Get the module from the remote container
const factory = await window[scope].get(module);
const Module = factory();
this.cache.set(key, Module);
return Module;
} catch (error) {
console.error('Error loading remote component:', error);
throw error;
}
}
loadScript(url) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${url}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
}
const loader = new RemoteComponentLoader();
export function RemoteComponent({ url, scope, module, fallback = 'Loading...' }) {
const [Component, setComponent] = useState(null);
const [error, setError] = useState(null);
const mountedRef = useRef(true);
useEffect(() => {
const loadRemoteComponent = async () => {
try {
const LoadedComponent = await loader.loadComponent(url, scope, module);
if (mountedRef.current) {
setComponent(() => LoadedComponent);
}
} catch (err) {
if (mountedRef.current) {
setError(err);
}
}
};
loadRemoteComponent();
return () => {
mountedRef.current = false;
};
}, [url, scope, module]);
if (error) {
return <div>Error loading component: {error.message}</div>;
}
if (!Component) {
return <div>{fallback}</div>;
}
return <Component />;
}
用法
function App() {
return (
<div>
<h1>My App</h1>
<RemoteComponent
url="http://localhost:3001/remoteEntry.js"
scope="remoteApp"
module="./Button"
fallback="Loading button..."
/>
</div>
);
}
4. 使用systemJs
import React, { useState, useEffect } from 'react';
import { importMap } from './importMap';
function SystemJSRemoteComponent({ componentName }) {
const [Component, setComponent] = useState(null);
useEffect(() => {
const loadComponent = async () => {
try {
// Configure SystemJS with import map
System.addImportMap(importMap);
// Import the remote component
const module = await System.import(componentName);
setComponent(() => module.default || module);
} catch (error) {
console.error('Failed to load remote component:', error);
}
};
loadComponent();
}, [componentName]);
if (!Component) {
return <div>Loading remote component...</div>;
}
return <Component />;
}
// importMap.js
export const importMap = {
imports: {
// npm packages
"react": "https://cdn.skypack.dev/react",
"react-dom": "https://cdn.skypack.dev/react-dom",
"lodash": "https://cdn.skypack.dev/lodash",
// Remote components
"my-button": "https://my-cdn.com/components/button.js",
"my-header": "https://my-cdn.com/components/header.js",
"shared-ui-library": "https://shared-components.company.com/library.js",
// Scoped packages
"@material-ui/core": "https://cdn.skypack.dev/@material-ui/core",
"@myorg/shared-components": "https://myorg-cdn.com/shared-components/index.js"
},
scopes: {
// Scoped mappings for version management
"https://my-cdn.com/components/": {
"utils": "https://my-cdn.com/utils/v2.0.0/index.js"
}
}
};
注意点
- 安全性:始终验证和清理远程组件以防止 XSS 攻击
- 版本控制:为远程组件实施适当的版本控制策略
- 错误处理:优雅地处理网络故障和组件加载错误