CSS Modules V1 是 Microsoft 提出的一项新建议,它是 ES Script 模块系统提出了一项扩展。在 CSS 模块的帮助下,Web 开发人员可以将 CSS 加载到组件定义中(例如 import styles from “styles.css”;),并且与其他模块类型无缝对接。
为什么我们需要 CSS 模块?
ES6 规范引入了 JavaScript 模块系统,为 Web 开发人员带来了诸多好处;引入模块后 JS 代码更趋于组件化,开发人员也更容易管理依赖项。但组件定义中缺少对 CSS 的支持,之前一直没有对应的解决方案。现有的实践总有以下一种或几种缺陷:
副作用,比如将
<style>
元素附加到文档中的操作就会有副作用。如果在文档的顶级作用域内完成此操作,那么它会破坏 shadow 根样式作用域。如果这个操作在 shadow 根内部完成,则组件的每个独立实例必须在其 shadow 根实例中包含它自己的<style>
元素。内联 CSS 文本作为 JavaScript 中的字符串。这种操作没有作充分的性能优化(它同时由 JS 和 CSS 解析器处理),并且会为开发人员带来糟糕的体验。
动态 fetch()ing CSS 通常不是静态可分析的,并且需要开发人员对复杂应用程序的依赖项做非常细致的管理工作。
CSS 模块扩展了 ES 模块基础结构,允许从 CSS 文件导入 CSSStyleSheet 对象,然后就可以通过adoptStyleSheets数组将其添加到文档或 shadowRoot 中了。引入 CSS 模块后,上述问题也都能得到解决。
这个功能也是开发社区呼吁的——可以参阅这里的讨论,其中有许多开发人员对其表示了兴趣。JS bundler 中的 CSS 加载器大受欢迎,也是这项功能潜在需求的佐证之一。
导入 CSS 模块
可以继续使用导入其他 ES 模块所用的import
语句导入 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示例。这就是 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 在这里指出,在选项 3 中共享 @imported 样式表后,开发人员用工具编辑 CSS 或主题系统时,可以动态更改共享样式表,并在样式表的所有不同导入器上应用更改。
但正如 @tabatkins 在这里提到的那样,选项 3 与当前的 @import 行为有很大的不同,它无法动态再现:CSS 对象模型不能用来使多个样式表依赖于单个子样式表。.parentStyleSheet 和.ownerRule 引用这里也存在问题,因为这些引用当前仅引用单个表,并且如果样式表具有多个导入器就会糊涂了。
这里的讨论更加深入一些。鉴于目前大家尚未达成共识,我们现在的 V1 版本会使用选项 1 来回避问题。这是向前兼容的,因为在 CSS 模块中只要使用 @import 就会阻止模块加载。我们不准备等待选项 2 和 3 争出结果后才推进工作,因为早早发布版本后我们就可以获得早期开发人员对该功能的反馈,更好地了解它在实践中的使用方式,从而作出更加合适的决策。此外,CSS 模块的 V1 版本完成后,HTML模块的开发工作也能正常推进了。
示例:使用 CSS 模块的自定义元素定义
以下是关于定义自定义元素的示例,其中 CSS 是作为 JavaScript 字符串内联置入的:
以下示例中,上面的自定义元素定义合并到一个 CSS 模块,以避免 CSS-as-a-JS-string(或插入<style>
标记等):
评论