基于 Webpack Module Federation 的 Vue 微前端实践

在大型前端项目中,团队协作效率低、技术栈绑定、独立部署困难等问题往往成为瓶颈。Webpack 5 推出的 Module Federation(模块联邦) 为微前端架构提供了创新解决方案

一、核心概念与架构设计

在动手前,先明确 Module Federation 微前端的三个核心角色,它们的协作关系决定了整体架构的灵活性:

角色 核心职责 示例定位
宿主应用(Host) 1. 作为 "主框架",提供统一入口和导航 2. 管理所有微应用的加载、卸载 3. 共享公共依赖(如 Vue、VueRouter) 企业中台的 "壳应用"
远程应用(Remote) 1. 独立开发、构建、部署的微应用 2. 暴露组件 / 页面供宿主加载 3. 可复用宿主的共享依赖 中台内的 "用户管理""订单管理" 模块
共享依赖(Shared) 宿主与远程应用共同依赖的库(如 Vue、axios),只需加载一次,避免重复打包 vue、vue-router、lodash

以下有一个极简但完整的微前端场景:

  • 宿主应用(端口 8080):提供导航栏和路由容器,加载远程应用
  • 远程应用 1:用户模块(端口 8081):暴露 "用户列表" 组件
  • 远程应用 2:订单模块(端口 8082):暴露 "订单列表" 组件
  • 三者共享 vuevue-router,避免重复加载

1. 第一步:创建项目骨架

sql 复制代码
# 1. 创建宿主应用(host-app)
vue create host-app
cd host-app
vue add router  # 添加 Vue Router
cd ..

# 2. 创建远程应用1:用户模块(user-app)
vue create user-app
cd user-app
vue add router
cd ..

# 3. 创建远程应用2:订单模块(order-app)
vue create order-app
cd order-app
vue add router
cd ..

2. 第二步:配置远程应用(以 user-app 为例)

远程应用的核心任务是:声明 "暴露哪些模块" 和 "共享哪些依赖" ,让宿主应用能找到并加载它。

(1)修改 vue.config.js

Vue CLI 项目通过 configureWebpack 配置 ModuleFederationPlugin(Webpack 5 内置插件,无需额外安装):

javascript

运行

javascript 复制代码
// user-app/vue.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  // 关键:远程应用的公共路径(必须是可访问的地址,开发环境用本地端口)
  publicPath: process.env.NODE_ENV === 'production' 
    ? 'https://your-cdn.com/user-app/'  // 生产环境 CDN 路径
    : 'http://localhost:8081/',         // 开发环境本地地址

  // 开发服务器配置(端口 + 跨域允许)
  devServer: {
    port: 8081,  // 远程应用端口(需与 publicPath 一致)
    headers: {
      // 允许宿主应用跨域加载(开发环境必备)
      'Access-Control-Allow-Origin': '*'
    }
  },

  // Webpack 插件配置
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        // 1. name:远程应用的唯一标识(宿主加载时会用到)
        name: 'userApp',

        // 2. filename:远程应用的"入口文件"(宿主通过该文件获取模块)
        // 约定俗成命名为 remoteEntry.js,便于识别
        filename: 'remoteEntry.js',

        // 3. exposes:声明要暴露的模块(键:宿主访问路径,值:本地文件路径)
        exposes: {
          // 宿主可通过 "userApp/UserList" 加载该组件
          './UserList': './src/components/UserList.vue',
          // 若需暴露页面级组件,可添加:'./UserDetail': './src/views/UserDetail.vue'
        },

        // 4. shared:声明共享依赖(避免重复加载)
        shared: {
          // 共享 Vue:singleton: true 确保全局只有一个 Vue 实例(避免 hooks 报错)
          vue: {
            singleton: true,
            // 约束版本:使用项目 package.json 中的 Vue 版本
            requiredVersion: require('./package.json').dependencies.vue
          },
          // 共享 Vue Router(同 Vue 逻辑)
          'vue-router': {
            singleton: true,
            requiredVersion: require('./package.json').dependencies['vue-router']
          }
        }
      })
    ]
  }
};

(2)编写暴露的组件(UserList.vue)

创建一个简单的 "用户列表" 组件,作为远程应用暴露的核心内容:

vue

xml 复制代码
<!-- user-app/src/components/UserList.vue -->
<template>
  <div class="user-list-container">
    <h3 class="module-title">🔍 用户管理模块(来自远程应用)</h3>
    <div class="user-table">
      <div class="table-header">
        <span>ID</span>
        <span>用户名</span>
        <span>角色</span>
      </div>
      <div class="table-body">
        <div class="table-row" v-for="user in userList" :key="user.id">
          <span>{{ user.id }}</span>
          <span>{{ user.name }}</span>
          <span>{{ user.role }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
// 模拟用户数据
const userList = [
  { id: 1, name: '张三', role: '系统管理员' },
  { id: 2, name: '李四', role: '运营专员' },
  { id: 3, name: '王五', role: '普通用户' }
];
</script>

<style scoped>
.user-list-container {
  padding: 24px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  margin: 0 24px;
}

.module-title {
  color: #165DFF;
  margin-bottom: 16px;
}

.user-table {
  width: 100%;
  border-collapse: collapse;
}

.table-header, .table-row {
  display: flex;
  width: 100%;
  line-height: 40px;
  border-bottom: 1px solid #f5f5f5;
}

.table-header {
  font-weight: bold;
  color: #666;
}

.table-header span, .table-row span {
  flex: 1;
  text-align: center;
}

.table-row:hover {
  background: #fafafa;
}
</style>

(3)启动远程应用

arduino 复制代码
cd user-app
npm run serve  # 启动后访问 http://localhost:8081

验证配置是否生效 :访问 http://localhost:8081/remoteEntry.js,若能看到一段 JS 代码(包含模块映射逻辑),说明远程应用配置成功。

3. 第三步:配置另一个远程应用(order-app)

订单模块的配置与用户模块几乎一致,只需修改以下几个关键参数(避免冲突):

javascript 复制代码
// order-app/vue.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  publicPath: process.env.NODE_ENV === 'production' 
    ? 'https://your-cdn.com/order-app/' 
    : 'http://localhost:8082/',  // 端口改为 8082

  devServer: {
    port: 8082,  // 端口改为 8082
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },

  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'orderApp',  // 唯一标识改为 orderApp
        filename: 'remoteEntry.js',
        exposes: {
          './OrderList': './src/components/OrderList.vue'  // 暴露订单列表组件
        },
        shared: {  // 共享依赖配置与 user-app 一致
          vue: {
            singleton: true,
            requiredVersion: require('./package.json').dependencies.vue
          },
          'vue-router': {
            singleton: true,
            requiredVersion: require('./package.json').dependencies['vue-router']
          }
        }
      })
    ]
  }
};

同样,创建 OrderList.vue 组件(逻辑与 UserList.vue 类似,替换为订单数据即可),启动应用:

arduino 复制代码
cd order-app
npm run serve  # 访问 http://localhost:8082/remoteEntry.js 验证

4. 第四步:配置宿主应用(host-app)

宿主应用的核心任务是:声明 "加载哪些远程应用" 和 "共享哪些依赖" ,并通过路由实现微应用的按需加载。

(1)修改 vue.config.js

javascript

运行

javascript 复制代码
// host-app/vue.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  publicPath: process.env.NODE_ENV === 'production' 
    ? 'https://your-cdn.com/host-app/' 
    : 'http://localhost:8080/',  // 宿主应用地址

  devServer: {
    port: 8080  // 宿主应用端口
  },

  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'hostApp',  // 宿主应用唯一标识(非必需,但建议配置)

        // 关键:声明要加载的远程应用(键:自定义别名,值:远程应用标识@地址)
        // 格式:[远程应用name]@[远程应用publicPath][远程应用filename]
        remotes: {
          userApp: 'userApp@http://localhost:8081/remoteEntry.js',
          orderApp: 'orderApp@http://localhost:8082/remoteEntry.js'
        },

        // 共享依赖:必须与远程应用的配置完全一致(否则无法共享)
        shared: {
          vue: {
            singleton: true,
            requiredVersion: require('./package.json').dependencies.vue
          },
          'vue-router': {
            singleton: true,
            requiredVersion: require('./package.json').dependencies['vue-router']
          }
        }
      })
    ]
  }
};

(2)配置路由:实现微应用按需加载

通过 Vue Router 的 "动态 import" 功能,实现 "路由切换时加载对应远程应用":

javascript

运行

javascript 复制代码
// host-app/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

// 动态加载远程应用组件(核心!)
// 格式:() => import('[远程应用别名]/[暴露的模块键]')
const UserList = () => import('userApp/UserList')
const OrderList = () => import('orderApp/OrderList')

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    // 访问 /users 时加载用户模块
    path: '/users',
    name: 'users',
    component: UserList
  },
  {
    // 访问 /orders 时加载订单模块
    path: '/orders',
    name: 'orders',
    component: OrderList
  }
]

const router = createRouter({
  history: createWebHistory(),  // 使用 HTML5 History 模式(推荐)
  routes
})

export default router

(3)优化宿主页面:添加导航和加载状态

修改 App.vue,添加导航栏和加载状态提示(提升用户体验):

vue

xml 复制代码
<!-- host-app/src/App.vue -->
<template>
  <div id="app">
    <!-- 导航栏:切换不同微应用 -->
    <nav class="app-nav">
      <router-link to="/">首页</router-link>
      <router-link to="/users">用户管理</router-link>
      <router-link to="/orders">订单管理</router-link>
    </nav>

    <!-- 路由容器:加载微应用 -->
    <div class="app-content">
      <!-- 加载状态提示(远程组件加载时显示) -->
      <template v-if="$route.name !== 'home'">
        <div class="loading" v-if="isLoading">加载中...</div>
      </template>
      <router-view @beforeEnter="handleBeforeEnter" @afterEnter="handleAfterEnter" />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isLoading = ref(false)

// 路由进入前:显示加载状态
const handleBeforeEnter = () => {
  isLoading.value = true
}

// 路由进入后:隐藏加载状态
const handleAfterEnter = () => {
  isLoading.value = false
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}

.app-nav {
  background: #165DFF;
  padding: 0 24px;
  line-height: 50px;
}

.app-nav a {
  color: #fff;
  text-decoration: none;
  margin-right: 24px;
  font-size: 14px;
}

.app-nav a.router-link-exact-active {
  font-weight: bold;
  border-bottom: 2px solid #fff;
}

.app-content {
  padding: 24px;
  background: #f5f7fa;
  min-height: calc(100vh - 50px);
}

.loading {
  text-align: center;
  padding: 48px;
  color: #666;
}
</style>

5. 第五步:运行并验证效果

  1. 启动所有应用(需打开三个终端):

    bash 复制代码
    # 终端1:启动宿主应用
    cd host-app && npm run serve
    
    # 终端2:启动用户模块
    cd user-app && npm run serve
    
    # 终端3:启动订单模块
    cd order-app && npm run serve
  2. 访问宿主应用 :打开浏览器访问 http://localhost:8080,观察以下效果:

    • 点击 "用户管理":路由切换到 /users,页面先显示 "加载中",随后加载用户列表(来自 user-app)

    • 点击 "订单管理":路由切换到 /orders,加载订单列表(来自 order-app)

    • 打开浏览器控制台 → Network 面板:

      • 可看到 remoteEntry.js(远程应用入口文件)被动态加载
      • vuevue-router 只加载一次(共享依赖生效)

git地址:gitee.com/xcxsj/webpa...

相关推荐
怪可爱的地球人2 小时前
Pinia状态管理有哪些常用API?
前端
小高0072 小时前
🤔函数柯里化:化繁为简的艺术与实践
前端·javascript·面试
却尘2 小时前
React useMemo 依赖陷阱:组件重挂载,状态无限复原
前端·javascript·react.js
Asort2 小时前
JavaScript 从零开始(三):浏览器控制台与VS Code协同工作环境搭建详解
前端·javascript
跟橙姐学代码2 小时前
自动化邮件发送的终极秘籍:Python库smtplib与email的完整玩法
前端·python·ipython
我是ed2 小时前
# vue3 实现甘特图
前端
m0_616188492 小时前
el-table的隔行变色不影响row-class-name的背景色
前端·javascript·vue.js
zheshiyangyang3 小时前
Vue3组件数据双向绑定
前端·javascript·vue.js
xw53 小时前
uni-app项目支付宝端Input不受控
前端·uni-app·支付宝