如何为 Vue 项目配置 `Webpack` 联邦模块(`Module Federation`),实现微前端架构下的代码共享?

嘿,各位观众老爷们,早上好/下午好/晚上好!我是你们的老朋友,今天咱们来聊聊一个能让前端项目“合体”的黑科技——Vue 项目中的 Webpack 联邦模块(Module Federation)。别害怕,听起来高大上,其实就是让不同的 Vue 项目像搭积木一样,共享代码,实现微前端。

今天咱们就来一场“联邦模块一日游”,从入门到精通,手把手教你把这个“共享经济”在你的 Vue 项目里玩转起来!

一、 啥是联邦模块?为啥要用它?

首先,咱得明白啥是联邦模块。简单来说,它就是 Webpack 5 提供的模块共享方案。想象一下,你有一个“大哥”Vue 项目,里面有一些常用的组件、函数,你想让其他“小弟”Vue 项目也能用,以前你可能得复制粘贴,或者发个 npm 包。现在有了联邦模块,直接让“小弟”项目远程引用“大哥”项目的模块,是不是很方便?

为什么要用联邦模块?

  • 代码共享: 减少重复代码,提高开发效率。
  • 独立部署: 各个微前端应用可以独立部署、更新,互不影响。
  • 技术栈无关: 理论上,只要是 Webpack 项目,都可以使用联邦模块,不局限于 Vue。
  • 灵活组合: 可以根据业务需求,灵活组合不同的微前端应用。
  • 版本隔离: 不同的微前端应用可以使用不同版本的依赖,避免冲突。

二、 实战演练:搭建一个简单的联邦模块项目

咱们用两个简单的 Vue 项目来演示,一个叫 host(大哥),一个叫 remote(小弟)。host 项目提供一个 HelloWorld 组件,remote 项目引用这个组件。

1. 初始化项目

首先,创建两个 Vue 项目:

vue create host
vue create remote

一路回车,默认选项即可。

2. 配置 host 项目 (大哥)

  • 安装 Webpack: (如果项目已经安装了 webpack,则忽略此步骤)

    cd host
    npm install webpack webpack-cli --save-dev
  • 创建 webpack.config.js 文件:

    host 项目的根目录下,创建一个 webpack.config.js 文件,内容如下:

    const { ModuleFederationPlugin } = require('webpack').container;
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { VueLoaderPlugin } = require('vue-loader');
    const path = require('path');
    
    module.exports = {
      mode: 'development',
      devtool: 'source-map',
      entry: './src/main.js',
      output: {
        publicPath: 'http://localhost:8080/', // 重要!这里必须是 host 项目的访问地址
      },
      devServer: {
        port: 8080,
        hot: true,
        headers: {
          'Access-Control-Allow-Origin': '*', // 允许跨域
        },
      },
      module: {
        rules: [
          {
            test: /.vue$/,
            use: 'vue-loader',
          },
          {
            test: /.css$/,
            use: ['vue-style-loader', 'css-loader'],
          },
        ],
      },
      resolve: {
        extensions: ['.vue', '.js'],
      },
      plugins: [
        new ModuleFederationPlugin({
          name: 'host', // 必须唯一,作为模块的标识
          filename: 'remoteEntry.js', // 暴露的入口文件,供 remote 项目引用
          exposes: {
            './HelloWorld': './src/components/HelloWorld.vue', // 暴露的模块,key 是模块名,value 是模块路径
          },
          shared: ['vue'], // 共享的依赖,避免重复加载
        }),
        new HtmlWebpackPlugin({
          template: './public/index.html',
        }),
        new VueLoaderPlugin(),
      ],
    };

    代码解释:

    • ModuleFederationPlugin:联邦模块的核心插件。
      • name:模块的名称,必须唯一。
      • filename:暴露的入口文件,remote 项目会通过这个文件来访问 host 项目的模块。
      • exposes:暴露的模块,key 是模块名,value 是模块路径。
      • shared:共享的依赖,hostremote 项目都用到的依赖,可以避免重复加载,提高性能。
    • HtmlWebpackPlugin:生成 HTML 文件,用于开发环境。
    • VueLoaderPlugin:处理 Vue 组件。
    • publicPath:非常重要! 必须是 host 项目的访问地址,否则remote项目会加载不到资源。
  • 修改 package.json

    修改 package.json 文件,添加 webpackwebpack-dev-server 命令:

    "scripts": {
      "serve": "webpack serve --config webpack.config.js",
      "build": "webpack --mode production --config webpack.config.js",
      "lint": "vue-cli-service lint"
    },
  • 创建 HelloWorld.vue 组件:

    src/components 目录下创建一个 HelloWorld.vue 组件,内容随意,例如:

    <template>
      <div class="hello">
        <h1>Hello from Host!</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HelloWorld',
    };
    </script>
    
    <style scoped>
    .hello {
      color: green;
    }
    </style>
  • 启动 host 项目:

    npm run serve

    现在,host 项目应该在 http://localhost:8080 运行。

3. 配置 remote 项目 (小弟)

  • 安装 Webpack: (如果项目已经安装了 webpack,则忽略此步骤)

    cd remote
    npm install webpack webpack-cli --save-dev
  • 创建 webpack.config.js 文件:

    remote 项目的根目录下,创建一个 webpack.config.js 文件,内容如下:

    const { ModuleFederationPlugin } = require('webpack').container;
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { VueLoaderPlugin } = require('vue-loader');
    const path = require('path');
    
    module.exports = {
      mode: 'development',
      devtool: 'source-map',
      entry: './src/main.js',
      output: {
        publicPath: 'http://localhost:8081/', // 重要!这里必须是 remote 项目的访问地址
      },
      devServer: {
        port: 8081,
        hot: true,
      },
      module: {
        rules: [
          {
            test: /.vue$/,
            use: 'vue-loader',
          },
          {
            test: /.css$/,
            use: ['vue-style-loader', 'css-loader'],
          },
        ],
      },
      resolve: {
        extensions: ['.vue', '.js'],
      },
      plugins: [
        new ModuleFederationPlugin({
          name: 'remote', // 必须唯一,作为模块的标识
          remotes: {
            host: 'host@http://localhost:8080/remoteEntry.js', // 引用 host 项目的模块,key 是模块名,value 是模块地址
          },
          shared: ['vue'], // 共享的依赖,避免重复加载
        }),
        new HtmlWebpackPlugin({
          template: './public/index.html',
        }),
        new VueLoaderPlugin(),
      ],
    };

    代码解释:

    • ModuleFederationPlugin:联邦模块的核心插件。
      • name:模块的名称,必须唯一。
      • remotes:引用远程模块,key 是模块名,value 是模块地址,格式为 模块名@远程入口文件地址
      • shared:共享的依赖,hostremote 项目都用到的依赖,可以避免重复加载,提高性能。
    • HtmlWebpackPlugin:生成 HTML 文件,用于开发环境。
    • VueLoaderPlugin:处理 Vue 组件。
    • publicPath:非常重要! 必须是 remote 项目的访问地址。
  • 修改 package.json

    修改 package.json 文件,添加 webpackwebpack-dev-server 命令:

    "scripts": {
      "serve": "webpack serve --config webpack.config.js",
      "build": "webpack --mode production --config webpack.config.js",
      "lint": "vue-cli-service lint"
    },
  • remote 项目中使用 HelloWorld 组件:

    修改 remote 项目的 src/App.vue 文件,引入 HelloWorld 组件:

    <template>
      <div id="app">
        <HelloWorld />
      </div>
    </template>
    
    <script>
    import { defineAsyncComponent } from 'vue'
    
    export default {
      name: 'App',
      components: {
        HelloWorld: defineAsyncComponent(() => import('host/HelloWorld'))
      }
    }
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>

    代码解释:

    • import('host/HelloWorld'):从 host 项目引入 HelloWorld 组件。这里的 host 就是在 remote 项目的 webpack.config.js 文件中配置的 remotes 里的 key
    • defineAsyncComponent:使用异步组件引入,因为远程模块需要异步加载。
  • 启动 remote 项目:

    npm run serve

    现在,remote 项目应该在 http://localhost:8081 运行,并且显示 host 项目的 HelloWorld 组件!

三、 进阶技巧:共享更多东西

上面的例子只是共享了一个简单的组件,实际上,你可以共享任何 JavaScript 模块,例如:

  • 工具函数: 封装一些常用的工具函数,例如日期格式化、字符串处理等。
  • Vuex Store: 共享 Vuex Store,实现状态共享。
  • UI 组件库: 共享 UI 组件库,统一风格。

1. 共享工具函数

  • host 项目中创建工具函数:

    host 项目的 src 目录下创建一个 utils.js 文件,内容如下:

    export function formatDate(date) {
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const day = String(date.getDate()).padStart(2, '0');
      return `${year}-${month}-${day}`;
    }
  • host 项目的 webpack.config.js 文件中暴露工具函数:

    exposes: {
      './HelloWorld': './src/components/HelloWorld.vue',
      './utils': './src/utils.js', // 暴露工具函数
    },
  • remote 项目中使用工具函数:

    修改 remote 项目的 src/App.vue 文件,引入工具函数:

    <template>
      <div id="app">
        <HelloWorld />
        <p>Today is: {{ formattedDate }}</p>
      </div>
    </template>
    
    <script>
    import { defineAsyncComponent } from 'vue'
    import { formatDate } from 'host/utils'; // 引入工具函数
    
    export default {
      name: 'App',
      components: {
        HelloWorld: defineAsyncComponent(() => import('host/HelloWorld'))
      },
      data() {
        return {
          formattedDate: formatDate(new Date()), // 使用工具函数
        };
      },
    }
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>

    现在,remote 项目应该显示今天的日期,并且使用了 host 项目的 formatDate 函数。

2. 共享 Vuex Store

共享 Vuex Store 稍微复杂一些,需要一些额外的配置。

  • host 项目中创建 Vuex Store:

    host 项目的 src 目录下创建一个 store 目录,并在其中创建一个 index.js 文件,内容如下:

    import Vue from 'vue';
    import Vuex from 'vuex';
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
      state: {
        count: 0,
      },
      mutations: {
        increment(state) {
          state.count++;
        },
      },
      actions: {
        increment(context) {
          context.commit('increment');
        },
      },
      getters: {
        getCount(state) {
          return state.count;
        },
      },
    });
  • host 项目的 webpack.config.js 文件中暴露 Vuex Store:

    exposes: {
      './HelloWorld': './src/components/HelloWorld.vue',
      './utils': './src/utils.js',
      './store': './src/store/index.js', // 暴露 Vuex Store
    },
    shared: ['vue', 'vuex'], // 共享 vuex
  • remote 项目中使用 Vuex Store:

    • 安装 Vuex:

      cd remote
      npm install vuex --save
    • remote 项目的 src/main.js 文件中引入 Vuex Store:

      import Vue from 'vue'
      import App from './App.vue'
      import store from 'host/store'; // 引入 Vuex Store
      
      Vue.config.productionTip = false
      
      new Vue({
        store, // 注入 Vuex Store
        render: h => h(App),
      }).$mount('#app')
    • remote 项目的 src/App.vue 文件中使用 Vuex Store:

      <template>
        <div id="app">
          <HelloWorld />
          <p>Today is: {{ formattedDate }}</p>
          <p>Count: {{ count }}</p>
          <button @click="increment">Increment</button>
        </div>
      </template>
      
      <script>
      import { defineAsyncComponent } from 'vue'
      import { formatDate } from 'host/utils';
      
      export default {
        name: 'App',
        components: {
          HelloWorld: defineAsyncComponent(() => import('host/HelloWorld'))
        },
        data() {
          return {
            formattedDate: formatDate(new Date()),
          };
        },
        computed: {
          count() {
            return this.$store.getters.getCount; // 获取 count
          },
        },
        methods: {
          increment() {
            this.$store.dispatch('increment'); // 触发 increment action
          },
        },
      }
      </script>
      
      <style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      </style>

    现在,remote 项目可以使用 host 项目的 Vuex Store,并且可以修改 host 项目的状态。

四、 注意事项和常见问题

  • publicPath 的配置: hostremote 项目的 webpack.config.js 文件中的 output.publicPath 必须正确配置,否则会导致资源加载失败。hostpublicPath 必须指向 host 项目的访问地址,remote的publicPath必须指向 remote 项目的访问地址。
  • shared 的配置: shared 选项用于共享依赖,避免重复加载。需要注意的是,共享的依赖必须是版本兼容的,否则可能会导致运行时错误。 建议共享vue vue-router vuex 这些基础库。
  • 跨域问题: 如果 hostremote 项目运行在不同的域名下,需要配置跨域,可以在 host 项目的 webpack-dev-server 中配置 headers 选项,允许跨域访问。
  • 版本冲突: 不同的微前端应用可能使用不同版本的依赖,可能会导致冲突。可以使用 shared 选项的 version 属性来指定共享依赖的版本。
  • 异步加载: 远程模块需要异步加载,可以使用 import() 语法或 defineAsyncComponent 函数来实现。
  • 类型提示: 如果共享的是 TypeScript 模块,需要配置类型提示,可以使用 d.ts 文件来声明模块的类型。

五、 总结

联邦模块是一个强大的工具,可以帮助我们构建灵活、可扩展的微前端应用。掌握联邦模块,可以提高开发效率,减少重复代码,实现代码共享。

联邦模块配置参数表

参数名 类型 描述
name string 模块的名称,必须唯一。
filename string 暴露的入口文件名。
exposes object 暴露的模块,key 是模块名,value 是模块路径。
remotes object 引用远程模块,key 是模块名,value 是模块地址,格式为 模块名@远程入口文件地址
shared array 共享的依赖,避免重复加载。
shared[].import string (可选)如果设置为 false,则不会从远程位置获取共享模块。
shared[].requiredVersion string (可选)指定共享模块的所需版本。如果远程提供的模块版本与此不匹配,则会抛出错误。
shared[].version string (可选) 指定当前模块的版本。 如果其他模块需要共享该模块,并且指定了requiredVersion,那么会校验版本是否匹配。
shared[].singleton boolean (可选)设置为 true,表示只允许存在一个共享模块实例。
shared[].eager boolean (可选) 设置为 true,会立即加载共享模块。默认情况下,共享模块是按需加载的。

联邦模块常见问题表

问题 可能原因 解决方案
远程模块加载失败 publicPath 配置错误、跨域问题、网络问题。 检查 publicPath 是否正确配置,配置跨域,检查网络连接。
共享依赖版本冲突 不同的微前端应用使用了不同版本的依赖。 使用 shared 选项的 version 属性来指定共享依赖的版本,尽量保持版本一致。
远程模块类型提示缺失 共享的是 TypeScript 模块,但是没有配置类型提示。 使用 d.ts 文件来声明模块的类型。
远程模块代码修改后,本地项目没有更新 浏览器缓存、Webpack 缓存。 清除浏览器缓存、Webpack 缓存,重新启动项目。
多个模块都引用同一个共享模块,导致重复加载 shared 配置不正确。 确保 shared 配置正确,并且共享模块的版本一致。
异步加载远程模块报错 异步加载方式不正确。 使用 import() 语法或 defineAsyncComponent 函数来实现异步加载。
加载远程模块时出现 CORS 错误 host 项目没有允许 remote 项目的跨域请求。 在 host 项目的 webpack-dev-server 配置中,添加 headers: { 'Access-Control-Allow-Origin': '*' },或者根据实际情况配置允许的域名。

今天的“联邦模块一日游”就到这里,希望大家有所收获。记住,实践是检验真理的唯一标准,赶紧动手试试吧! 如果还有其他问题,欢迎在评论区留言,我会尽力解答。 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注