在大型前端项目中,团队协作效率低、技术栈绑定、独立部署困难等问题往往成为瓶颈。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):暴露 "订单列表" 组件
- 三者共享
vue
和vue-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. 第五步:运行并验证效果
-
启动所有应用(需打开三个终端):
bash# 终端1:启动宿主应用 cd host-app && npm run serve # 终端2:启动用户模块 cd user-app && npm run serve # 终端3:启动订单模块 cd order-app && npm run serve
-
访问宿主应用 :打开浏览器访问
http://localhost:8080
,观察以下效果:-
点击 "用户管理":路由切换到
/users
,页面先显示 "加载中",随后加载用户列表(来自 user-app) -
点击 "订单管理":路由切换到
/orders
,加载订单列表(来自 order-app) -
打开浏览器控制台 → Network 面板:
- 可看到
remoteEntry.js
(远程应用入口文件)被动态加载 vue
和vue-router
只加载一次(共享依赖生效)
- 可看到
-
git地址:gitee.com/xcxsj/webpa...