背景
我的一个react项目中有许多实用的组件,我的vue项目中想使用其功能,但是我又不想复制所有代码,太麻烦了,该如何是好呢?- 模块联邦立刻回答道,用我就行了。
GIthub地址
readme.md中包括启动说明
基本原理

基本配置语法
js
1. const { ModuleFederationPlugin } = require('webpack').container; // 引入webpack模块联邦插件
2. 在plugins中配置ModuleFederationPlugin
Vue配置
module.exports = {
mode: 'development',
entry: './src/main.js',
devServer: {
port: 3002,
hot: true,
open: true,
historyApiFallback: true,
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new VueLoaderPlugin(),
new ModuleFederationPlugin({
name: 'vueHost',
filename: 'remoteEntry.js',
remotes: {
reactRemote: 'reactRemote@http://localhost:3001/remoteEntry.js', // 引入react暴露的远程文件
},
shared: {
vue: {
singleton: true, // 确保整个应用中只有一个版本的该依赖, (内存中只有一个vue库的实例)
eager: true, // 立即加载模块,而不是按需加载
requiredVersion: '^3.3.4', // 确保使用正确的版本
},
react: {
singleton: true,
requiredVersion: '^18.2.0',
import: false, // 不主动导入React,由Remote提供
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
import: false, // 不主动导入ReactDOM,由Remote提供
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
js
React配置
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
devServer: {
port: 3001,
hot: true,
open: true,
historyApiFallback: true,
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'reactRemote',
filename: 'remoteEntry.js',
exposes: { // 暴露给vue的文件
'./ReactButton': './src/components/ReactButton', // 暴露的组件
'./ReactCard': './src/components/ReactCard',// 暴露的组件
'./react': 'react', // 暴露的第三方库(vue不需要额外npm下载react的 原因)
'./react-dom/client': 'react-dom/client', // 暴露的第三方库
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
如何实现
第一步:创建Vue Host应用
1.1 创建项目结构
bash
mkdir vue-host
cd vue-host
1.2 配置package.json
json
{
"name": "vue-host",
"version": "1.0.0",
"description": "Vue Host Application for Module Federation",
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production",
"serve": "webpack serve --mode development --port 3002"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"html-webpack-plugin": "^5.5.3",
"style-loader": "^3.3.3",
"vue-loader": "^17.2.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"@vue/compiler-sfc": "^3.3.4"
}
}
关键点:
- 不安装React依赖:Vue应用通过Module Federation从React Remote应用获取React库
- 配置了开发和生产环境的构建脚本
- 实现了真正的依赖共享,减少包体积
1.3 配置Webpack (Host)
javascript
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/main.js',
devServer: {
port: 3002,
hot: true,
open: true,
historyApiFallback: true,
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new VueLoaderPlugin(),
new ModuleFederationPlugin({
name: 'vueHost',
filename: 'remoteEntry.js',
remotes: {
reactRemote: 'reactRemote@http://localhost:3001/remoteEntry.js',
},
shared: {
vue: {
singleton: true,
eager: true,
requiredVersion: '^3.3.4',
},
react: {
singleton: true,
requiredVersion: '^18.2.0',
import: false, // 不主动导入React,由Remote提供
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
import: false, // 不主动导入ReactDOM,由Remote提供
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
关键配置说明:
name: 'vueHost'
: 定义Host应用的名称remotes
: 配置远程应用,指向React应用的remoteEntry.jsshared
: 配置共享依赖,确保版本一致性import: false
: 不主动导入React依赖,由Remote应用提供
1.4 创建入口文件 (main.js)
javascript
import('./bootstrap');
为什么使用动态导入:
- 解决"Shared module is not available for eager consumption"错误
- 确保共享模块在正确的时机加载
1.5 创建启动文件 (bootstrap.js)
javascript
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
1.6 创建主应用组件 (App.vue)
vue
<template>
<div id="app">
<nav class="navbar">
<h1>Vue Host Application</h1>
<div class="nav-links">
<router-link to="/" class="nav-link">首页</router-link>
<router-link to="/react-component" class="nav-link">React组件</router-link>
</div>
</nav>
<main class="main-content">
<router-view />
</main>
</div>
</template>
<script>
export default {
name: 'App'
};
</script>
1.7 配置路由 (router/index.js)
javascript
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import ReactComponent from '../views/ReactComponent.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/react-component',
name: 'ReactComponent',
component: ReactComponent
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
第二步:创建React Remote应用
2.1 创建项目结构
bash
mkdir react-remote
cd react-remote
2.2 配置package.json
json
{
"name": "react-remote",
"version": "1.0.0",
"description": "React Remote Application for Module Federation",
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production",
"serve": "webpack serve --mode development --port 3001"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"html-webpack-plugin": "^5.5.3",
"style-loader": "^3.3.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
2.3 配置Webpack (Remote)
javascript
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
devServer: {
port: 3001,
hot: true,
open: true,
historyApiFallback: true,
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'reactRemote',
filename: 'remoteEntry.js',
exposes: {
'./ReactButton': './src/components/ReactButton',
'./ReactCard': './src/components/ReactCard',
'./react': 'react',
'./react-dom/client': 'react-dom/client',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
关键配置说明:
name: 'reactRemote'
: 定义Remote应用的名称exposes
: 暴露给其他应用使用的组件和库filename: 'remoteEntry.js'
: 远程入口文件名- 暴露React和ReactDOM:让其他应用可以使用React库
2.4 创建入口文件 (index.js)
javascript
import('./bootstrap');
2.5 创建启动文件 (bootstrap.js)
javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
2.6 创建React组件
ReactButton组件:
javascript
import React from 'react';
import './ReactButton.css';
const ReactButton = ({
text = 'Button',
variant = 'primary',
size = 'medium',
onClick,
disabled = false,
...props
}) => {
const buttonClass = `react-button react-button--${variant} react-button--${size}`;
return (
<button
className={buttonClass}
onClick={onClick}
disabled={disabled}
{...props}
>
<span className="react-button__text">{text}</span>
<span className="react-button__ripple"></span>
</button>
);
};
export default ReactButton;
第三步:创建React组件包装器
3.1 创建ReactWrapper组件 (vue-host/src/components/ReactWrapper.vue)
vue
<template>
<div ref="reactContainer"></div>
</template>
<script>
export default {
name: 'ReactWrapper',
props: {
component: {
type: Function,
required: true
},
props: {
type: Object,
default: () => ({})
}
},
async mounted() {
await this.renderReactComponent();
},
async updated() {
await this.renderReactComponent();
},
beforeUnmount() {
if (this.reactRoot) {
this.reactRoot.unmount();
}
},
methods: {
async renderReactComponent() {
if (!this.component || !this.$refs.reactContainer) return;
// 清理之前的渲染
if (this.reactRoot) {
this.reactRoot.unmount();
}
try {
// 从React Remote应用获取React和ReactDOM
// 这样Vue应用就不需要安装React依赖了
const [React, ReactDOM] = await Promise.all([
import('reactRemote/react'), // 从Remote获取React
import('reactRemote/react-dom/client') // 从Remote获取ReactDOM
]);
// 创建React根节点
this.reactRoot = ReactDOM.default.createRoot(this.$refs.reactContainer);
// 渲染React组件
this.reactRoot.render(React.default.createElement(this.component, this.props));
} catch (error) {
console.error('Failed to render React component:', error);
}
}
}
};
</script>
包装器的作用:
- 封装React组件的渲染逻辑
- 处理组件的生命周期
- 提供简洁的Vue组件接口
- 从Remote应用获取React库:Vue应用不需要安装React依赖
3.2 创建React组件展示页面 (vue-host/src/views/ReactComponent.vue)
vue
<template>
<div class="react-component">
<div class="header">
<h2>React远程组件</h2>
<p>这个组件来自React子应用,通过Module Federation动态加载</p>
</div>
<div class="component-container">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>正在加载React组件...</p>
</div>
<div v-else-if="error" class="error">
<h3>加载失败</h3>
<p>{{ error }}</p>
<button @click="loadComponent" class="retry-button">重试</button>
</div>
<div v-else class="react-component-wrapper">
<!-- 使用React包装器组件 -->
<ReactWrapper
:component="ReactButton"
:props="{
text: '来自Vue Host的按钮',
variant: 'primary',
onClick: handleReactClick
}"
/>
<ReactWrapper
:component="ReactButton"
:props="{
text: '另一个按钮',
variant: 'secondary',
onClick: handleReactClick2
}"
/>
</div>
</div>
</div>
</template>
<script>
import ReactWrapper from '../components/ReactWrapper.vue';
export default {
name: 'ReactComponent',
components: {
ReactWrapper
},
data() {
return {
loading: false,
error: null,
ReactButton: null
};
},
async mounted() {
await this.loadComponent();
},
methods: {
async loadComponent() {
this.loading = true;
this.error = null;
try {
// 动态导入React远程组件
const module = await import('reactRemote/ReactButton');
this.ReactButton = module.default;
} catch (err) {
this.error = `加载失败: ${err.message}`;
console.error('Failed to load React component:', err);
} finally {
this.loading = false;
}
},
handleReactClick() {
alert('React组件在Vue中被点击了!');
},
handleReactClick2() {
console.log('第二个React按钮被点击了');
}
}
};
</script>
第四步:配置Babel
4.1 Vue应用Babel配置 (.babelrc)
json
{
"presets": ["@babel/preset-env"]
}
4.2 React应用Babel配置 (.babelrc)
json
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
第五步:创建根目录配置
5.1 根目录package.json
json
{
"name": "webpack-module-federation-demo",
"version": "1.0.0",
"description": "Module Federation Demo with Vue Host and React Remote",
"scripts": {
"install:all": "npm install && cd vue-host && npm install && cd ../react-remote && npm install",
"dev:vue": "cd vue-host && npm run dev",
"dev:react": "cd react-remote && npm run dev",
"dev": "concurrently \"npm run dev:react\" \"npm run dev:vue\"",
"build:vue": "cd vue-host && npm run build",
"build:react": "cd react-remote && npm run build",
"build": "npm run build:react && npm run build:vue"
},
"devDependencies": {
"concurrently": "^8.2.0"
}
}
效果:

我们可以成功看到react暴露出的组件在vue项目中被展示了。