软件开发架构师

微软提出CSS Modules V1 :通过import语句将CSS模块导入到组件中-InfoQ

前端 6 2019-09-03 04:06

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 模块:

复制代码
import styles from "styles.css";
document.adoptedStyleSheets = [... document.adoptedStyleSheets, styles];

模块的默认导出是从 CSS 文件生成的 CSSStyleSheet。CSS 模块没有命名导出。

一些实现细节

程序会检查 HTTP 响应头中的 MIME 类型以确定如何解析指定模块。MIME 类型的 text/css 将被视为 CSS 模块。导入的每个 CSS 模块都有自己的模块记录,符合 ES6 规范的定义;这些模块还会参与模块映射,并进入模块依赖关系图。

CSS 模块的 V1 版本将使用 Synthetic 模块构建。具体来说,给定一个 text/css 文件,要创建一个新的 CSS 模块遵循以下步骤:

  1. 通过 constructor 创建一个 CSSStyleSheet()。

  2. 在新样式表上调用 CSSStyleSheet.replaceSync ,并将文件内容作为参数(关于为什么这里用 replaceSync 而不是 replace,后文会具体说明)。此调用抛出的错误会导致模块创建失败并出现解析错误。

  3. 通过 CreateSyntheticModule 创建一个新的 Synthetic 模块,其中“default”作为 exportNames 的唯一条目,并使用 evaluateSteps 调用 SetMutableBinding(“default”, sheet),其中 sheet 是在步骤 1 中创建的 CSSStyleSheet。

  4. 使用在步骤 3 中创建的 Synthetic 模块创建新的 CSS 模块脚本作为其记录。

为什么使用 CSSStyleSheet.replaceSync 而不是 CSSStyleSheet.replace?

CSS 模块的 V1 版本功能不全,暂时不支持 @import。这里的原因在于,不清楚 CSS 模块中的 @import 是否应该被视为模块图中它自己的 CSS 模块,也不知道 CSS 模块是否应该是 leaf module。我们正在考虑三种可行方法:

  1. CSS 模块是 leaf module,且不允许 @import 引用(遵循可构造样式表中的 replaceSync 示例。这就是 CSS 模块 V1 版本采用的实现方法,这也是为什么上文描述的 CSS 模块创建的第 2 步使用 replaceSync 而不是 replace;如果给定输入包含 @import 规则,则抛出 replaceSync。

  2. CSS 模块是 leaf module;在为 CSS 模块创建模块记录之前,加载其样式表的完整 @import 树,如果无法解析,则将其视为模块的解析错误。

  3. 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 字符串内联置入的:

复制代码
<!doctype html>
<html>
<head>
<script type="module">
class HTML5Element extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: "open" });
let style = document.createElement( "style");
style.innerText = `
.outerDiv {
border:0.1em solid blue;
display:inline-block;
padding: 0.4em;
}
{1}
.devText {
font-weight: bold;
font-size: 1.2em;
text-align: center;
margin-top: 0.3em;
}
{1}
.mainImage {
height:254px;
}
`;
let outerDiv = document.createElement( "div");
outerDiv.className = "outerDiv";
let mainImage = document.createElement( "img");
mainImage.className = "mainImage";
mainImage.src = "https://www.w3.org/html/logo/downloads/HTML5_Logo_512.png";
let devText = document.createElement( "div");
devText.className = "devText";
devText.innerText = "CSS Modules Are Great!";
this.shadowRoot.appendChild(outerDiv);
outerDiv.appendChild(mainImage);
outerDiv.appendChild(devText);
this.shadowRoot.appendChild(style);
}
}
window.customElements.define( "my-html5-element", HTML5Element);
</script>
</head>
<body>
<my-html5-element> </my-html5-element>
</body>
</html>

以下示例中,上面的自定义元素定义合并到一个 CSS 模块,以避免 CSS-as-a-JS-string(或插入<style>标记等):

复制代码
<!doctype html>
<html>
<head>
<script type="module">
import styles from './html5Element.css';
class HTML5Element extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: "closed" });
this.shadowRoot.adoptedStyleSheets = [styles];
let outerDiv = document.createElement( "div");
outerDiv.className = "outerDiv";
let mainImage = document.createElement( "img");
mainImage.className = "mainImage";
mainImage.src = "https://www.w3.org/html/logo/downloads/HTML5_Logo_512.png";
let devText = document.createElement( "div");
devText.className = "devText";
devText.innerText = "CSS Modules Are Great!";
shadowRoot.appendChild(outerDiv);
outerDiv.appendChild(mainImage);
outerDiv.appendChild(devText);
}
}
window.customElements.define( "my-html5-element", HTML5Element);
</script>
</head>
<body>
<my-html5-element> </my-html5-element>
</body>
</html>

英文原文: CSS Modules V1 Explainer

文章评论