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项目中被展示了。

相关推荐
大虾写代码3 小时前
nvm和nrm的详细安装配置,从卸载nodejs到安装NVM管理nodejs版本,以及安装nrm管理npm版本
前端·npm·node.js·nvm·nrm
星哥说事3 小时前
下一代开源 RAG 引擎,让你的 AI 检索与推理能力直接起飞
前端
....4923 小时前
Vue3 与 AntV X6 节点传参、自动布局及边颜色控制教程
前端·javascript·vue.js
今禾3 小时前
深入浅出:ES6 Modules 与 CommonJS 的爱恨情仇
前端·javascript·面试
前端小白19953 小时前
面试取经:Vue篇-Vue2响应式原理
前端·vue.js·面试
子兮曰3 小时前
⭐告别any类型!TypeScript从零到精通的20个实战技巧,让你的代码质量提升300%
前端·javascript·typescript
前端AK君3 小时前
如何开发一个SDK插件
前端
小满xmlc3 小时前
WeaveFox AI 重新定义前端开发
前端
日月晨曦3 小时前
大文件上传实战指南:让「巨无霸」文件也能「坐高铁」
前端