Appearance
第5章 CSS 隔离与资源加载
"JavaScript 沙箱防止的是逻辑污染,CSS 隔离防止的是视觉坍塌——后者往往更难调试,因为它不会抛出任何异常,只是默默地让你的页面面目全非。"
本章要点
- 深入理解 CSS 隔离的三种核心策略:Shadow DOM、Scoped CSS、Dynamic Stylesheet,掌握每种方案的实现原理与边界条件
- 从源码层面剖析 import-html-entry 如何将一个 HTML 文件解析为 Scripts、Styles、Template 三部分
- 理解子应用资源预加载策略的设计哲学与实现细节
- 掌握资源加载失败时的容错与重试机制,构建生产级别的健壮性
还记得前言中那个凌晨三点的故事吗?一个 .container { margin: 0 auto } 穿透了沙箱,导致全站白屏。那次事故的根因不在 JavaScript 隔离——JS 沙箱工作得很好。问题出在 CSS。
CSS 的全局性是 Web 平台最古老的设计决策之一。在单体应用中,这个问题通过 BEM 命名约定、CSS Modules、CSS-in-JS 等方案已经被充分驯化。但在微前端场景下,情况完全不同——你无法要求所有子应用统一使用同一种样式方案,你甚至无法确保不同团队不会使用相同的 class 名。CSS 隔离不是锦上添花,它是微前端架构的生存底线。
上一章我们深入剖析了 JS 沙箱的三种实现(SnapshotSandbox、LegacySandbox、ProxySandbox)。本章的主角是 CSS 隔离——同样重要,但实现路径截然不同。JS 隔离的核心武器是 Proxy,CSS 隔离的核心武器却分裂成了三条路线,每条路线都有自己的优势与致命缺陷。
我们还将深入 import-html-entry 的源码——这是乾坤资源加载的基石。理解它如何解析 HTML、提取样式和脚本,是理解整个乾坤资源管理体系的前提。
让我们开始。
5.1 CSS 隔离三策略:Shadow DOM、Scoped CSS、Dynamic Stylesheet
5.1.1 问题的本质
CSS 隔离需要解决的核心问题只有一个:如何让子应用的样式只作用于子应用自身的 DOM,不影响主应用和其他子应用?
这个问题可以被分解为两个方向:
- 子应用的样式不泄漏出去(outward isolation)——子应用定义的
.container不应该影响主应用的.container - 外部的样式不渗透进来(inward isolation)——主应用的全局 reset 样式不应该破坏子应用的内部布局
typescript
// CSS 隔离的两个方向
interface CSSIsolation {
outwardIsolation: boolean; // 子应用样式不泄漏
inwardIsolation: boolean; // 外部样式不渗透
}
// 三种策略的隔离能力对比
const strategies: Record<string, CSSIsolation> = {
shadowDOM: { outwardIsolation: true, inwardIsolation: true },
scopedCSS: { outwardIsolation: true, inwardIsolation: false },
dynamicStylesheet: { outwardIsolation: true, inwardIsolation: false },
};
// Shadow DOM 是唯一能同时做到双向隔离的方案
// 但它的代价也最大——这就是架构权衡的经典案例下图展示了三种 CSS 隔离策略的架构对比与隔离能力差异:
乾坤提供了两个配置项来控制 CSS 隔离策略:
typescript
// 乾坤的 CSS 隔离配置
registerMicroApps([
{
name: 'sub-app',
entry: '//localhost:7100',
container: '#container',
activeRule: '/sub-app',
},
], {
// 方式一:严格隔离 —— 使用 Shadow DOM
// 对应源码中的 strictStyleIsolation
sandbox: {
strictStyleIsolation: true,
},
// 方式二:实验性隔离 —— 使用 Scoped CSS
// 对应源码中的 experimentalStyleIsolation
sandbox: {
experimentalStyleIsolation: true,
},
});
// 两者不能同时开启
// 如果都不开启,则使用 Dynamic Stylesheet(默认策略)5.1.2 策略一:Shadow DOM(strictStyleIsolation)
Shadow DOM 是 Web Components 标准的一部分,它提供了浏览器原生的 DOM 和样式隔离能力。乾坤的 strictStyleIsolation 选项正是利用了这个能力。
原理:将子应用的整个 DOM 树包裹在一个 Shadow DOM 中。Shadow DOM 内部的样式天然不会泄漏到外部,外部的样式也无法渗透进来(除了可继承的 CSS 属性)。
来看乾坤源码中 strictStyleIsolation 的实现:
typescript
// qiankun/src/loader.ts(简化)
function createElement(
appContent: string,
strictStyleIsolation: boolean,
scopedCSS: boolean,
appInstanceId: string,
): HTMLElement {
const containerElement = document.createElement('div');
containerElement.innerHTML = appContent;
const appElement = containerElement.firstChild as HTMLElement;
if (strictStyleIsolation) {
// 核心:如果开启了严格样式隔离
if (!supportShadowDOM) {
console.warn(
'[qiankun]: strictStyleIsolation is not supported in this browser.'
);
} else {
const { innerHTML } = appElement;
appElement.innerHTML = '';
let shadow: ShadowRoot;
if (appElement.attachShadow) {
// 创建 Shadow DOM
shadow = appElement.attachShadow({ mode: 'open' });
} else {
// 兼容旧版 API
shadow = (appElement as any).createShadowRoot();
}
// 将子应用的 HTML 内容放入 Shadow DOM 中
shadow.innerHTML = innerHTML;
}
}
// ... scopedCSS 的处理逻辑(见下一节)
return appElement;
}这段代码的关键步骤是:
- 创建一个容器
div,将子应用的 HTML 内容放入 - 调用
attachShadow({ mode: 'open' })创建 Shadow Root - 将原本的 innerHTML 移入 Shadow Root
这样,子应用的所有 DOM 节点和样式都运行在 Shadow DOM 内部,与外界天然隔离。
html
<!-- 隔离后的 DOM 结构 -->
<div id="__qiankun_microapp_wrapper_for_sub_app__">
#shadow-root (open)
<div id="sub-app-container">
<style>
.container { margin: 0 auto; }
/* 这个样式被锁在 Shadow DOM 内部 */
/* 外面的 .container 完全不受影响 */
</style>
<div class="container">子应用内容</div>
</div>
</div>