源自 | MSEdgeExplainers
译者 | 王强
编辑 | Yonie
CSS Modules V1 是 Microsoft 提出的一项新建议,它是 ES Script 模块系统的一项扩展。在 CSS 模块的帮助下,Web 开发人员可以将 CSS 加载到组件定义中(例如 import styles from "styles.css"),并且与其他模块类型无缝对接。
为什么我们需要 CSS 模块?
ES6 规范引入了 JavaScript 模块系统,为 Web 开发人员带来了诸多好处;引入模块后 JS 代码更趋于组件化,开发人员也更容易管理依赖项。但组件定义中缺少对 CSS 的支持,之前一直没有对应的解决方案。现有的实践总有以下一种或几种缺陷:
副作用,比如将元素附加到文档中的操作就会有副作用。如果在文档的顶级作用域内完成此操作,那么它会破坏 shadow 根样式作用域。如果这个操作在 shadow 根内部完成,则组件的每个独立实例必须在其 shadow 根实例中包含它自己的元素。
内联 CSS 文本作为 JavaScript 中的字符串。这种操作没有作充分的性能优化(它同时由 JS 和 CSS 解析器处理),并且会为开发人员带来糟糕的体验。
动态 fetch()ing CSS 通常不是静态可分析的,并且需要开发人员对复杂应用程序的依赖项做非常细致的管理工作。
CSS 模块扩展了 ES 模块基础结构,允许从 CSS 文件导入 CSSStyleSheet 对象,然后就可以通过 adoptStyleSheets 数组 将其添加到文档或 shadowRoot 中了。引入 CSS 模块后,上述问题也都能得到解决。
这个功能也是开发社区呼吁的——可以参阅这里的讨论:https://github.com/w3c/webcomponents/issues/759,其中有许多开发人员对其表示了兴趣。JS bundler 中的 CSS 加载器大受欢迎,也是这项功能潜在需求的佐证之一。
导入 CSS 模块
可以继续使用导入其他 ES 模块所用的语句导入 CSS 模块:
模块的默认导出是从 CSS 文件生成的 CSSStyleSheet。CSS 模块没有命名导出。
一些实现细节
程序会检查 HTTP 响应头中的 MIME 类型以确定如何解析指定模块。MIME 类型的 text/css 将被视为 CSS 模块。导入的每个 CSS 模块都有自己的模块记录,符合 ES6 规范的定义;这些模块还会参与模块映射,并进入模块依赖关系图。
CSS 模块的 V1 版本将使用 Synthetic 模块构建。具体来说,给定一个 text/css 文件,要创建一个新的 CSS 模块遵循以下步骤:
通过 constructor 创建一个 CSSStyleSheet()。
在新样式表上调用 CSSStyleSheet.replaceSync,并将文件内容作为参数(关于为什么这里用 replaceSync 而不是 replace,后文会具体说明)。此调用抛出的错误会导致模块创建失败并出现解析错误。
通过 CreateSyntheticModule 创建一个新的 Synthetic 模块,其中“default”作为 exportNames 的唯一条目,并使用 evaluateSteps 调用 SetMutableBinding(“default”, sheet),其中 sheet 是在步骤 1 中创建的 CSSStyleSheet。
使用在步骤 3 中创建的 Synthetic 模块创建新的 CSS 模块脚本作为其记录。
为什么使用 CSSStyleSheet.replaceSync 而不是 CSSStyleSheet.replace?
CSS 模块的 V1 版本功能不全,暂时不支持 @import。这里的原因在于,不清楚 CSS 模块中的 @import 是否应该被视为模块图中它自己的 CSS 模块,也不知道 CSS 模块是否应该是 leaf module。我们正在考虑三种可行方法:
CSS 模块是 leaf module,且不允许 @import 引用(遵循可构造样式表中的 replaceSync 示例: https://wicg.github.io/construct-stylesheets/#dom-cssstylesheet-replacesync)。这就是 CSS 模块 V1 版本采用的实现方法,这也是为什么上文描述的 CSS 模块创建的第 2 步使用 replaceSync 而不是 replace;如果给定输入包含 @import 规则,则抛出 replaceSync。
CSS 模块是 leaf module;在为 CSS 模块创建模块记录之前,加载其样式表的完整 @import 树,如果无法解析,则将其视为模块的解析错误。
CSS 模块不是 leaf modul。将 CSS 模块的 @import 后的样式表作为模块图中请求的子模块处理,子模块带有自己的模块记录。它们将被实例化并作为不同的模块评估。
从长远来看,1 号选项会引入不必要的限制。
选项 2 和 3 之间的主要区别之一是,3 意味着如果 CSS 文件对于给定领域多次 @import,则每个导入都会共享单独的一份 CSSStyleSheet(因为对于给定的模块 specifier,一个模块仅会被实例化 / 评估一次)。如果开发人员错误地多次包含一个样式表或者由于共享的 CSS 依赖,就有可能带来内存 / 性能损失。另一方面这也是与现有行为背道而驰,现在同一.css 文件的多个 @import 各自带有自己的 CSSStyleSheet。
@justinfagnani 在这里指出:https://github.com/w3c/webcomponents/issues/759#issuecomment-490670571,在选项 3 中共享 @imported 样式表后,开发人员用工具编辑 CSS 或主题系统时,可以动态更改共享样式表,并在样式表的所有不同导入器上应用更改。
但正如 @tabatkins 在这里提到的那样:https://github.com/w3c/webcomponents/issues/759#issuecomment-490685490,选项 3 与当前的 @import 行为有很大的不同,它无法动态再现:CSS 对象模型不能用来使多个样式表依赖于单个子样式表。.parentStyleSheet 和.ownerRule 引用这里也存在问题,因为这些引用当前仅引用单个表,并且如果样式表具有多个导入器就会糊涂了。
这里的讨论更加深入一些:https://github.com/w3c/webcomponents/issues/759#issuecomment-490256626。鉴于目前大家尚未达成共识,我们现在的 V1 版本会使用选项 1 来回避问题。这是向前兼容的,因为在 CSS 模块中只要使用 @import 就会阻止模块加载。我们不准备等待选项 2 和 3 争出结果后才推进工作,因为早早发布版本后我们就可以获得早期开发人员对该功能的反馈,更好地了解它在实践中的使用方式,从而作出更加合适的决策。此外,CSS 模块的 V1 版本完成后,HTML 模块的开发工作也能正常推进了。
示例:使用 CSS 模块的自定义元素定义
以下是关于定义自定义元素的示例,其中 CSS 是作为 JavaScript 字符串内联置入的:
以下示例中,上面的自定义元素定义合并到一个 CSS 模块,以避免 CSS-as-a-JS-string(或插入标记等):
英文原文:https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/master/CSSModules/v1Explainer.md
领取专属 10元无门槛券
私享最新 技术干货