随着微前端架构在大型项目中的广泛应用,不同团队独立开发的子应用需要在同一个宿主应用中运行,同时又必须确保彼此之间的代码、状态、样式不会互相干扰。隔离(Isolation)便成为了一个核心问题。
那么,

  • 为什么通常在微前端应用隔离时不选择 iframe 方案?
  • 微前端一般如何做 JavaScript 隔离?
  • 而 qiankun 又是如何实现这一目标的呢?

接下来,让我们一探究竟!

为什么需要 JavaScript 隔离?

在微前端架构中,各个子应用通常由不同团队开发,它们可能使用不同的技术栈、框架甚至版本。当多个子应用运行在同一个全局环境中时,会产生以下问题:

  • 全局变量污染:子应用可能会修改全局变量或挂载全局方法,导致其他子应用发生异常。
  • 样式冲突:虽然主要关注的是 JavaScript 隔离,但全局 CSS 变量或样式同样会引起冲突。
  • 事件和定时器冲突:子应用中的事件监听、定时器等可能在全局范围内互相干扰,导致意想不到的行为。

为了避免这些问题,必须为每个子应用创建一个独立的运行环境,即隔离其 JavaScript 作用域。

为什么不选择 iframe 方案?

在微前端架构中,虽然 iframe 天然提供了较高的隔离度,但通常不选用 iframe 方案,主要有以下几个原因:

  1. 性能问题
    iframe 每次加载都会建立一个新的浏览器上下文,会增加内存占用和页面加载时间。多个 iframe 嵌套时,性能开销更明显,不利于整体应用的响应速度。

  2. 通信与数据共享困难
    iframe 之间或 iframe 与宿主页面之间的数据传递通常需要依赖 postMessage 等方式,这种通信方式比直接模块间通信更繁琐且容易出错。微前端往往需要组件化的高效协同,直接使用 JavaScript 模块和共享状态会更顺畅。

  3. 样式和资源隔离问题
    虽然 iframe 可实现 CSS 的隔离,但在实际应用中,难以做到与主应用风格的统一。反之,通过构建工具(如 webpack module federation 或 single-spa)实现微前端隔离,既能保证各自独立,又能更好地共享全局样式或数据。

  4. 开发调试复杂度
    iframe 的嵌套和跨域问题会使调试和监控变得复杂。微前端更倾向于采用独立构建、模块化集成的方式,这样既保证各应用独立运行,也能借助统一的工具链进行调试和打包优化。

综上所述,虽然 iframe 提供了天然的沙箱机制,但在微前端场景下,为了更高效的性能、更灵活的组件交互和更便捷的开发调试,通常会选择基于模块化和 JavaScript 隔离的方案,而不是使用 iframe。

主流微前端架构如何实现 JavaScript 隔离

1. 沙箱(Sandbox)技术

沙箱技术是实现 JavaScript 隔离的一种常见手段。通过在子应用中建立沙箱环境,可以确保其对全局对象(如 window、document)的修改不会影响到其他子应用。

利用 ES6 Proxy 可以拦截对全局变量的读写操作,构造一个虚拟的全局对象,使子应用只能在该对象上操作,而不会污染实际的 window 对象。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createSandbox() {
const fakeWindow = {};
return new Proxy(window, {
get(target, prop) {
// 如果 fakeWindow 中存在该属性,则优先返回它
if (prop in fakeWindow) {
return fakeWindow[prop];
}
// 否则返回真实 window 上的属性
return target[prop];
},
set(target, prop, value) {
// 将所有写操作保存到 fakeWindow 中
fakeWindow[prop] = value;
return true;
},
});
}

const sandbox = createSandbox();
// 在 sandbox 环境下运行子应用代码
sandbox.myGlobalVar = 'Hello Sandbox!';
console.log(window.myGlobalVar); // undefined
console.log(sandbox.myGlobalVar); // "Hello Sandbox!"

这种方式可以将子应用对全局变量的修改限制在沙箱中,从而实现基本的隔离。

2. 模块化加载与动态导入

利用 ES6 模块化特性,微前端应用可以将子应用拆分为独立模块。通过构建工具(如 Webpack、Rollup 或 Vite)进行打包,结合动态导入(dynamic import)技术,可以只加载当前需要的模块,从而避免不必要的全局污染。

示例代码(动态导入):

1
2
3
4
5
// 动态加载子应用模块
import('./micro-app.js').then(module => {
// 子应用模块的初始化代码
module.bootstrap();
});

这种方式可以确保子应用仅在需要时加载,并且其模块作用域与其他子应用相互独立。

3. 运行时隔离(Runtime Isolation)

运行时隔离通过在运行时重写全局对象的访问行为,实现子应用的隔离。常见手段包括:

  • 重写 window 对象:在子应用运行前,将其 window 替换为沙箱对象,拦截所有对全局属性的访问。
  • 利用 Web Worker:虽然主要用于多线程运算,但在某些场景下,可以将子应用运行在 Web Worker 中,实现隔离。

这种方法通常需要较为复杂的实现,并且会增加一定的性能开销,因此在微前端架构中,多数场景会首选 Proxy 沙箱或借助成熟框架实现隔离。

Qiankun 是如何实现 JavaScript 隔离的?

Qiankun 作为国内领先的微前端解决方案,通过独特的沙箱(Sandbox)机制为各子应用提供了良好的 JavaScript 隔离能力。

基于 Proxy 的沙箱

在现代浏览器中,Qiankun 主要采用基于 ES6 Proxy 的沙箱方案。这种方案的核心思路是利用 Proxy 拦截对全局对象的访问和修改操作,将这些操作限定在沙箱内部。

1. 实现原理

  • 拦截全局变量的访问
    Qiankun 会构造一个 Proxy 对象,包装真实的 window 对象。当子应用试图读取或设置全局变量时,Proxy 会拦截这些操作。如果是读取操作,首先会在沙箱内查找该属性;如果不存在,则回退到真实的 window 上。

  • 隔离全局修改
    对全局变量的写操作都会被定向到沙箱内的一个”虚拟全局对象”,而不是直接修改真实的 window 对象。这意味着子应用对 window 的修改仅在沙箱内部有效,当子应用卸载时,这些修改会被清除,不会对其他子应用或主应用产生影响。

2. 示例代码

下面是一个简化的基于 Proxy 的沙箱示例,用于说明基本原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createSandbox() {
const sandboxData = {}; // 存放子应用对全局变量的修改

return new Proxy(window, {
get(target, prop) {
// 如果沙箱内已有该属性,则返回沙箱内的值
if (prop in sandboxData) {
return sandboxData[prop];
}
// 否则返回真实 window 的属性
return target[prop];
},
set(target, prop, value) {
// 将所有写操作存放在沙箱内,而不修改真实 window
sandboxData[prop] = value;
return true;
},
has(target, prop) {
return prop in sandboxData || prop in target;
}
});
}

const sandbox = createSandbox();
sandbox.myGlobalVar = 'Hello Qiankun!';
console.log(window.myGlobalVar); // undefined
console.log(sandbox.myGlobalVar); // "Hello Qiankun!"

通过这种方式,子应用在 sandbox 内的所有全局操作都不会泄漏到主应用中。

备选方案:快照沙箱

在不支持 Proxy 的旧浏览器(如 IE11)中,Qiankun 则采用了快照沙箱方案。其思路是在子应用启动前,记录当前全局对象的状态;在子应用运行过程中,将所有修改记录下来;而当子应用卸载时,再将全局状态恢复为启动前的状态。这样可以在一定程度上实现全局隔离,但相比 Proxy 沙箱来说,性能和隔离精度可能稍逊一筹。

隔离方案的优缺点

优点

  • 高效隔离:基于 Proxy 的沙箱能实时拦截对全局对象的修改,实现高度隔离。
  • 防止全局污染:子应用对全局变量的修改不会影响主应用和其他子应用,确保系统稳定性。
  • 动态恢复:子应用卸载时可以快速恢复全局环境,不会遗留残余数据。

缺点

  • 兼容性问题:Proxy 沙箱在不支持 Proxy 的旧浏览器中无法使用,需采用快照沙箱,但后者隔离效果和性能不如 Proxy 方案。
  • 实现复杂度:对于部分边缘场景(如跨 iframe 或特殊 DOM 操作),隔离机制仍可能存在细微差异,需额外处理。

Qiankun 如何做样式隔离?

基于 Shadow DOM的严格样式隔离

严格样式隔离利用浏览器的 Shadow DOM 技术为每个子应用创建独立的 DOM 子树,使其样式和结构都被封装在该 Shadow Root 内部。这样,子应用内部定义的 CSS 规则不会泄露到全局,也不会受到外部样式的干扰,从而实现真正的隔离。

在 Qiankun 的启动配置中,可以开启严格样式隔离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';

// 注册子应用
registerMicroApps([
{
name: 'micro-app',
entry: '//localhost:7100',
container: '#micro-container',
activeRule: '/micro-app',
},
]);

// 设置默认挂载应用
setDefaultMountApp('/micro-app');

// 启动 qiankun,并启用严格样式隔离
start({
sandbox: {
strictStyleIsolation: true,
},
});

在上述配置中,设置 sandbox.strictStyleIsolationtrue 后,Qiankun 会在挂载子应用时将其根容器替换为 Shadow Root,这样子应用的所有样式都被限定在这个 Shadow DOM 内部。例如,子应用原本在全局作用域定义的样式只会在其 Shadow DOM 内起作用,而不会影响主应用或其他子应用。

优点与局限

  • 优点

    • 完全隔离:真正做到样式和 DOM 的双重封装,避免全局冲突。

    • 原生支持:依赖浏览器内置的 Shadow DOM,不需要额外处理样式重写。

  • 局限

    • 兼容性:旧版浏览器(如 IE11)不支持 Shadow DOM,需要降级处理。

    • 弹窗或全局组件:某些 UI 组件(例如挂载在 document.body 的弹窗)可能无法直接应用 Shadow DOM 隔离,需要额外处理。

实验性样式隔离 – 基于样式前缀重写

当严格样式隔离因兼容性或业务需求无法采用时,Qiankun 提供了实验性样式隔离方案。该方案通过动态修改子应用中的 CSS 选择器,为其添加一个独特的前缀(通常是一个标识属性或类名),从而使子应用的样式只在特定 DOM 范围内生效。

实验性隔离方案会将所有选择器重写,添加前缀(如 [qiankun-child]):

1
2
3
4
5
6
7
8
9
/* 原始样式 */
.btn {
color: red;
}

/* 重写后样式 */
[qiankun-child] .btn {
color: red;
}

这种重写可以通过如下简化的示例代码实现(仅作演示):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function transformCSS(cssString, prefix = '[qiankun-child]') {
// 简单示例:为每个选择器前加上前缀
return cssString.replace(/(^|\})(\s*[^{]+)/g, (match, p1, selectors) => {
// 分割多个选择器并添加前缀
const newSelectors = selectors
.split(',')
.map(s => `${prefix} ${s.trim()}`)
.join(', ');
return `${p1} ${newSelectors}`;
});
}

const originalCSS = `.btn { color: red; }`;
const transformedCSS = transformCSS(originalCSS);
console.log(transformedCSS);
// 输出:[qiankun-child] .btn { color: red; }

在实际项目中,Qiankun 会自动解析子应用的 CSS,重写其中的选择器,然后注入经过转换的样式表,确保子应用样式仅在其容器内部生效。

配置示例:

在 Qiankun 中,启用实验性样式隔离非常简单,只需在启动配置中设置对应选项:

1
2
3
4
5
start({
sandbox: {
experimentalStyleIsolation: true,
},
});

当配置 experimentalStyleIsolation 后,Qiankun 会为每个子应用的根元素添加一个标识符(例如 qiankun-child 属性或一个特定的 class),并在加载子应用时自动对 CSS 选择器进行前缀重写。

优点与局限

  • 优点

    • 兼容性好:无需依赖 Shadow DOM,适用于所有现代浏览器,包括部分不支持 Shadow DOM 的环境。

    • 灵活性高:可以在不破坏全局样式的前提下,实现子应用样式的隔离。

  • 局限

    • 重写复杂性:对于复杂的 CSS 选择器,自动重写可能会出现细微差异,需进行充分测试。

    • 权重问题:前缀重写后的 CSS 可能会因为选择器权重变化而需要额外调整,确保样式正确应用。

希望这篇文章可以帮到你!

Happy Coding!