随着前端业务复杂度不断提升,微前端架构凭借 “拆分巨石应用、独立开发部署” 的优势被广泛应用,qiankun 作为微前端领域的主流解决方案,其 “隔离机制” 是面试中的高频考点 —— 主应用与子应用、子应用之间若共享全局环境,极易出现 JS 变量污染、CSS 样式冲突问题。今天就从原理到实现,拆解 qiankun 如何解决 JS 与 CSS 隔离难题。
一、JS 隔离:用 “沙箱” 守住全局环境不被污染
微前端场景中,不同子应用可能依赖不同版本的 Vue/React,甚至会修改window上的全局变量(如Vue、$)。若不做隔离,子应用 A 的修改会覆盖子应用 B 的全局变量,导致功能异常。qiankun 通过 “沙箱(Sandbox)” 机制,为每个子应用打造独立的 JS 运行环境,核心有三种实现方案。
1. SnapshotSandbox(快照沙箱):最简单的 “备份 - 还原” 思路
快照沙箱的核心逻辑类似 “系统还原”—— 在子应用挂载前给window拍张 “快照”,卸载时再根据快照恢复window状态,避免子应用的修改影响全局。
实现原理
-
挂载前(激活沙箱) :遍历
window对象,保存所有属性及其原始值,形成 “快照”(如window.foo的初始值、window.Vue的引用等)。 -
运行中:子应用可自由修改
window变量(如新增window.app1Data、修改window.foo),这些改动会直接作用于真实window。 -
卸载后(失活沙箱) :对比当前
window与快照:- 子应用新增的属性(快照中没有的)直接删除(如
delete window.app1Data); - 子应用修改过的属性(与快照值不一致的)还原为初始值(如
window.foo恢复为快照时的数值)。
- 子应用新增的属性(快照中没有的)直接删除(如
简化伪代码
js
function createSnapshotSandbox() {
const rawWindow = window; // 指向真实window
let windowSnapshot = null; // 存储挂载前的window快照
let modifiedProps = {}; // 记录子应用修改过的属性
return {
// 激活:拍快照
activate() {
windowSnapshot = {};
// 遍历window,保存所有可访问属性的初始值
for (const key in rawWindow) {
try {
windowSnapshot[key] = rawWindow[key];
} catch (err) {
// 忽略不可访问的属性(如某些浏览器私有属性)
}
}
},
// 子应用修改全局变量时调用,记录变更
setGlobalProp(key, value) {
if (!modifiedProps.hasOwnProperty(key)) {
modifiedProps[key] = rawWindow[key]; // 保存原始值
}
rawWindow[key] = value; // 应用修改
},
// 失活:恢复window到快照状态
deactivate() {
for (const key in rawWindow) {
if (!(key in windowSnapshot)) {
// 删除子应用新增的全局属性
delete rawWindow[key];
} else if (rawWindow[key] !== windowSnapshot[key]) {
// 还原被修改的属性
rawWindow[key] = windowSnapshot[key];
}
}
modifiedProps = {}; // 清空变更记录
}
};
}
优缺点
- 优点:逻辑简单,易理解,无需依赖特殊 API;
- 缺点:挂载 / 卸载时需全量遍历
window,性能较差;且同一时间只能运行一个子应用(多子应用并行会覆盖快照),仅适合简单场景。
2. LegacySandbox(单实例沙箱):优化 “变更记录” 的轻量方案
为解决快照沙箱 “全量遍历” 的性能问题,qiankun 早期推出了 LegacySandbox—— 不再全量备份window,而是只记录子应用 “新增 / 修改” 的属性,卸载时仅处理这些变更,减少遍历开销。
实现原理
维护三份核心映射表,精准追踪全局变量变更:
-
addedPropsMap:记录子应用新增的全局属性(如window.app1Flag); -
modifiedPropsOriginalMap:记录子应用修改前的属性原始值(如window.foo的初始值); -
currentUpdatedPropsMap:记录子应用修改后的属性最新值(如window.foo的新值)。
运行流程:
-
激活沙箱:遍历
currentUpdatedPropsMap,将子应用上次运行时的修改恢复到window(比如子应用 A 上次改了window.foo=123,激活时就把window.foo设为 123); -
运行中:子应用修改全局变量时,通过
setWindowProp方法记录变更:- 若属性是新增的(
window原本没有),加入addedPropsMap; - 若属性是首次修改,将原始值存入
modifiedPropsOriginalMap,新值存入currentUpdatedPropsMap;
- 若属性是新增的(
-
卸载沙箱:删除
addedPropsMap中的新增属性,用modifiedPropsOriginalMap还原修改过的属性。
简化伪代码
js
class LegacySandbox {
constructor(appName) {
this.appName = appName; // 子应用名称,用于标识沙箱
this.addedPropsMap = new Map(); // 新增属性:key→value
this.modifiedPropsOriginalMap = new Map(); // 修改属性:key→原始值
this.currentUpdatedPropsMap = new Map(); // 当前修改:key→新值
}
// 激活沙箱:恢复上次运行的状态
activate() {
this.currentUpdatedPropsMap.forEach((newVal, key) => {
window[key] = newVal;
});
}
// 记录全局变量变更
setWindowProp(prop, value) {
if (!window.hasOwnProperty(prop)) {
// 新增属性,记录到addedPropsMap
this.addedPropsMap.set(prop, value);
} else if (!this.modifiedPropsOriginalMap.has(prop)) {
// 首次修改,保存原始值
this.modifiedPropsOriginalMap.set(prop, window[prop]);
}
// 记录最新值
this.currentUpdatedPropsMap.set(prop, value);
// 应用修改到真实window
window[prop] = value;
}
// 失活沙箱:清理变更
deactivate() {
// 1. 删除子应用新增的属性
this.addedPropsMap.forEach((_, prop) => {
delete window[prop];
});
// 2. 还原修改过的属性
this.modifiedPropsOriginalMap.forEach((originalVal, prop) => {
window[prop] = originalVal;
});
}
}
优缺点
- 优点:仅追踪变更属性,性能比快照沙箱好;支持子应用 “再次激活时恢复上次状态”;
- 缺点:仍依赖真实
window,同一时间只能运行一个子应用(多子应用并行会冲突),无法满足复杂场景。
3. ProxySandbox(代理沙箱):现代浏览器的 “终极方案”
前面两种沙箱都有 “单实例限制”,无法支持多个子应用同时运行(比如主应用同时打开子应用 A 和子应用 B)。qiankun 在现代浏览器中(支持 ES6 Proxy),采用 ProxySandbox 实现 “多实例隔离”,核心是给每个子应用创建一个 “假 window”,让修改只作用于假 window,不污染真实全局。
实现原理
核心依赖 ES6 的Proxy和with语句,关键步骤如下:
-
创建 “假 window”(fakeWindow) :一个空对象,无原型链(
Object.create(null)),避免继承真实window的属性; -
生成 Proxy 代理:对
fakeWindow做代理,拦截 “读 / 写 / 删除” 等操作:- 读属性(get) :优先从
fakeWindow取(子应用自己的修改),若没有则从真实window取(如console、document等全局对象); - 写属性(set) :只把值写入
fakeWindow,不修改真实window; - 删除属性(deleteProperty) :只删除
fakeWindow中的属性,不影响真实window;
- 读属性(get) :优先从
-
绑定子应用运行环境:用
new Function和with语句,让子应用的代码 “以为” 自己在操作真实window,实际所有操作都被 Proxy 拦截到fakeWindow上。
简化伪代码
js
// 1. 创建代理沙箱
function createProxySandbox() {
const fakeWindow = Object.create(null); // 空的假window,无原型
// 生成Proxy,拦截对fakeWindow的操作
const sandboxProxy = new Proxy(fakeWindow, {
get(target, prop) {
// 读:先读子应用自己的fakeWindow,没有则读真实window
if (prop in target) {
return target[prop];
}
return window[prop];
},
set(target, prop, value) {
// 写:只写入fakeWindow,不污染真实window
target[prop] = value;
return true; // 符合Proxy规范,返回成功标识
},
deleteProperty(target, prop) {
// 删除:只删fakeWindow的属性
if (prop in target) {
delete target[prop];
}
return true;
},
has(target, prop) {
// with语句查找变量时触发,确保能找到真实window的属性(如console)
return prop in target || prop in window;
}
});
return sandboxProxy;
}
// 2. 让子应用在沙箱中运行
function runAppInSandbox(appCode, sandboxProxy) {
// 创建函数:参数为window,内部用with绑定沙箱代理
const appWrapper = new Function("window", `
with(window) {
${appCode} // 子应用代码在这里执行
}
`);
// 传入沙箱代理作为window,子应用会以为这是真实window
appWrapper(sandboxProxy);
}
// 3. 测试:两个子应用并行运行,互不干扰
const sandboxA = createProxySandbox(); // 子应用A的沙箱
const sandboxB = createProxySandbox(); // 子应用B的沙箱
// 子应用A修改window.foo
runAppInSandbox(`
window.foo = "我是子应用A的foo";
console.log("子应用A:", window.foo); // 输出“我是子应用A的foo”
`, sandboxA);
// 子应用B修改window.foo
runAppInSandbox(`
window.foo = "我是子应用B的foo";
console.log("子应用B:", window.foo); // 输出“我是子应用B的foo”
`, sandboxB);
// 真实window不受影响
console.log("真实window.foo:", window.foo); // 输出“undefined”
优缺点
- 优点:支持多子应用并行运行(每个子应用有独立
fakeWindow);性能好(无需全量遍历 / 恢复);隔离彻底; - 缺点:依赖 ES6 Proxy,不支持 IE 等老旧浏览器(需配合其他沙箱降级)。
二、CSS 隔离:三种方案按需选择,平衡隔离与兼容性
与 JS 隔离不同,qiankun 没有强制统一的 CSS 隔离方案,而是提供三种选项,开发者可根据业务兼容性需求灵活选择。
1. 默认方案:无强隔离,追求性能优先
qiankun 默认不启用 CSS 隔离,子应用的样式会直接插入主应用的head标签中。这种方式的优点是性能最好(无额外样式处理开销),但缺点也明显 —— 子应用的样式可能污染主应用(如子应用的.header样式覆盖主应用的.header),主应用的全局样式(如body、p)也可能影响子应用。
适用场景:子应用与主应用样式命名规范一致(如都用 BEM 命名),能通过命名避免冲突;或对性能要求极高,且样式冲突风险低的场景。
2. StrictStyleIsolation(严格隔离):用 Shadow DOM 彻底隔离
开启严格隔离后,qiankun 会将子应用挂载到一个Shadow DOM容器中。Shadow DOM是浏览器原生特性,其内部的样式不会影响外部(主应用),外部样式也无法渗透到内部(子应用),实现 “完全隔离”。
启用方式
在注册子应用时配置sandbox.strictStyleIsolation: true:
js
import { registerMicroApps } from 'qiankun';
registerMicroApps(
[
{
name: 'app1',
entry: '//localhost:8081',
container: '#app-container',
activeRule: '/app1',
},
],
{
sandbox: {
strictStyleIsolation: true, // 启用Shadow DOM隔离
},
}
);
优缺点
- 优点:隔离彻底,无需担心样式冲突;
- 缺点:
Shadow DOM对部分全局样式兼容性差(如子应用引用的全局样式库无法生效);部分浏览器(如 IE)不支持Shadow DOM。
3. ExperimentalStyleIsolation(实验性隔离):动态加前缀,兼容更好
实验性隔离的思路类似 Vue 的scoped CSS—— 给子应用的容器标签添加一个自定义属性(如data-qiankun="app1"),然后在子应用样式插入前,动态给所有 CSS 选择器加前缀(如.header变成[data-qiankun="app1"] .header),确保子应用的样式只作用于自身容器内的元素。
启用方式
配置sandbox.experimentalStyleIsolation: true:
js
registerMicroApps(
[/* 子应用配置 */],
{
sandbox: {
experimentalStyleIsolation: true, // 启用实验性隔离
},
}
);
优缺点
- 优点:兼容性比 Shadow DOM 好(支持大部分现代浏览器);无需修改子应用样式,接入成本低;
- 缺点:属于 “实验性” 特性,对复杂 CSS 选择器(如嵌套选择器、伪元素)的处理可能存在边缘 case;样式前缀处理会有轻微性能开销。
总结:qiankun 隔离方案的选择建议
| 隔离类型 | 方案 | 核心优势 | 适用场景 |
|---|---|---|---|
| JS 隔离 | SnapshotSandbox | 逻辑简单,无 API 依赖 | 简单场景、单子应用运行、兼容老旧浏览器 |
| JS 隔离 | LegacySandbox | 性能优于快照,支持状态恢复 | 单子应用运行,对性能有一定要求 |
| JS 隔离 | ProxySandbox | 多实例并行,隔离彻底 | 现代浏览器、多子应用同时运行的复杂场景 |
| CSS 隔离 | 默认无隔离 | 性能最好 | 样式命名规范统一,冲突风险低 |
| CSS 隔离 | StrictStyleIsolation | 隔离彻底 | 需完全隔离,且兼容 Shadow DOM 的场景 |
| CSS 隔离 | ExperimentalStyleIsolation | 兼容性好 | 需隔离且追求兼容性,可接受实验性特性 |
掌握 qiankun 的隔离机制,不仅能应对面试,更能在实际项目中根据业务场景选择合适的方案,避免因全局污染导致的诡异问题。