CSS 隔离与 qiankun 样式问题总结

16 阅读4分钟

一、CSS Modules 能做什么

类选择器ID 选择器基于文件路径 + 类名做 hash,保证组件间类名不冲突。

// button.module.scss
.button { color: red; }

编译后:

.button_a1b2c3 { color: red; }

JS 侧同步生成映射:

const styles = { button: 'button_a1b2c3' }

<button className={styles.button}>Click</button>

编译规则

选择器类型是否加 hash示例
类选择器 .button.button_a1b2c3
ID 选择器 #header#header_d4e5f6
元素选择器 h1❌ 原样输出h1
通配符 *❌ 原样输出*
:root❌ 原样输出:root

二、CSS Modules 解决不了的问题

问题一:全局样式是裸的

全局样式的目的本来就是作用于整个页面,用的都是元素选择器,CSS Modules 不处理。

// global.scss(不是 .module.scss)
* { box-sizing: border-box; }
body { margin: 0; }
h1 { font-size: 24px; }

如果强行放进 .module.scss,编译后变成无效选择器,完全不生效:

*_a1b2c3 { box-sizing: border-box; }  /* 不存在的选择器 */
body_x9y8z7 { margin: 0; }            /* 不存在的选择器 */

微前端里的影响: 多个子应用的全局样式同时注入同一页面,互相覆盖,谁后加载谁生效。

<head>
  <style>/* 子应用 A */ body { margin: 0; } h1 { color: red; }</style>
  <style>/* 子应用 B */ body { margin: 10px; } h1 { color: blue; }</style>
  <!-- B 覆盖了 A -->
</head>

问题二:第三方库样式是全局的

import 'antd/dist/antd.css'  // 全局注入,CSS Modules 管不到

两个子应用引入了不同版本的 antd,样式叠在一起必然冲突。

问题三:弹窗挂载到 body 的问题

antd Modal、Drawer 等组件为了解决 z-index 层叠和 overflow: hidden 裁剪问题,会把弹窗 DOM 挂到 document.body 下,而不是组件内部。

<body>
  <div id="app">
    <button>打开弹窗</button>
    <!-- 弹窗不在这里 -->
  </div>

  <div class="ant-modal-root">  ← 弹窗挂在这里
    弹窗内容
  </div>
</body>

CSS Modules 改的是类名,和挂载位置无关,这个问题它解决不了。


三、qiankun 的两种 CSS 隔离方案及问题

方案一:Shadow DOM(strictStyleIsolation: true

将子应用挂载节点包在 Shadow DOM 内,样式物理隔离,不穿透边界。

start({ strictStyleIsolation: true })

问题:弹窗逃出 Shadow DOM

<body>
  <div data-qiankun="app-name">
    #shadow-root          ← Shadow DOM 边界
      子应用内容
      <button>打开 Modal</button>
  </div>

  <div class="ant-modal-root">  ← antd 把弹窗挂到了 Shadow DOM 外面
    弹窗内容                     ← Shadow DOM 内的样式覆盖不到这里,样式丢失
  </div>
</body>

方案二:Scoped CSS(experimentalStyleIsolation: true

qiankun 扫描子应用所有 style 标签,给每条 CSS 规则动态加属性前缀。

start({ experimentalStyleIsolation: true })
/* 原始 */
.button { color: red; }

/* 处理后 */
div[data-qiankun="app-name"] .button { color: red; }

三个边界问题:

① 动态插入的样式覆盖不到

qiankun 在 mount 时扫描,antd 弹窗是用户点击后才动态插入 style,时机在扫描之后:

mount 时扫描 style 标签
  ↓
用户点击按钮
  ↓
antd 动态插入弹窗样式  ← qiankun 不知道,没有加前缀,全局生效

:root 语义被破坏

:roothtml 元素,是整个页面最顶层的节点,常用来定义全局 CSS 变量:

:root { --primary-color: #1890ff; }
.button { color: var(--primary-color); }

加前缀后:

/* :root 不可能是 div 的后代,这个选择器永远匹配不到任何元素 */
div[data-qiankun="app-name"] :root { --primary-color: #1890ff; }

/* 变量取不到值,主题色失效 */
div[data-qiankun="app-name"] .button { color: var(--primary-color); }

@keyframes 引用断裂

/* 处理后:keyframes 名加了前缀 */
@keyframes app-name-fadeIn { from { opacity: 0; } to { opacity: 1; } }

/* 但 animation 引用处没有同步改名,找不到动画,直接失效 */
div[data-qiankun="app-name"] .modal { animation: fadeIn 0.3s ease; }

为什么不修这些问题:

  • :root 即使替换成容器选择器,挂到 body 的弹窗仍取不到变量,问题没有根本解决
  • @keyframes 需要完整 CSS AST 解析,引入解析器会显著增大包体积、增加 mount 耗时
  • 动态样式可以用 MutationObserver 监听,但插入和回调之间有时间差会闪烁,且 CSSStyleSheet API 插入的样式监听不到

本质原因:在应用层用正则模拟 CSS 作用域,天花板就在这里,要彻底解决需要浏览器渲染层支持(即 Shadow DOM)。


四、实际项目的解决方案

方案一:指定弹窗挂载容器(解决 Shadow DOM 弹窗问题)

import { ConfigProvider } from 'antd'

function App() {
  return (
    <ConfigProvider
      getPopupContainer={() => document.querySelector('div[data-qiankun="app-name"]')}
    >
      <YourApp />
    </ConfigProvider>
  )
}

❌ 只解决 antd,其他第三方库需单独处理。

方案二:postcss 构建时自动加前缀(推荐)

不依赖 qiankun 运行时处理,构建产物天然带前缀:

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-prefix-selector')({
      prefix: '.app-finance',
      exclude: [/global.scss/],  // 排除真正需要全局的样式
    }),
  ],
}
/* 原始 */
.button { color: red; }
body { margin: 0; }

/* 构建后 */
.app-finance .button { color: red; }
.app-finance body { margin: 0; }  /* body 也被限定在容器内 */

弹窗、动态样式、@keyframes 都不受影响,因为前缀是编译期加的,不依赖运行时扫描。

:root 变量需要改成挂在容器选择器上,团队需要统一规范。

方案三:CSS Modules + postcss 组合(务实选择)

CSS Modules        → 解决子应用内部组件间类名冲突
postcss 前缀       → 解决子应用之间全局样式冲突
getPopupContainer  → 解决第三方库弹窗样式丢失

关闭 qiankun 自带的 CSS 沙箱,完全由构建工具保证隔离,运行时没有额外开销,边界情况可控。


五、总结对比

方案解决组件间冲突解决应用间冲突弹窗问题运行时开销
CSS Modules
qiankun Shadow DOM❌ 样式丢失
qiankun Scoped CSS✅ 大部分⚠️ 部分失效
postcss 前缀
CSS Modules + postcss✅(需配合 getPopupContainer)