本文将深入讲解微前端架构的核心概念、主流方案对比及实战落地经验,帮助你构建可扩展的大型前端应用。
📋 目录
- 一、微前端概述
- 二、主流微前端方案对比
- 三、qiankun实战
- [四、Module Federation方案](#四、Module Federation方案)
- 五、微前端通信机制
- 六、微前端最佳实践
一、微前端概述
1.1 什么是微前端
微前端是一种将前端应用分解成更小、更简单的独立应用的架构风格,每个应用可以独立开发、测试和部署。
传统单体应用 vs 微前端架构
单体应用:
┌─────────────────────────────────┐
│ 大型前端应用 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │模块A│ │模块B│ │模块C│ ... │
│ └─────┘ └─────┘ └─────┘ │
│ 紧密耦合 │
└─────────────────────────────────┘
微前端架构:
┌─────────────────────────────────┐
│ 主应用(基座) │
├─────────┬─────────┬─────────────┤
│ 子应用A │ 子应用B │ 子应用C │
│ (Vue) │ (React) │ (Angular) │
│ 独立部署 │ 独立部署 │ 独立部署 │
└─────────┴─────────┴─────────────┘
1.2 微前端核心价值
| 价值 | 说明 | 适用场景 |
|---|---|---|
| 技术栈无关 | 不同子应用可用不同框架 | 遗留系统改造 |
| 独立开发部署 | 团队自治,互不影响 | 大型团队协作 |
| 增量升级 | 渐进式重构 | 技术栈迁移 |
| 独立运行时 | 状态隔离,互不干扰 | 复杂业务系统 |
1.3 微前端面临的挑战
javascript
// 挑战1:样式隔离
// 子应用A的样式可能影响子应用B
.button { color: red; } // 全局污染
// 挑战2:JS沙箱
// 子应用修改全局变量影响其他应用
window.globalConfig = { theme: 'dark' };
// 挑战3:应用通信
// 子应用之间如何传递数据
// 挑战4:路由管理
// 主应用和子应用路由如何协调
// 挑战5:公共依赖
// 如何避免重复加载Vue、React等
二、主流微前端方案对比
2.1 方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| iframe | 原生隔离 | 天然隔离、简单 | 性能差、通信复杂 |
| qiankun | JS沙箱+样式隔离 | 功能完善、社区活跃 | 接入成本中等 |
| Module Federation | Webpack5模块共享 | 共享依赖、性能好 | 需要Webpack5 |
| single-spa | 路由劫持 | 灵活、轻量 | 需自行处理隔离 |
| Web Components | 原生组件化 | 标准化、隔离好 | 兼容性、生态 |
2.2 iframe方案
❌ iframe的问题
html
<!-- 主应用 -->
<iframe src="http://sub-app.com" id="sub-app"></iframe>
<script>
// 问题1:URL不同步,刷新丢失状态
// 问题2:DOM结构不共享,弹窗无法全局居中
// 问题3:性能开销大,每个iframe都是独立页面
// 问题4:通信复杂,需要postMessage
</script>
适用场景
javascript
// iframe适合:
// 1. 完全独立的第三方应用嵌入
// 2. 对隔离性要求极高的场景
// 3. 简单的页面嵌入,无复杂交互
三、qiankun实战
3.1 主应用配置
javascript
// main-app/src/main.js
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';
// 注册子应用
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8081',
container: '#sub-container',
activeRule: '/vue',
props: {
token: 'shared-token',
onGlobalStateChange: () => {}
}
},
{
name: 'react-app',
entry: '//localhost:8082',
container: '#sub-container',
activeRule: '/react',
props: {
token: 'shared-token'
}
}
], {
beforeLoad: async (app) => {
console.log('子应用加载前', app.name);
},
beforeMount: async (app) => {
console.log('子应用挂载前', app.name);
},
afterMount: async (app) => {
console.log('子应用挂载后', app.name);
},
beforeUnmount: async (app) => {
console.log('子应用卸载前', app.name);
},
afterUnmount: async (app) => {
console.log('子应用卸载后', app.name);
}
});
// 设置默认进入的子应用
setDefaultMountApp('/vue');
// 启动qiankun
start({
sandbox: {
strictStyleIsolation: true, // 严格样式隔离(Shadow DOM)
// experimentalStyleIsolation: true // 实验性样式隔离
},
prefetch: 'all' // 预加载
});
vue
<!-- main-app/src/App.vue -->
<template>
<div id="main-app">
<header>
<nav>
<router-link to="/vue">Vue子应用</router-link>
<router-link to="/react">React子应用</router-link>
</nav>
</header>
<main>
<!-- 子应用挂载容器 -->
<div id="sub-container"></div>
</main>
</div>
</template>
3.2 Vue子应用配置
javascript
// vue-sub-app/src/main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './router';
let app = null;
let router = null;
let history = null;
// 渲染函数
function render(props = {}) {
const { container } = props;
history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue/' : '/');
router = createRouter({
history,
routes
});
app = createApp(App);
app.use(router);
// 挂载到指定容器或默认容器
const mountEl = container
? container.querySelector('#app')
: document.getElementById('app');
app.mount(mountEl);
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// qiankun生命周期钩子
export async function bootstrap() {
console.log('vue子应用bootstrap');
}
export async function mount(props) {
console.log('vue子应用mount', props);
render(props);
}
export async function unmount() {
console.log('vue子应用unmount');
app.unmount();
app = null;
router = null;
history.destroy();
}
export async function update(props) {
console.log('vue子应用update', props);
}
javascript
// vue-sub-app/vue.config.js
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*' // 允许跨域
}
},
configureWebpack: {
output: {
library: 'vueSubApp',
libraryTarget: 'umd', // 打包成umd格式
chunkLoadingGlobal: 'webpackJsonp_vueSubApp'
}
}
});
3.3 React子应用配置
javascript
// react-sub-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
let root = null;
function render(props = {}) {
const { container } = props;
const mountEl = container
? container.querySelector('#root')
: document.getElementById('root');
root = ReactDOM.createRoot(mountEl);
const basename = window.__POWERED_BY_QIANKUN__ ? '/react' : '/';
root.render(
<BrowserRouter basename={basename}>
<App />
</BrowserRouter>
);
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// qiankun生命周期
export async function bootstrap() {
console.log('react子应用bootstrap');
}
export async function mount(props) {
console.log('react子应用mount', props);
render(props);
}
export async function unmount() {
console.log('react子应用unmount');
root.unmount();
root = null;
}
javascript
// react-sub-app/config-overrides.js (使用react-app-rewired)
module.exports = {
webpack: (config) => {
config.output.library = 'reactSubApp';
config.output.libraryTarget = 'umd';
config.output.chunkLoadingGlobal = 'webpackJsonp_reactSubApp';
config.output.publicPath = 'http://localhost:8082/';
return config;
},
devServer: (configFunction) => {
return (proxy, allowedHost) => {
const config = configFunction(proxy, allowedHost);
config.headers = {
'Access-Control-Allow-Origin': '*'
};
return config;
};
}
};
四、Module Federation方案
4.1 基本概念
javascript
// Module Federation核心概念
// Host: 消费其他应用模块的应用
// Remote: 暴露模块给其他应用的应用
// Shared: 共享的依赖模块
4.2 Remote应用配置
javascript
// remote-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp', // 应用名称
filename: 'remoteEntry.js', // 入口文件
// 暴露的模块
exposes: {
'./Button': './src/components/Button',
'./Header': './src/components/Header',
'./utils': './src/utils/index'
},
// 共享依赖
shared: {
react: {
singleton: true, // 单例模式
requiredVersion: '^18.0.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0'
}
}
})
]
};
4.3 Host应用配置
javascript
// host-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
// 远程应用配置
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js'
},
// 共享依赖
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0'
}
}
})
]
};
4.4 使用远程模块
jsx
// host-app/src/App.jsx
import React, { Suspense, lazy } from 'react';
// 动态导入远程模块
const RemoteButton = lazy(() => import('remoteApp/Button'));
const RemoteHeader = lazy(() => import('remoteApp/Header'));
function App() {
return (
<div>
<h1>Host Application</h1>
<Suspense fallback={<div>Loading...</div>}>
<RemoteHeader title="来自远程应用的Header" />
<RemoteButton onClick={() => alert('clicked')}>
远程按钮
</RemoteButton>
</Suspense>
</div>
);
}
export default App;
4.5 Vite中使用Module Federation
javascript
// vite.config.js
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'host-app',
remotes: {
remoteApp: 'http://localhost:5001/assets/remoteEntry.js'
},
shared: ['vue', 'pinia']
})
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false
}
});
五、微前端通信机制
5.1 qiankun全局状态管理
javascript
// main-app/src/store/global.js
import { initGlobalState } from 'qiankun';
// 初始化全局状态
const initialState = {
user: null,
token: '',
theme: 'light'
};
const actions = initGlobalState(initialState);
// 主应用监听状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('主应用监听到状态变化:', state, prev);
});
// 修改状态
actions.setGlobalState({ user: { name: 'admin' } });
export default actions;
javascript
// 子应用中使用
// vue-sub-app/src/main.js
export async function mount(props) {
const { onGlobalStateChange, setGlobalState } = props;
// 监听全局状态
onGlobalStateChange((state, prev) => {
console.log('子应用监听到状态变化:', state, prev);
// 同步到子应用store
store.commit('updateGlobalState', state);
}, true); // true表示立即触发一次
// 修改全局状态
setGlobalState({ theme: 'dark' });
render(props);
}
5.2 自定义事件通信
javascript
// 事件总线
class EventBus {
constructor() {
this.events = new Map();
}
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
// 返回取消订阅函数
return () => this.off(event, callback);
}
off(event, callback) {
if (!this.events.has(event)) return;
const callbacks = this.events.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
emit(event, data) {
if (!this.events.has(event)) return;
this.events.get(event).forEach(callback => callback(data));
}
}
// 挂载到window
window.__MICRO_APP_EVENT_BUS__ = new EventBus();
// 子应用A发送消息
window.__MICRO_APP_EVENT_BUS__.emit('user:login', { userId: 1 });
// 子应用B接收消息
window.__MICRO_APP_EVENT_BUS__.on('user:login', (data) => {
console.log('用户登录:', data);
});
5.3 Props传递
javascript
// 主应用传递props
registerMicroApps([
{
name: 'sub-app',
entry: '//localhost:8081',
container: '#container',
activeRule: '/sub',
props: {
// 共享数据
userInfo: getUserInfo(),
// 共享方法
logout: () => {
store.dispatch('logout');
router.push('/login');
},
// 共享服务
request: axiosInstance,
// 路由跳转
navigate: (path) => {
router.push(path);
}
}
}
]);
// 子应用接收props
export async function mount(props) {
const { userInfo, logout, request, navigate } = props;
// 使用共享的axios实例
request.get('/api/data').then(res => {
console.log(res);
});
// 调用主应用方法
// logout();
// navigate('/other-app');
}
六、微前端最佳实践
6.1 样式隔离方案
javascript
// 方案1:CSS Modules
// Button.module.css
.button {
color: red;
}
// 方案2:CSS-in-JS
import styled from 'styled-components';
const Button = styled.button`
color: red;
`;
// 方案3:BEM命名规范
.sub-app-a__button--primary {
color: red;
}
// 方案4:添加应用前缀
// postcss.config.js
module.exports = {
plugins: {
'postcss-prefix-selector': {
prefix: '#sub-app-container',
transform: (prefix, selector) => {
return `${prefix} ${selector}`;
}
}
}
};
6.2 公共依赖处理
javascript
// 方案1:externals + CDN
// webpack.config.js
module.exports = {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios'
}
};
// index.html
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
// 方案2:Module Federation shared
shared: {
vue: {
singleton: true,
eager: true,
requiredVersion: '^3.0.0'
}
}
// 方案3:主应用提供依赖
// 主应用
window.Vue = Vue;
window.VueRouter = VueRouter;
// 子应用
const Vue = window.Vue || require('vue');
6.3 路由管理
javascript
// 主应用路由配置
const routes = [
{
path: '/',
component: Layout,
children: [
{ path: '', component: Home },
{ path: 'about', component: About }
]
},
// 子应用路由通配
{ path: '/vue/:pathMatch(.*)*', component: SubAppContainer },
{ path: '/react/:pathMatch(.*)*', component: SubAppContainer }
];
// 子应用路由配置
const routes = [
{ path: '/', component: SubHome },
{ path: '/list', component: SubList },
{ path: '/detail/:id', component: SubDetail }
];
// 子应用内跳转到其他子应用
function navigateToOtherApp(path) {
// 使用history API
window.history.pushState(null, '', path);
// 触发popstate事件让qiankun感知
window.dispatchEvent(new PopStateEvent('popstate'));
}
6.4 错误处理与监控
javascript
// 主应用全局错误处理
import { addGlobalUncaughtErrorHandler } from 'qiankun';
addGlobalUncaughtErrorHandler((event) => {
console.error('微前端全局错误:', event);
// 上报错误
reportError({
type: 'micro-frontend-error',
message: event.message || event.reason,
stack: event.error?.stack
});
// 显示错误提示
showErrorNotification('子应用加载失败,请刷新重试');
});
// 子应用加载失败处理
registerMicroApps(apps, {
beforeLoad: async (app) => {
showLoading();
},
afterMount: async (app) => {
hideLoading();
},
beforeUnmount: async (app) => {
// 清理子应用状态
}
});
// 子应用内部错误边界(React)
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
reportError({ error, errorInfo, app: 'sub-app-name' });
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
6.5 性能优化
javascript
// 1. 预加载子应用
import { prefetchApps } from 'qiankun';
// 首屏加载完成后预加载
window.addEventListener('load', () => {
prefetchApps([
{ name: 'sub-app-1', entry: '//localhost:8081' },
{ name: 'sub-app-2', entry: '//localhost:8082' }
]);
});
// 2. 按需加载
start({
prefetch: 'all', // 'all' | 'none' | string[]
sandbox: {
experimentalStyleIsolation: true
}
});
// 3. 资源缓存
// nginx配置
location /sub-app/ {
add_header Cache-Control "public, max-age=31536000";
}
// 4. 子应用代码分割
// 子应用webpack配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
}
}
📊 微前端方案选型指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 遗留系统改造 | qiankun | 接入成本低,兼容性好 |
| 新项目多团队 | Module Federation | 性能好,共享依赖 |
| 简单页面嵌入 | iframe | 隔离性强,简单 |
| 组件级复用 | Module Federation | 模块粒度细 |
| 技术栈统一 | single-spa | 轻量灵活 |
💡 总结
微前端架构的核心要点:
- ✅ 合理拆分:按业务域拆分,避免过度拆分
- ✅ 样式隔离:CSS Modules、Shadow DOM、命名规范
- ✅ JS沙箱:qiankun沙箱、iframe隔离
- ✅ 通信机制:全局状态、事件总线、Props
- ✅ 公共依赖:externals、Module Federation shared
- ✅ 性能优化:预加载、代码分割、缓存策略
微前端不是银弹,需要根据团队规模和业务复杂度合理选择!
💬 如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论~