qiankun v3 样式隔离内部实现解析:@scope、blob-link 与 CSSOM 拦截机制
【免费下载链接】qiankun📦 🚀 Blazing fast, simple and complete solution for micro frontends.项目地址: https://gitcode.com/gh_mirrors/qi/qiankun
本文面向 qiankun 维护者与进阶使用者,系统拆解 v3 中样式隔离(
sandbox.styleIsolation)的完整实现管线:它如何基于原生 CSS@scope对行内<style>、外部<link rel="stylesheet">与运行时 CSSOM 注入三条路径分别改写样式,以及由此带来的缓存策略、preload 重写与边界限制。读完本文,你将理解为什么 v3 弃用 2.x 的 Shadow DOM 方案、何时可以安全启用该选项,以及排查"启用后应用失样式"问题的完整思路。面向使用者的能力边界与开启方式见 样式隔离概念,一步步开启指南见 启用 CSS 样式隔离。
样式隔离的目标是阻止微应用的 CSS 泄漏到主应用或其兄弟应用中。在 qiankun v3 中,这是一个可选开启、按应用粒度生效的运行时机制,其实现建立在浏览器原生 CSS@scope规则之上——而不是 Shadow DOM。开启后,微应用携带的每一份样式表都会被改写,使其中规则只在该应用容器内部命中。
机制全貌:什么是它、什么不是它
在应用的配置中设置sandbox: { styleIsolation: true },qiankun 就会把该应用的 CSS 包装进一个绑定到应用容器的@scope块:
@scope ([data-name="your-app"]) { /* 该应用的规则,已被改写 */ }作用域根(scope root)恒定为[data-name="<appName>"]。qiankun 会给每个应用容器打上与注册应用名相等的data-name属性,并据此推导选择器。这一行为不可自定义——没有选项允许传入你自己的 scope root。容器属性打标的实现位于 packages/sandbox/src/core/sandbox/container.ts:挂载时写入container.dataset.name = appName,卸载时若不存在其他同名实例则恢复原属性或移除,从而保证data-name与应用生命周期严格同步。
由于包装发生在 CSS 层面而非将应用挂载进 shadow tree,微应用的 DOM 仍然留在主文档中:全局库、portal、document级查询都按 JS 沙箱 预期的方式继续工作。
隔离在意图上是单向的:它阻止微应用声明的规则作用到容器之外,但不会"沙箱化"主应用或浏览器默认样式表对微应用内部的影响。完整分支流程如下:
样式隔离默认关闭。如果你从不设置sandbox.styleIsolation,<style>与<link>节点会原样穿过 loader,不做任何处理。
v3 与 2.x 的本质差异
v3 的样式隔离就是上文这套@scope+ blob-link 机制,由一个布尔开关控制。2.x 的sandbox.strictStyleIsolation与sandbox.experimentalStyleIsolation(基于 Shadow DOM)在 v3 中不存在,唯一的旋钮是sandbox.styleIsolation。从 2.x 迁移的细节见 从 qiankun 2.x 迁移。
路径一:行内<style>的重写管线
对于行内<style>元素,qiankun 读取其textContent,进行变换,再把作用域化结果写回同一节点。除包裹@scope之外,变换还做了几件单纯包装会做错的事。核心实现位于 packages/shared/src/assets-transpilers/style.ts:
@font-face与@namespace被提升(hoist)出@scope块并保持全局。作用域化@font-face会破坏字体加载,@namespace必须是文档级的。源码用extractAtRules先把这两类规则抽取出来,最后再拼回作用域块之前(见transpileStyleTextSync)。@keyframes以每应用前缀重命名——格式为__qk_<appName>_<name>——并且所有animation/animation-name引用都会同步改写。这是因为@scope只作用域化选择器,不会作用域化全局的 keyframe 命名空间;两个都定义了spinkeyframe 的应用若不重命名会互相覆盖。源码中prefixKeyframes用QIANKUN_KEYFRAMES_PREFIX = '__qk_'拼接前缀,并同时重写animation-name长属性与animation简写属性(对animation-*长属性有守卫,避免误伤,见 style.ts)。- 行内样式的相对
url(...)不做 rebase。当前行内路径不会向 transformer 传入样式表 base URL。当宿主与微应用不共享同一 document base 时,请使用绝对、data:或blob:URL。 @import被递归内联。每个被导入的样式表都通过应用装饰后的fetch获取、执行同样变换后拼接进来,并用 visited 集合去重(重复导入会告警并跳过)。由于该路径不基于微应用 entry 解析相对地址,行内样式中请使用绝对 import URL。递归内联与相对路径解析实现在inlineImports中(style.ts)。
因为内联@import可能产生网络往返,qiankun 会先同步清空<style>的textContent,待所有内容解析完成后才填入作用域化 CSS(见 style.ts)。这避免了在 fetch 窗口期未作用域的源样式全局生效。
路径二:外部<link rel="stylesheet">的 blob-link 方案
原生@scope只能包装你掌控的 CSS 文本,而浏览器对外部样式表是不透明加载的——没有钩子可以在其到达时包装它。因此开启样式隔离后,qiankun 会阻止浏览器原生加载<link>,自己接管 fetch,即代码库所称的blob-link 方案,实现在 packages/shared/src/assets-transpilers/link.ts:
- 将
href基于 base URL 解析,然后移除href属性并把原始地址存入data-href。没有href,浏览器永远不会加载未作用域的样式表。 - 通过应用装饰后的
fetch获取 CSS,再对它执行与行内样式相同的@scope包装变换。 - 在同一
<link>元素上以blob:URL 提供服务:包装后的 CSS 成为Blob,其 object URL 被重新设回元素的href。
与行内路径不同,外部样式表的变换会收到解析后的样式表 URL 作为 base。因此相对url(...)和@import引用都会先基于外部样式表 URL 解析,再应用作用域化。
节点身份是被刻意保留的——qiankun 只交换href,从不替换元素。这让所有原生<link>语义免费保持有效:media、disabled、title以及document.styleSheets中的条目始终存活;流式 loader 的"pending 样式表阻塞后续脚本"簿记仍能看到一个正常的 pending link(blob href 落地时触发load);应用附加在动态注入<link>上的onload/onerror处理器也继续工作。
如果 fetch 或变换失败,blobhref 永远不会被设置——元素自身不会发出任何事件。qiankun 会在该 link 上手动派发error事件并丢弃样式表(见 link.ts),而不是回退为未作用域加载。丢弃是刻意选择:无法被作用域化的样式表不允许全局泄漏。
缓存与去重:变换后的样式表先按 URL 缓存,再按应用作用域键(appName:scopeRoot)缓存;同一 URL 的并发 fetch 会被去重(pendingFetches共享同一个 Promise)。因此被多个应用共享的同一外部样式表,只会按不同 scope root 的数量各 fetch 与变换一次。link.ts还暴露了clearStylesheetCache()与getStylesheetCacheStats()用于测试与监控(link.ts)。
路径三:运行时 CSSOM 的 insertRule 拦截
运行时以编程方式插入的样式从不经过 loader,所以 qiankun 在 CSSOM 层拦截它们。当样式隔离激活时,CSSStyleSheet.prototype.insertRule被 monkey-patch(引用计数:只要存在存活的样式隔离应用就安装,最后一个卸载时才移除,见 packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts)。若样式表拥有节点(ownerNode)携带样式隔离配置,传入的规则文本在到达原生insertRule前会被作用域化——包裹@scope并做同样的 keyframe 重命名。transpileStyleRule的同步实现见 style.ts。
这条同步路径会跳过已@scope包裹的规则(避免重建时双重包裹),并保持@font-face/@namespace全局,与静态变换保持一致。这正是 CSS-in-JS 库与运行时构建样式表的框架能够与其他样式一并被作用域化的原因。patchCSSOM在卸载时还保留宿主在 qiankun 初始化后自行安装的补丁(仅在当前补丁仍是被安装的那个时才恢复原生实现)。
Preload 重写:让预热请求不被浪费
通过<link rel="preload" as="style">预热的响应只能被原生样式表请求复用。开启样式隔离后,transpiler 通过fetch()消费样式表,原始 preload 将白费。因此 qiankun 会把该 link 改写为as="fetch"并加上crossorigin="anonymous"(除非它已使用use-credentials),使随后的fetch()能复用预热响应。该逻辑位于 link.ts,注释明确说明其与 script/modulepreload 改写同一 rationale:保持 preload 缓存可命中。
另外,只要 ESM 沙箱 处于激活状态,qiankun 就会把rel="modulepreload"改写为rel="preload" as="fetch",因为引擎导入的是改写后的 blob URL 而非原始模块 URL。此改写不依赖样式隔离;原始 modulepreload 的凭据行为通过crossorigin设置保留(link.ts)。
需求与限制
::: warning 需要浏览器原生支持 CSS@scope实现中没有任何 polyfill 与回退方案,完全依赖浏览器支持 CSS@scope规则。在不支持@scope的浏览器中,包裹规则是惰性的,样式不会被隔离。@scope是浏览器较新引入的特性,依赖它之前请对照你的目标浏览器矩阵验证支持情况。 :::
::: warning 外部样式表必须可被 CORS 获取 因为外部样式表通过fetch重新获取并以blob:URL 提供,跨域样式表必须返回正确的 CORS 头。若无法获取,qiankun 会丢弃它——样式表静默消失(伴随一条 console 警告)而不是以未作用域状态加载。请为微应用样式表开启 CORS,否则隔离应用会以无样式状态渲染。 :::
::: info 已知边界场景
@font-face冲突:字体规则被刻意保持全局以保证字体正确加载。两个声明相同font-family名称的应用因此可能冲突。请为每个应用使用不同的 font-family 名称。- 动态拼接的 keyframe 名称:keyframe 重命名是静态文本变换。如果 JS 在运行时用字符串拼接构造动画名称(而不是在 CSS 中字面书写),该引用不会被改写,动画可能无法解析。 :::
::: tip 与 qiankun 2.x 的差异 v3 样式隔离就是本文描述的@scope+ blob-link 机制,由一个布尔值切换。2.x 的sandbox.strictStyleIsolation与sandbox.experimentalStyleIsolation(基于 Shadow DOM)在 v3 中不存在,唯一旋钮是sandbox.styleIsolation。迁移参考 从 qiankun 2.x 迁移。 :::
容器外的内容不在作用域内
菜单、对话框、tooltip 等渲染到document.body下而非微应用容器内的 portal,位于 scope root 之外,应用的作用域选择器不会命中它们。建议将 portal root 放在props.container内,或显式样式化该外部表面。作用域按应用名(name)而不是实例句柄键控:复用同一name的并发实例共享同一作用域选择器,当它们的 CSS 需要彼此隔离时请使用不同的名称。完整用户侧能力说明见 样式隔离概念。
公共开关:sandbox.styleIsolation
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sandbox.styleIsolation | boolean | false | 通过@scope包裹启用运行时 CSS 隔离。启用后,微应用的所有样式都被作用域化到其容器([data-name="<appName>"])。 |
styleIsolation是应用配置中sandbox对象内的按应用字段。配置的解析发生在 packages/qiankun/src/core/loadApp.ts:typeof sandboxCfg === 'object' ? { enabled: true, styleIsolation: Boolean(sandboxCfg.styleIsolation) } : sandboxCfg,随后作为styleIsolation透传给 sandbox 控制器与 node transformer(loadApp.ts)。将它与styleIsolation作用域根生成逻辑(container.ts:scopeRoot: \[data-name="${appName}"]`)对照阅读,可以完整看到从配置项到最终@scope` 选择器的全链路。
作为loadMicroApp的第二个参数传入:
import { loadMicroApp } from 'qiankun'; const container = document.getElementById('subapp-viewport'); if (!container) throw new Error('subapp-viewport not found'); const microApp = loadMicroApp( { name: 'react-app', entry: '//localhost:7101', container, }, { sandbox: { styleIsolation: true }, }, );启用与验证:从开关到确认生效
在路由驱动模式下,把配置放入应用注册对象的configuration字段;React 与 Vue 的<MicroApp>组件则通过settingsprop 传入相同配置(见 启用 CSS 样式隔离)。启用后建议按以下步骤验证:
- 在宿主与微应用中添加同 class 的测试元素;
- 在微应用 CSS 中给该 class 一个明显的样式;
- 确认只有微应用容器内的元素获得该样式;
- 卸载应用并确认其容器与动态插入的样式被清理。
若启用后应用变成无样式状态,优先排查两件事:浏览器是否支持@scope,以及 CORS 是否拦截了某个外部 CSS 请求。资源加载失败的捕获方式见 处理加载与运行时错误。完整配置字段列表见 AppConfiguration,loadMicroApp的完整签名见 loadMicroApp。
【免费下载链接】qiankun📦 🚀 Blazing fast, simple and complete solution for micro frontends.项目地址: https://gitcode.com/gh_mirrors/qi/qiankun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考