qiankun 样式隔离完全指南:用原生 CSS @scope 为微前端划定样式边界
【免费下载链接】qiankun📦 🚀 Blazing fast, simple and complete solution for micro frontends.项目地址: https://gitcode.com/gh_mirrors/qi/qiankun
微前端架构中最常见也最棘手的问题之一,就是子应用(micro-app)的 CSS 泄漏到宿主页面或兄弟应用,造成全局样式互相污染。qiankun v3 提供了一套基于浏览器原生 CSS@scope的运行时样式隔离机制:通过sandbox.styleIsolation一个开关,即可将子应用声明的所有样式规则限定在其容器内,无需 Shadow DOM。本文将从概念模型、覆盖范围、边界与限制、启用方式到源码级实现原理,完整讲解这套机制,读完你既能正确地在项目中开启样式隔离,也能理解其底层工作方式并规避常见坑点。
什么是 qiankun 样式隔离
样式隔离(Style Isolation)的目标是:阻止子应用声明的 CSS 匹配到其容器之外的元素。qiankun 的这套机制有四个关键特征:
- 按需开启(opt-in):默认关闭,只有显式配置才生效;
- 按应用粒度(scoped per app):每个应用独立决定是否隔离,隔离与未隔离的应用可以共存;
- 基于浏览器原生 CSS
@scope:不做任何 polyfill,也不依赖 Shadow DOM; - 挂在 sandbox 对象内:因为运行时动态注入的样式依赖沙箱的 DOM 拦截能力——如果只隔离 CSS 而没有 JS 沙箱,动态插入的样式会悄悄泄漏出去。
该选项位于sandbox配置对象中:JS 沙箱默认开启,而sandbox.styleIsolation默认值为false(见 AppConfiguration 中的配置参考)。
核心模型:一个 @scope 块
开启sandbox: { styleIsolation: true }后,qiankun 会把应用的所有样式规则限制在以其应用名(app name)标识的容器内。概念上,应用的样式变成这样:
@scope ([data-name="catalog"]) { /* 该微应用的所有规则 */ }qiankun 会给每个应用容器打上等于注册应用名的data-name属性,并从这个属性推导出 scope root 选择器。scope root 固定为[data-name="<appName>"],不可自定义——没有任何配置项允许你传入自己的作用域根选择器。
需要特别强调:应用仍然停留在宿主文档中,不会被移入 Shadow DOM。因此既有的文档级集成(如document.querySelector、全局事件、门户等)依然可以正常工作,但需要遵循下面将要讲到的"单向边界"约束。
隔离覆盖范围
开启样式隔离后,qiankun 覆盖以下三类样式来源:
| 样式来源 | 隔离开启时的行为 |
|---|---|
内联<style> | 其规则被限定到应用容器内。 |
外部<link rel="stylesheet"> | qiankun 先读取样式表内容并完成 scoping,再使其生效。 |
| 运行时插入的规则与常见 CSS-in-JS 输出 | 与子应用关联的规则在插入时即被 scoping。 |
两个容易踩坑的细节:
- 外部样式表内的相对资源 URL(如
url(...)、@import)会以该样式表自身的 URL 为基准进行解析,因此可以安全使用相对路径; - 内联
<style>内容不会针对微应用入口做 URL 重写。当宿主与微应用文档 base URL 不同时,内联样式中的相对资源请改用绝对 URL(或data:/blob:URL)。
另外,如果某个样式表无法被安全地 scoping(例如跨域获取失败),qiankun不会回退为全局应用未 scoping 的样式,而是直接丢弃该样式表——这是刻意设计的安全选择,详见下文"外部<link>的 blob-link 方案"。
一条单向边界
样式隔离阻止的是微应用的规则向外泄漏(leak out)。它不阻止宿主样式、继承属性(inherited properties)、浏览器默认样式以及共享的 CSS 自定义属性向内流入(flow in)子应用。
因此需要注意:
- 宿主的全局选择器仍然可能命中微应用内部的元素——这是设计如此,不是 bug;
- 继承的
color、font等属性依然会从宿主传给子应用根元素; - 共享的 CSS 自定义属性(variables)同样可以穿透边界。
Portal 需要特别处理。菜单、对话框、tooltip 等如果渲染在document.body下而不是微应用容器内,就处于 scope root 之外,应用的 scoped 选择器将无法匹配到它们。推荐做法是:将 portal 的挂载根放在props.container之内,或者为容器外的这块界面显式编写样式。
隔离是按应用配置的。隔离的应用与未隔离的应用可以共存,但未隔离应用的 CSS 仍然可能影响整个页面。
要求与限制
在启用该选项前,请逐条核对以下硬性要求与已知限制:
- 必须支持原生 CSS
@scope。qiankun 不提供 polyfill,也没有任何降级回退方案。在@scope未被支持的浏览器中,包裹规则是惰性的(inert),样式不会被隔离。@scope是较新的 CSS 特性,务必对照你的目标浏览器矩阵(browser matrix)确认后再启用。 - 跨域样式表需要 CORS。qiankun 必须能够读取样式表的 CSS 内容。无法被 fetch 的样式表会被丢弃而非全局泄漏。请确保微应用服务器以及第三方样式表来源返回正确的
Access-Control-Allow-Origin响应头,否则隔离应用会以无样式状态渲染(并在控制台输出警告)。 @font-face保持全局。字体规则被刻意保留在全局,以便字体能正常加载。两个应用声明相同的font-family名称可能产生冲突,请为每个应用使用应用特有的font-family名称。- 关键帧(keyframes)名称在声明于 CSS 中时会被隔离。qiankun 会以应用为前缀重命名
@keyframes并同步改写animation/animation-name引用;但如果动画名称是在 JavaScript 中动态拼接构造的(而非在 CSS 中字面书写),则无法随样式表一并改写,动画可能无法解析(不生效)。 - 应用容器之外的内容都在作用域之外。这包括 portal,以及应用代码故意移动出去的节点。
- 作用域以应用名(name)为键,而非实例句柄。并发存在的多个实例如果复用了同一个
name,它们共享同一个 scope 选择器。当这些实例的 CSS 需要彼此隔离时,请为它们使用不同的应用名。
如何启用样式隔离
在需要隔离的应用上设置该选项即可。以手动加载为例(loadMicroApp的第二个参数):
import { loadMicroApp } from 'qiankun'; const container = document.getElementById('micro-app'); if (!container) throw new Error('micro-app container not found'); const microApp = loadMicroApp( { name: 'catalog', entry: 'https://catalog.example.com', container, }, { sandbox: { styleIsolation: true }, }, ); // 当页面不再展示该应用时: await microApp.unmount();其他接入方式同样支持:
- React / Vue 的
<MicroApp>组件:通过其settingsprop 传入相同配置(settings: { sandbox: { styleIsolation: true } }); - 路由驱动模式(
registerMicroApps):把sandbox: { styleIsolation: true }放在该应用的configuration字段中。
从源码看,loadApp.ts 会把sandbox配置解构并规范化:typeof sandbox === 'object' ? { enabled: true, styleIsolation: Boolean(sandboxCfg.styleIsolation) } : sandboxCfg,即styleIsolation最终被布尔化为开关,并随沙箱配置下发给样式转换管线。
验证隔离结果
启用后,建议按以下步骤端到端验证:
- 在宿主和微应用中各添加一个同 class 的测试元素;
- 在微应用 CSS 中给该 class 一个明显的样式;
- 确认只有微应用容器内的元素获得该样式;
- 卸载应用,确认其容器和动态插入的样式都被清理干净。
如果启用隔离后应用反而失去样式,优先检查两点:浏览器是否支持@scope;CORS 是否拦截了某个外部 CSS 请求。
源码级原理:样式重写管线
本节对应维护者视角的内部文档 style isolation internals,以及实际实现 style.ts 与 link.ts。
qiankun v3 的样式隔离是一个运行时机制:开启后,子应用携带的每张样式表都会被重写,使其规则只在应用容器内匹配。核心转换入口是transpileStyleText,它先提取需要保持全局的 at-rule,再做 keyframes 重命名,最后把剩余内容包进@scope块:
@scope ([data-name="your-app"]) { /* 应用规则,已重写 */ }样式隔离默认关闭。如果不设置sandbox.styleIsolation,<style>与<link>节点会原样通过加载器,不做任何处理。
内联<style>:textContent 重写
对于内联<style>元素,qiankun 读取其textContent,执行转换,再把 scoped 后的结果写回同一个节点。除了外层@scope包裹,转换还做了几件纯包裹做不对的事:
@font-face与@namespace被提升(hoist)出@scope块并保持全局。把@font-face放进 scope 会破坏字体加载,@namespace必须处于文档级,因此两者都会被提升到样式表顶部(源码中extractAtRules负责抽取、transpileStyleTextSync负责拼接)。@keyframes被按应用加前缀重命名,规则为__qk_<appName>_<name>(前缀常量QIANKUN_KEYFRAMES_PREFIX = '__qk_',见 style.ts)。同时所有animation/animation-name引用都会被改写匹配。这样两个应用都定义spin关键帧时不会互相覆盖——因为@scope只能 scoping 选择器,无法 scoping 全局的 keyframes 命名空间。- 内联样式的相对
url(...)不会 rebase。当前内联样式路径不会向转换器传入样式表 base URL,因此当宿主与微应用共享同一文档 base 时无碍,否则请使用绝对、data:或blob:URL。 @import被递归内联。每个被导入的样式表都通过应用装饰后的fetch拉取、以相同方式转换后拼接进来,并用 visited 集合去重。内联样式路径同样不会把导入 URL 解析到微应用入口,请使用绝对导入 URL。
由于内联@import可能需要网络往返,qiankun 会先同步清空<style>的 textContent,等所有内容解析完成后再填回 scoped CSS——这就避免了在 fetch 窗口期内未 scoped 的原始样式先全局生效。
外部<link rel="stylesheet">:blob-link 方案
原生@scope只能包裹你能控制的 CSS 文本,而浏览器加载外部样式表是"不透明"的——没有钩子可以在其到达时包裹它。因此在样式隔离下,qiankun 会阻止浏览器原生加载<link>,改为自己接管 fetch。实现细节在 link.ts 中:
- 解析
href(相对于 base URL),然后移除href属性,把原始地址暂存在data-href中。没有href,浏览器就永远不会加载未 scoped 的样式表; - 通过应用装饰后的
fetch获取 CSS,执行与内联样式相同的@scope包裹转换(这一次会传入已解析的样式表 URL 作为 base,因此相对url(...)与@import都能正确解析); - 以
blob:URL 形式喂回同一个<link>元素:转换后的 CSS 变成Blob,其对象 URL 重新设置为元素的href。
节点身份(node identity)被刻意保留——qiankun 只换href,从不换元素。这让所有原生<link>语义免费保留:media、disabled、title以及document.styleSheets中的条目都保持有效;流式加载器"待处理样式表阻塞后续脚本"的记账逻辑仍然看到一个正常的 pending link(blob href 落地时load事件触发);应用为动态注入的<link>挂载的onload/onerror处理器也继续工作。
如果 fetch 或转换失败,不会设置任何 blob href——元素本身不会发出事件。此时 qiankun 手动在 link 上派发一个error事件并丢弃该样式表,而不是回退为未 scoped 加载。丢弃是刻意选择:无法被 scoped 的样式表不允许泄漏到全局。
此外,转换后的样式表会先按 URL 缓存,再按 (appName, scopeRoot) 缓存键缓存;同一 URL 的并发 fetch 会被去重(pendingFetches映射)。因此同一个外部样式表被多个应用共享时,只会被 fetch 和转换与不同 scope root 数量相等的次数。
运行时 CSSOM:insertRule 拦截
程序化插入的样式永远不会经过加载器,因此 qiankun 在 CSSOM 层面对其进行拦截。当样式隔离生效时,CSSStyleSheet.prototype.insertRule会被 monkey-patch(带引用计数:只要还有任一样式隔离应用存活就保持安装,最后一个应用卸载时移除)。实现位于 forStandardSandbox.ts 的patchCSSOM中:
const patchedInsertRule = function insertRule(this: CSSStyleSheet, rule: string, index?: number): number { const ownerNode = this.ownerNode as HTMLElement | null; if (ownerNode) { const config = resolveStyleOwnerConfig(ownerNode); if (config?.styleIsolation) { const scopedRule = transpileStyleRule(rule, config.styleIsolation); return nativeInsertRule.call(this, scopedRule, index); } } return nativeInsertRule.call(this, rule, index); };这条同步路径(transpileStyleRule)会跳过已经@scope包裹的规则以避免重复包裹,并让@font-face/@namespace保持全局,与静态转换保持一致。正是它保证了 CSS-in-JS 库和框架在运行时构建的样式表同样被 scoping。
Preload 重写
通过<link rel="preload" as="style">预热的响应只能被原生样式表请求复用。样式隔离开启后,转换管线改用fetch()消费样式表,原始 preload 会浪费。因此 qiankun 会把该 link 重写为as="fetch",并在非use-credentials时补上crossorigin="anonymous",让后续fetch()能复用预热响应(见 link.ts 中postProcessPreloadLink)。
另外,当 ESM 沙箱激活时,qiankun 会把rel="modulepreload"重写为rel="preload" as="fetch"——因为引擎导入的是重写后的 blob URL 而非原始模块 URL。这条重写不依赖样式隔离,属于 ESM 沙箱的配套行为。
与 qiankun 2.x 的差异
v3 的样式隔离就是上面描述的@scope+ blob-link 机制,由单个布尔开关控制。qiankun 2.x 中的sandbox.strictStyleIsolation与sandbox.experimentalStyleIsolation(基于 Shadow DOM)在 v3 中已不存在,唯一的旋钮就是sandbox.styleIsolation。从 2.x 迁移时请参考 Migrate from qiankun 2.x。
配置参考
| Option | Type | Default | Description |
|---|---|---|---|
sandbox.styleIsolation | boolean | false | 通过@scope包裹启用运行时 CSS 隔离。启用后,微应用的所有样式都被 scoping 到其容器([data-name="<appName>"])内。 |
styleIsolation是应用配置中sandbox对象内的按应用字段,作为第二个参数传给 loadMicroApp;完整字段列表见 AppConfiguration,任务导向的实操步骤见 Enable CSS style isolation。
测试与回归保障
仓库为样式隔离提供了完整的测试覆盖,可作为验证和参考:
- 端到端测试style-isolation.spec.ts 包含对照组(未开启时子应用全局 CSS 泄漏进宿主)与开启后(CSS 停留在容器内)的断言,并覆盖无 body、多脚本、patch-append 等边界场景;
- 单元测试link.test.ts 与 link-performance.test.ts 验证外部样式表的转换与缓存行为;
- 沙箱侧测试attribution.test.ts 与 lifecycle.test.ts 验证
insertRule按样式表当前 DOM 位置 scoping,以及 patch 的安装/卸载生命周期。
一句话总结:sandbox.styleIsolation: true用浏览器原生@scope把子应用 CSS 关进以应用名命名的容器里,方向单一、无 polyfill、无回退;启用前核对浏览器支持与 CORS,启用后注意 portal、@font-face与动态 keyframes 三个边界,即可获得干净可靠的微前端样式隔离。
【免费下载链接】qiankun📦 🚀 Blazing fast, simple and complete solution for micro frontends.项目地址: https://gitcode.com/gh_mirrors/qi/qiankun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考