微前端
约 1073 字大约 4 分钟
微前端Micro Frontend
2024-08-13
概述
微前端是将微服务架构理念应用于前端的方法,将单体前端拆分为多个小型、独立的前端应用。
┌─────────────────────────────────────────────────┐
│ 主应用容器 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 子应用A │ │ 子应用B │ │ 子应用C │ │
│ │ (Vue) │ │(React) │ │(Angular)│ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────┘核心价值
| 价值 | 说明 |
|---|---|
| 独立开发 | 各子应用可由不同团队独立开发 |
| 独立部署 | 子应用可独立部署,不影响其他应用 |
| 技术栈无关 | 支持 Vue、React、Angular 等任意框架 |
| 增量升级 | 可逐步升级或替换旧系统 |
实现方案
1. 微前端框架
| 框架 | 特点 |
|---|---|
| qiankun | 蚂蚁金服出品,基于 single-spa,轻量级 |
| single-spa | 最早的微前端框架,生态成熟 |
| EMP | 欢聚时代出品,Webpack Module Federation 支持 |
| icestark | 飞冰微前端方案 |
2. qiankun 示例
主应用配置
// main-app/src/main.js
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8080',
container: '#container',
activeRule: '/vue',
},
{
name: 'react-app',
entry: '//localhost:8081',
container: '#container',
activeRule: '/react',
},
]);
start();<!-- index.html -->
<div id="container"></div>子应用配置 (Vue)
// vue-app/src/main.js
import Vue from 'vue';
import App from './App.vue';
let instance = null;
function render() {
instance = new Vue({
render: h => h(App),
}).$mount('#app');
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// qiankun 生命周期
export async function bootstrap() {
console.log('[vue-app] bootstrap');
}
export async function mount(props) {
console.log('[vue-app] mount', props);
render();
}
export async function unmount() {
instance.$destroy();
instance = null;
}// vue-app/vue.config.js
module.exports = {
devServer: {
port: 8080,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};通信机制
方案对比
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| Props 传递 | 父子应用通信 | 低 |
| CustomEvent | 简单事件通信 | 低 |
| window.storage 事件 | 跨标签页通信 | 低 |
| qiankun initState | 官方推荐状态方案 | 中 |
| 基于 Redux/Zustand | 复杂状态管理 | 高 |
Props 通信
// 主应用
<vue-app name="主应用" @handle="handleChange" />
// 主应用传递 props
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8080',
container: '#container',
activeRule: '/vue',
props: {
name: 'main-app',
onGlobalStateChange: (state) => console.log(state),
setGlobalState: (state) => console.log(state),
},
},
]);共享状态方案
// shared/store.js
import { initGlobalState } from 'qiankun';
const initialState = {
user: null,
token: '',
};
const actions = initGlobalState(initialState);
export { actions };路由分发
主应用路由配置
// 方式一:统一在主应用管理
import { registerMicroApps, start } from 'qiankun';
import { addMicroApps } from 'qiankun';
import VueRouter from 'vue-router';
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
// 其他主应用路由
],
});
// 注册子应用
addMicroApps([
{ name: 'vue-app', entry: '//localhost:8080', container: '#container', activeRule: '/vue' },
{ name: 'react-app', entry: '//localhost:8081', container: '#container', activeRule: '/react' },
]);
start();
// 方式二:子应用自行管理
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8080',
container: '#container',
activeRule: (location) => location.pathname.startsWith('/vue'),
},
]);CSS 隔离
方案
| 方案 | 实现 | 优缺点 |
|---|---|---|
| CSS Modules | 编译时处理 | 简单,但需改动构建配置 |
| BEM 命名 | 约定俗成 | 无需改动,但需团队遵守 |
| Shadow DOM | 运行时隔离 | 隔离性强,但样式定制困难 |
| dynamicstylesheet | 动态加载/卸载 | 灵活,但需运行时处理 |
qiankun 样式隔离
// 开启严格的样式隔离
start({
singular: true,
styleSandbox: true,
});
// 或使用预加载
registerMicroApps(apps, {
beforeLoad: [
app => {
console.log('[主应用] before load', app.name);
},
],
beforeMount: [
app => {
console.log('[主应用] before mount', app.name);
},
],
afterUnmount: [
app => {
console.log('[主应用] after unmount', app.name);
},
],
});JS 隔离
qiankun 通过 Proxy 沙箱实现 JS 隔离,确保子应用之间的全局变量不冲突。
// 快照沙箱(Legacy)
// 适用于低版本浏览器或单一子应用场景
// Proxy 沙箱(默认)
// 为每个子应用创建独立的全局对象副本
const sandbox = new ProxyWindow({
name: 'vue-app',
active: true,
});应用间跳转
子应用跳转到主应用
// 子应用内部
window.__ROUTER_BASE__ = import.meta.env.BASE_URL;
// 主应用挂载全局方法
window.navigateTo = (path) => {
router.push(path);
};子应用间跳转
// 通过 props 获取主应用方法
export function mount(props) {
props.navigateTo('/react/dashboard');
}资源预加载
import { prefetchApps } from 'qiankun';
// 鼠标悬停时预加载
const preloadApp = () => prefetchApps([
{ name: 'vue-app', entry: '//localhost:8080' },
{ name: 'react-app', entry: '//localhost:8081' },
]);
<button onMouseEnter={preloadApp}>进入应用</button>最佳实践
- 应用拆分粒度: 按业务域拆分,避免过度碎片化
- 接口设计: 主子应用通过接口通信,避免直接依赖
- 版本管理: 统一版本号,建立兼容性规范
- 样式隔离: 默认开启样式隔离,注意第三方库样式
- 性能优化: 合理使用预加载和懒加载
- 监控告警: 建立统一的前端监控体系
