Webpack模块联邦 - vue项目嵌套react项目部分功能实践

背景

我的一个react项目中有许多实用的组件,我的vue项目中想使用其功能,但是我又不想复制所有代码,太麻烦了,该如何是好呢?- 模块联邦立刻回答道,用我就行了。

GIthub地址

github.com/abcdftg/web...

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.js
  • shared: 配置共享依赖,确保版本一致性
  • 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项目中被展示了。

相关推荐
彼日花7 分钟前
前端新人30天:从手足无措到融入团队
前端·程序员
搞科研的小刘选手10 分钟前
【学术会议合集】2025-2026年地球科学/遥感方向会议征稿信息
大数据·前端·人工智能·自动化·制造·地球科学·遥感测绘
蓝莓味的口香糖29 分钟前
【CSS】flex布局
前端·css
彩旗工作室1 小时前
用 Supabase 打造统一认证中心:为多应用提供单点登录(SSO)
服务器·前端·数据库
EveryPossible1 小时前
第一版代码
前端·javascript·css
ObjectX前端实验室2 小时前
【图形编辑器架构】渲染层篇 — 从 React 到 Canvas 的声明式渲染实现
前端·计算机图形学·图形学
一直在学习的小白~2 小时前
小程序开发:开启定制化custom-tab-bar但不生效问题,以及使用NutUI-React Taro的安装和使用
webpack·小程序·webapp
java水泥工2 小时前
基于Echarts+HTML5可视化数据大屏展示-智慧消防大屏
前端·echarts·html5
杨超越luckly2 小时前
HTML应用指南:利用POST请求获取全国索尼体验型零售店位置信息
前端·arcgis·html·数据可视化·门店数据
ObjectX前端实验室2 小时前
【图形编辑器架构】节点树篇 — 从零构建你的编辑器数据中枢
前端·计算机图形学·图形学