微前端架构详解
一、什么是微前端
微前端是微服务的概念在前端领域的扩展。将Web应用拆分成多个子应用,每个应用由独立的团队开发、测试、部署。
微前端的原则:
- 技术栈无关:可根据业务领域灵活混用Vue\React\Angular等技术栈
- 代码隔离,无法隔离的代码,应用之间采用前缀来区分
- 独立交付:独立开发、独立测试、独立部署
- 团队自治:团队负责全生命周期管理,减少协作成本
典型应用场景:
- 遗留系统现代化改造
- 多团队协作的大型 SaaS 平台
- 需要动态加载不同功能模块的 Portal 类应用
二、微前端要解决的问题
1、通信机制
- 子应用之间通信:共享状态库(Redux、Vuex、Pina等)、URL参数等
- 父子应用通信: 使用CustomEvent发布订阅模式
2、应用隔离
隔离类型 | 问题描述 | 解决方案 |
---|---|---|
JS沙箱 | 全局变量/事件监听污染 | Proxy代理、Shadow Realm、快照沙箱 |
CSS隔离 | 样式冲突、选择器冲突 | CSS Scope、命名空间、Shadow Dom |
存储隔离 | Cookie/LocalStorage冲突 | 前缀命名、封装API |
3、资源加载与共享
- 问题:公共依赖如何加载,避免公共库被多次加载
- 方案:
- Webpack 的
externals
将公共库排除。
- Webpack 的
4、路由与导航
- 问题:主应用于子应用路由冲突,浏览器历史记录管理
- 方案:
- 主应用统一路由分发(URL前缀匹配)
- 使用history.pushState实现无刷新跳转
- 路由劫持
三、微前端的实现原理
1、通过路由映射分发
2、iframe嵌套
- 原理:每个子应用独立运行在 iframe 中。
- 优点:天然隔离(JS/CSS/DOM)。
- 缺点:URL 不同步、性能差、通信复杂(
postMessage
)。
3、WebComponent
webComponent有三个步骤:
- 使用customElements定义组件
- 使用template包含组件的HTML内容,支持使用slot插槽
- 使用Shadow DOM将元素的样式和行为封装在一个隔离的DOM中
<!-- 主应用调用 -->
<micro-app name="cart" src="https://child.com/cart.js"></micro-app>
<!-- 子应用封装 -->
<template id="tpl">
<style>/* Scoped CSS */</style>
<div>子应用内容</div>
</template>
<script>
class MicroApp extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true));
}
}
customElements.define('micro-app', MicroApp);
</script>
3、Module Federation
Module Federation是Webpack5提出来的概念,用来解决多个应用之间代码共享的问题。
核心概念:
- Host(主应用):消费远程模块的容器
- Remote(子应用):暴露模块的远程服务
- Shared:共享依赖(如 React、Vue)
- **Container:**被MF构建的模块
/// 配置webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
name: "appA",
//出口文件
filename: "remoteEntry.js",
//暴露可访问的组件
exposes: {
"./input": "./src/input",
},
//或者其他模块的组件
//如果把这一模块当作基座模块的话,
//这里应该配置其他子应用模块的入口文件
remotes: {
appB: "appB@http://localhost:3002/remoteEntry.js",
},
//共享依赖,其他模块不需要再次下载,便可使用
shared: ['react', 'react-dom'],
})
四、微前端的框架
4.1 single-spa
原理:提供加载器、路由托管,不处理资源加载/隔离。
工作流程:
- 子应用导出
bootstrap
,mount
,unmount
函数。 - 主应用注册子应用并配置路由匹配规则。
- 路由变化时,加载并执行对应子应用。
- 子应用导出
示例配置:
singleSpa.registerApplication( 'app1', () => System.import('app1'), // 动态加载 location => location.pathname.startsWith('/app1') );
4.2 qiankun(基于 single-spa)
- 核心增强:
- HTML Entry:解析子应用 HTML 获取资源列表,替代 JS Entry。
- JS 沙箱:支持 Proxy 和快照沙箱(兼容 IE)。
- 样式隔离:自动为子应用 CSS 添加前缀隔离。
- 预加载:空闲时预加载子应用资源。
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
]);
// 启动 qiankun
start();
需要注意的是,如果采用vite构建的应用,需要使用插件vite-plugin-qiankun
// 子应用vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
plugins: [
vue(),
qiankun('vue-app', { // 子应用名字,与主应用注册的子应用名字保持一致
useDevMode: true
})
],
server: {
origin: 'http://localhost:5174', // 解决静态资源加载404问题
host: 'localhost',
port: 5174
}
})
//子应用main.js
import { createApp } from 'vue'
import App from './App.vue'
import { store } from './store'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let instance = null;
const initQianKun = () => {
renderWithQiankun({
mount(props) {
render(props.container)
// 可以通过props读取主应用的参数:msg
// 监听主应用传值
props.onGlobalStateChange((res) => {
store.count = res.count
console.log(res.count)
})
},
bootstrap() { },
unmount() {
instance.unmount()
instance._instance = null
instance = null
},
update() { }
})
}
const render = (container) => {
if (instance) return;
// 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
// 注意:这边需要避免 id(app) 重复导致子应用挂载失败
const appDom = container ? container.querySelector("#app") : "#app"
instance = createApp(App)
instance.mount(appDom)
}
// 判断当前应用是否在主应用中
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()
// route文件
import { createWebHashHistory } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const router = createRouter({
// 值和主应用中的activeRule保持一致
history: createWebHashHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? '/vueApp' : '/'),
routes: routes
});
详细实践: 点击这里查看实践指南
4.3 Mirco-Ap
Mirco-App是京东出的一款基于 Web Component 原生组件进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。
核心技术实现
Web Components集成
- 自定义元素注册
class MicroApp extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.loadApp(); } } customElements.define('micro-app', MicroApp);
JS作用域隔离
- 基于Proxy的沙箱
function createSandbox(appName) { const proxy = new Proxy(window, { get(target, key) { if (key in target) { return target[key]; } // 返回子应用自有属性 }, set(target, key, value) { if (!target.hasOwnProperty(key)) { // 存储到子应用独立空间 target[`${appName}_${key}`] = value; } else { target[key] = value; } return true; } }); return proxy; }
样式隔离策略
- Shadow DOM原生隔离
- 自动前缀转换
/* 输入 */ .container { background: #fff; } /* 输出 */ micro-app[name=app1] .container { background: #fff; }
数据通信机制
- 基于CustomEvent
// 主应用发送数据 const event = new CustomEvent('micro-app-event', { detail: { type: 'data-update', payload } }); document.querySelector('micro-app').dispatchEvent(event); // 子应用接收 window.addEventListener('micro-app-event', (e) => { console.log(e.detail); });
4.4 无界
无界是腾讯推出的一款微前端解决方式。它是一种基于 Web Components + iframe 的全新微前端方案。
核心技术:
无界沙箱
- 基于iframe的天然隔离
- 主应用创建代理iframe:
<iframe src="about:blank"></iframe>
- 子应用运行在隐藏iframe中
DOM代理机制
- 劫持iframe内的document
const proxyDocument = new Proxy(document, { get: function(target, key) { if (key === 'createElement') { return function(tagName) { const element = target.createElement(tagName); // 代理元素到主应用 return elementProxy(element); }; } return target[key]; } });
- 同步DOM到主应用容器
事件代理
- 劫持事件监听方法
Element.prototype.addEventListener = function(type, listener, options) { // 存储事件监听器 this._listeners = this._listeners || {}; this._listeners[type] = this._listeners[type] || []; this._listeners[type].push(listener); // 在主应用容器上绑定代理事件 hostElement.addEventListener(type, proxyListener); };
路由同步
- 监听iframe的history变化
- 同步到主应用URL
iframe.contentWindow.history.pushState = new Proxy(history.pushState, { apply: function(target, thisArg, args) { const result = target.apply(thisArg, args); // 同步主应用URL window.history.pushState(...args); return result; } });
五、原理解析
1、JS沙箱机制实现
- 快照沙箱: 通过将window的属性复制到
windowSnapshot
中
class SnapshotSandbox {
constructor() {
this.proxy = window;
this.modifyPropsMap = {};
this.active();
}
active() {
this.windowSnapshot = {};
for (const prop in window) {
this.windowSnapshot[prop] = window[prop];
}
Object.keys(this.modifyPropsMap).forEach(p => {
window[p] = this.modifyPropsMap[p];
});
}
inactive() {
for (const prop in window) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
}
}
- 代理沙箱: 使用proxy实现
class ProxySandbox {
constructor() {
const rawWindow = window;
const fakeWindow = {};
this.proxy = new Proxy(fakeWindow, {
set(target, key, value) {
target[key] = value;
return true;
},
get(target, key) {
return target[key] || rawWindow[key];
}
});
}
}
2、Shadow Realm
Shadow Realm 是 ECMAScript 2023 正式标准,提供真正的 JavaScript 运行时隔离环境。与传统沙箱不同,它创建完全独立的全局对象和内置函数副本,实现真正的环境隔离。
class ShadowRealm {
constructor() {
// 创建全新的全局对象
this.global = new Proxy({}, {
get: (_, key) => this._scopedGlobals[key]
});
// 复制内置函数
this._scopedGlobals = {
Array: function() { /* 独立实现 */ },
Date: function() { /* 独立实现 */ },
// ...其他内置对象
};
}
evaluate(sourceText) {
// 在隔离环境中执行代码
const wrapped = `(function(__global__) {
with(__global__) {
${sourceText}
}
})`;
return (0, eval)(wrapped)(this.global);
}
}
// 创建子应用沙箱
const appRealm = new ShadowRealm();
// 执行子应用代码
appRealm.evaluate(`
class MyApp {
mount(container) {
this.el = document.createElement('div');
this.el.textContent = '隔离的子应用';
container.appendChild(this.el);
}
}
new MyApp();
`);
// 获取子应用导出
const { MyApp } = await appRealm.importValue('./app.js', 'MyApp');
const app = new MyApp();
app.mount(document.getElementById('container'));
3、CSS添加命名空间
function scopeCSS(css, prefix) {
return css.replace(/([^{}]+)(?=\s*{)/g, selector => {
return selector.split(',').map(part => {
return `${prefix} ${part.trim()}`
}).join(',');
});
}
// 输入: .btn, .header { color: red; }
// 输出: [data-app="app1"] .btn, [data-app="app1"] .header { color: red; }
4、 Scoped CSS
通过属性选择器实现作用域隔离
<style>
/* 自动转换 */
.container[data-v-7ba5bd90] { color: red; }
</style>
<div class="container" data-v-7ba5bd90>内容</div>
5、Shadow DOM
架构原理
核心特性实现
创建 Shadow DOM:
const host = document.getElementById('host'); const shadowRoot = host.attachShadow({ mode: 'open' });
样式隔离:
<style> :host { /* 宿主元素样式 */ } .btn { /* 仅内部有效 */ } </style>
插槽机制(Slot):
<!-- 定义 --> <div class="card"> <slot name="title">默认标题</slot> <slot name="content"></slot> </div> <!-- 使用 --> <my-card> <h1 slot="title">自定义标题</h1> <p slot="content">自定义内容</p> </my-card>
事件重定向:
shadowRoot.addEventListener('click', e => { // 事件目标重定向 e.target = host; host.dispatchEvent(new CustomEvent('shadow-click', e)); });
微前端应用示例
class MicroApp extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; border: 1px solid #ddd; }
h2 { color: var(--primary-color, blue); }
</style>
<div class="app-container">
<h2>子应用标题</h2>
<slot name="content"></slot>
</div>
`;
}
}
customElements.define('micro-app', MicroApp);
6、 HTML Enity
与传统 JS Entry 对比
特性 | JS Entry | HTML Entry |
---|---|---|
入口文件 | app.js |
index.html |
资源声明 | 需手动配置 externals | 自动解析 link/script 标签 |
样式处理 | 独立加载 CSS 文件 | 自动提取并注入样式 |
子应用结构 | 需暴露生命周期函数 | 无需改造,直接接入 |
核心代码实现
class HTMLEntryLoader {
async loadApp(url, container) {
// 1. 获取 HTML
const html = await fetch(url).then(r => r.text());
// 2. 解析 DOM
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 3. 提取资源
const styles = [...doc.querySelectorAll('link[rel="stylesheet"]')];
const scripts = [...doc.querySelectorAll('script')];
// 4. 创建沙箱容器
const appContainer = document.createElement('div');
appContainer.id = `app-${Date.now()}`;
container.appendChild(appContainer);
// 5. 加载样式
styles.forEach(style => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = new URL(style.href, url).href;
document.head.appendChild(link);
});
// 6. 执行脚本
const execContext = new ShadowRealm();
for (const script of scripts) {
if (script.src) {
const scriptUrl = new URL(script.src, url).href;
const code = await fetch(scriptUrl).then(r => r.text());
execContext.evaluate(code);
} else {
execContext.evaluate(script.textContent);
}
}
// 7. 渲染内容
appContainer.innerHTML = doc.body.innerHTML;
}
}
高级特性实现
资源预加载:
function prefetchResources(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 预加载所有资源 [...doc.querySelectorAll('[src],[href]')].forEach(res => { const url = res.src || res.href; const link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; document.head.appendChild(link); }); }
依赖去重:
const sharedLibs = ['react', 'react-dom']; function filterDuplicateScripts(scripts) { return scripts.filter(script => { return !sharedLibs.some(lib => script.src.includes(`/${lib}/`) && window[lib] ); }); }
样式作用域:
function scopeStyles(styleContent, prefix) { return styleContent.replace( /([^{}]+)(?=\s*{)/g, selector => `${prefix} ${selector}`); }
六、三大框架对比分析
特性 | qiankun | wujie | micro-app |
---|---|---|---|
隔离机制 | JS沙箱+样式重写 | iframe沙箱+DOM代理 | Shadow DOM+Proxy沙箱 |
通信方式 | 全局状态管理 | props/$wujie对象 | CustomEvent+数据监听 |
路由同步 | 手动配置activeRule | 自动同步 | 自动同步 |
性能开销 | 中等(需要解析HTML) | 较低(iframe原生隔离) | 低(原生Web Components) |
接入成本 | 需子应用改造导出生命周期 | 无侵入 | 无侵入 |
多实例支持 | ✓ | ✓ | ✓ |
IE兼容性 | ✓(降级方案) | ✗ | ✗ |
SSR支持 | ✗ | ✓ | ✗ |