Skip to content

第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,不影响主应用和其他子应用?

这个问题可以被分解为两个方向:

  1. 子应用的样式不泄漏出去(outward isolation)——子应用定义的 .container 不应该影响主应用的 .container
  2. 外部的样式不渗透进来(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;
}

这段代码的关键步骤是:

  1. 创建一个容器 div,将子应用的 HTML 内容放入
  2. 调用 attachShadow({ mode: 'open' }) 创建 Shadow Root
  3. 将原本的 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>

基于 VitePress 构建