微前端架构
深度解析
从 JS 沙箱原理到 CSS 隔离策略,从依赖共享方案到技术选型决策 —— 全面拆解微前端的核心隔离机制与工程化实践。
微前端要解决的三大问题
JS 沙箱隔离
多个子应用共享同一个全局 window,如何防止全局变量污染和冲突?
CSS 样式隔离
不同子应用的 CSS 选择器可能重名,如何保证样式互不干扰?
公共依赖共享
每个子应用都打包 React/Vue 会导致资源浪费,如何高效共享?
JS 沙箱方案
沙箱(Sandbox)是微前端的核心能力,它为每个子应用创建一个隔离的 JavaScript 执行环境,防止子应用修改全局对象导致其他应用出错。
iframe 沙盒
利用浏览器原生 iframe 标签实现完全隔离。每个子应用运行在独立的浏览上下文中,拥有独立的 window、document 和全局作用域。
- +隔离性最强,CSS/JS 完全独立
- +无需任何 JS 沙箱改造
- +天然支持多实例并行运行
- +子应用可使用任意前端框架
- !URL 状态无法自动同步(路由割裂)
- !弹窗/遮罩层无法突破 iframe 边界
- !DOM 不共享,通信成本高
- !性能开销较大,每次加载完整文档
- !SEO 不友好
查看代码示例
// 主应用创建 iframe
const iframe = document.createElement('iframe');
iframe.src = 'https://sub-app.example.com';
iframe.style.cssText = 'width:100%;height:100%;border:none;';
container.appendChild(iframe);
// 通信
iframe.contentWindow.postMessage(
{ type: 'INIT', data: { user, token } },
'https://sub-app.example.com'
);
window.addEventListener('message', (e) => {
if (e.origin === 'https://sub-app.example.com') {
console.log('收到子应用消息:', e.data);
}
});快照沙盒 (Snapshot Sandbox)
在子应用激活时恢复全局状态快照,在卸载时保存当前状态快照。通过 diff 比较实现状态的保存与恢复。
- +兼容性极好,支持 IE 等旧浏览器
- +实现相对简单,易于理解
- +子应用无感知,无需改造
- !同一时刻只能运行一个子应用
- !快照/恢复性能损耗随 window 属性增多而增大
- !无法处理 window 上的 getter/setter
- !可能存在状态残留
查看代码示例
class SnapshotSandbox {
constructor() {
this.snapshot = {};
this.modifyPropsMap = {};
}
// 激活:恢复快照
active() {
// 恢复上次保存的状态
for (const key in this.snapshot) {
window[key] = this.snapshot[key];
}
}
// 卸载:保存快照
inactive() {
// 记录当前 window 状态
for (const key in window) {
if (window[key] !== this.snapshot[key]) {
this.modifyPropsMap[key] = window[key];
// 还原为激活前的状态
window[key] = this.snapshot[key];
}
}
// 合并修改到快照
this.snapshot = { ...this.snapshot, ...this.modifyPropsMap };
}
}代理沙盒 (Proxy Sandbox)
使用 ES6 Proxy 创建 fakeWindow 代理对象。子应用的所有全局变量读写操作都映射到各自的 fakeWindow 上,互不干扰。
- +支持多实例同时运行(多代理并存)
- +不修改真实 window,无副作用
- +可精确记录子应用的全局变量修改
- +支持白名单机制,可配置可逃逸的变量
- !不支持 IE 浏览器(Proxy 是 ES6 特性)
- !部分 DOM API 在 Proxy 下可能行为异常
- !with 语句配合 Proxy 有一定性能开销
- !无法完全拦截所有 window 属性(如 window.window)
查看代码示例
class ProxySandbox {
constructor(name) {
this.name = name;
this.fakeWindow = {};
// 记录所有被 set 过的属性
this.updatedValueSet = new Set();
this.proxy = new Proxy(window, {
get: (target, key) => {
// 优先从 fakeWindow 取值
if (key in this.fakeWindow) {
return this.fakeWindow[key];
}
const val = target[key];
// 绑定正确的 this 指向
return typeof val === 'function'
? val.bind(target) : val;
},
set: (_, key, value) => {
this.fakeWindow[key] = value;
this.updatedValueSet.add(key);
return true;
},
has: (_, key) => {
return key in this.fakeWindow || key in window;
},
});
}
}VM 沙盒 (V8 Sandbox)
利用浏览器端的 JavaScript 解析/执行能力,将子应用代码包裹在自定义的执行上下文中。通过 new Function 或自定义模块加载器实现。
- +隔离粒度最细,可控制到变量级别
- +可实现模块级别的沙箱隔离
- +支持精确的依赖注入
- +体积小,启动速度快
- !with + new Function 存在 CSP 安全策略限制
- !调试困难,源码映射复杂
- !部分浏览器 API 调用可能失效
- !动态执行代码的性能开销
查看代码示例
// micro-app 的沙箱实现思路
function createVmSandbox(code, context) {
// 使用 with 语句将 context 作为作用域链
const fn = new Function(
'window', 'self', 'globalThis',
`with(window) {
try { ${code} }
catch(e) { console.error(e); }
}`
);
// Proxy 包装 context
const proxy = new Proxy(context, {
has: () => true, // 所有变量都从 context 查找
get: (target, key) => target[key] ?? window[key],
set: (target, key, value) => {
target[key] = value;
return true;
}
});
fn(proxy, proxy, proxy);
}沙箱方案对比速查表
| 方案 | 隔离强度 | 多实例 | IE 兼容 | 性能 | 代表框架 |
|---|---|---|---|---|---|
| iframe | ★★★★★ | ✅ | ✅ | ⚠️ 较差 | 无界(wujie) |
| 快照沙盒 | ★★★☆☆ | ❌ | ✅ | ⚡ 良好 | qiankun legacy |
| 代理沙盒 | ★★★★☆ | ✅ | ❌ | ⚡ 良好 | qiankun proxy |
| VM 沙盒 | ★★★★☆ | ✅ | ❌ | ⚡ 良好 | micro-app |
CSS 隔离方案
样式冲突是微前端最常遇到的问题之一。以下方案按隔离强度从高到低排列,各有其适用场景。
Shadow DOM
利用浏览器原生的 Shadow DOM 能力,将子应用的 DOM 和样式完全封装在 Shadow Root 内部,外部样式无法侵入,内部样式也不会泄漏。
代码示例
// micro-app 默认使用 Shadow DOM
class MicroApp extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
// 子应用 HTML 被注入到 Shadow 内部
shadow.innerHTML = '<div id="app-root"></div>';
// 子应用的样式也被注入到 Shadow 内部
// 外部样式完全不影响子应用
}
}Scoped CSS(动态作用域)
在运行时动态为子应用的所有样式规则添加唯一的作用域前缀(如 [data-app-id='vue-app']),使样式仅匹配对应子应用的 DOM 节点。
代码示例
// qiankun scopedCSS 实现思路
function scopedCSS(styleElement, appName) {
// 劫持 sheet.cssRules
const sheet = styleElement.sheet;
const rules = sheet.cssRules;
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (rule instanceof CSSStyleRule) {
// 为选择器添加作用域限定
// .btn → .btn[data-name="sub-app"]
const selector = rule.selectorText;
rule.selectorText = addScope(selector, appName);
}
}
}
function addScope(selector, appName) {
return selector.split(',').map(s =>
'[data-name="' + appName + '"] ' + s.trim()
).join(', ');
}CSS Modules / CSS-in-JS
在编译阶段通过 CSS Modules 的哈希命名或 CSS-in-JS 的运行时唯一类名生成,确保每个子应用的样式类名唯一,从根源上避免冲突。
代码示例
// CSS Modules 示例
// Button.module.css
.btn { background: var(--primary); }
.btn_active { background: var(--active); }
// Button.jsx - 编译后类名自动唯一化
import styles from './Button.module.css';
// 渲染为: <button class="btn_x8k2a btn_active_f3j9p">Click</button>
<button className={`${styles.btn} ${isActive && styles.btn_active}`}>
// styled-components 示例
const StyledBtn = styled.button`
background: ${p => p.primary ? '#8B5CF6' : '#fff'};
// 自动添加唯一哈希类名
`;BEM 命名约定
通过严格的命名规范(Block__Element--Modifier),为每个子应用的所有 CSS 类名添加应用前缀,人为避免命名冲突。
代码示例
/* 每个子应用使用唯一前缀 */
/* 子应用 A */
.app-cart__header--active { color: #8B5CF6; }
.app-cart__item--selected { border: 2px solid #8B5CF6; }
/* 子应用 B */
.app-order__header--active { color: #F472B6; }
.app-order__item--selected { border: 2px solid #F472B6; }
/* PostCSS 自动添加前缀 */
// postcss.config.js
module.exports = {
plugins: {
'postcss-prefix-selector': {
prefix: '.app-cart',
transform(_, selector) {
return '.app-cart ' + selector;
}
}
}
};💡 实践建议:组合使用
在实际项目中,通常组合使用多种 CSS 隔离方案。推荐策略:
依赖共享方案
如果每个子应用都独立打包 React、Vue 等基础库,将导致巨大的资源浪费和性能问题。 依赖共享是微前端工程化的核心优化手段。
Webpack Externals + Import Maps
主应用通过 script 标签全局加载公共依赖(React、Vue 等),子应用通过 Webpack Externals 配置排除这些依赖,运行时从全局获取。
查看代码示例
<!-- 主应用 HTML -->
<script type="importmap">
{
"imports": {
"react": "https://cdn.example.com/react@18.2.0/umd/react.min.js",
"react-dom": "https://cdn.example.com/react-dom@18.2.0/umd/react-dom.min.js",
"vue": "https://cdn.example.com/vue@3.3.0/dist/vue.global.prod.js"
}
}
</script>
// 子应用 webpack.config.js
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
vue: 'Vue'
}
};
// 子应用代码正常 import,运行时使用全局变量
import React from 'react'; // → window.ReactModule Federation(模块联邦)
Webpack 5 原生支持的模块共享方案。应用可作为远程模块暴露组件/工具,其他应用按需异步加载,实现真正的运行时模块共享。
查看代码示例
// App1(远程应用)- webpack.config.js
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./utils': './src/utils/index',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
});
// App2(消费应用)- webpack.config.js
new ModuleFederationPlugin({
name: 'app2',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
// App2 中使用远程组件
const RemoteButton = React.lazy(
() => import('app1/Button')
);SystemJS + UMD
SystemJS 作为通用模块加载器,可在浏览器中加载 UMD/AMD/System 模块格式。主应用使用 SystemJS 加载子应用,子应用输出为 UMD 格式实现依赖共享。
查看代码示例
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.min.js"></script>
<script>
System.import('https://sub-app.example.com/main.js')
.then(appModule => {
appModule.bootstrap(document.getElementById('app'));
});
</script>
// 子应用 webpack 配置 output.libraryTarget
module.exports = {
output: {
libraryTarget: 'system', // 或 'umd'
filename: 'main.js',
},
};
// 通过 importmap 共享依赖
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.example.com/react.production.min.js",
"react-dom": "https://cdn.example.com/react-dom.production.min.js"
}
}
</script>主流框架对比
了解了底层原理,让我们看看市面上主流的微前端框架是如何将这些方案组合使用的。
qiankun
生态成熟、社区活跃、文档完善
沙箱不够彻底、CSS 隔离有边界问题
micro-app
类 Web Component 接入、零改造、轻量
沙箱偶有边界 case、社区较小
wujie(无界)
iframe 隔离但无 URL 割裂问题、组件化接入
较新、生态待完善
Module Federation
Webpack 5 原生、运行时共享、独立部署
不提供沙箱和路由、强绑定 Webpack
🎯 技术选型决策指南
核心要点回顾
微前端的核心价值在于技术栈无关的独立部署能力。 沙箱隔离保证运行时安全,CSS 隔离保证视觉独立,依赖共享优化加载性能。 没有银弹方案,根据团队实际情况选择合适的组合才是关键。