此 CSS Modules 非彼 CSS Modules

前一篇文章提到过 CSS Modules 通过 Hash 函数实现了 Scoped CSS,这里继续讨论 CSS Modules。本文还会涉及 TC39 的 Import Assertions 和 JSON Modules 提案,以及 WICG 的 CSS Modules 和 HTML Modules 提案。

CSS Modules(CSS 模块)

CSS Modules 已经不是什么新鲜的技术,2015 年 5 月 @sokra (webpack 作者)、@markdalgleish、@geelen 撰写了第一版 CSS Modules 规范,后又提出了 Interoperable CSS (ICSS) 规范。

下面回顾一下 CSS Modules 规范的内容。

  1. CSS Modules 中所有的类名和动画名称默认是局部作用域。
  2. 从 JS 模块中导入 CSS Module 时,CSS Module 会导出一个将所有局部名称映射成全局名称的对象。
/* style.css */
.foo {
  color: green;
}

.bar {
  color: red;
}
import styles from './style.css';

console.log(styles);
// {
//   bar: "Z8n6BLpys8v_bekqflCp",
//   foo: "W2bbHY3B6Xwz3MeKV2dn"
// }
  1. 对于局部 CSS 类名,推荐命名成 camelCase 风格,但不强制。也可以使用常见的 kebab-case 风格,和 style['class-name'] 相比,style.className 更简洁。

  2. 使用 :global:global(.class-name) 声明全局作用域样式,相似的,局部作用域使用 :local:local(.className)

:local(.className) {
  background: red;
}

:local .className {
  color: green;
}

:local(.className .subClass) {
  color: green;
}

:local .className .subClass :global(.global-class-name) {
  color: blue;
}

:global(.global-class-name) {
  background: red;
}

:global .global-class-name {
  color: green;
}

:global(.global-class-name .global-sub-class) {
  color: green;
}

:global .global-class-name .global-sub-class :local(.localClassName) {
  color: blue;
}
  1. 使用 compose 组合样式,可以有多条 compose 规则,也可以一条规则组合多个类名 compose: classNameA classNameB
.className {
  color: green;
  background: red;
}

.otherClassName {
  composes: className;
  color: yellow;
}
  1. compose 支持组合来自其他文件和全局的样式类名。
.otherClassName {
  composes: className from './style.css';
}

.otherClassName {
  composes: globalClassName from global;
}

Import Assertions(导入断言)

在讨论 WICG 的 CSS Modules 提案之前,先看下 TC39 的两个提案。

Import Assertions 提案为 import 语句增加了新的内联语法,能够传递更多信息,如模块类型(type)。增加对媒体类型(Media Type, MIME)的断言是为了解决一部分安全问题,比如服务端返回了非预期的媒体类型而导致非预期的代码被客户端执行。

目前该提案的首次应用是 JSON Modules(JSON Modules 提案原本是该提案的一部分)。

JSON Modules:

import json from './foo.json' assert { type: 'json' };
import('foo.json', { assert: { type: 'json' } });

后续大概率会支持其他类型模块,比如 WebAssembly:

import foo from './foo.wasm';

new Worker('foo.wasm', { type: 'module', assert: { type: 'webassembly' } });

Node.js 已经有实验性支持(--experimental-wasm-modules)。

比如 CSS:

import styles from './style.css' assert { type: 'css' };

比如 HTML:

import template from './template.html' assert { type: 'html' };

理论上 JS 模块也可以加上断言,虽然有些画蛇添足:

import foo from './foo.js' assert { type: 'javascript' };

JSON Modules(JSON 模块)

JSON Modules 是来自 TC39 的提案,为 JS 增加了导入 JSON 模块的能力。JSON Modules 提案基于 Import Assertions 提案。

import json from './foo.json' assert { type: 'json' };

import('foo.json', { assert: { type: 'json' } });

Node.js 原本就支持 JSON 模块,但是仅限在 CommonJS 模块中使用(依赖 CJS 加载器):

const json = require('./package.json');

ESM 加载器的实验性支持需要开启 --experimental-json-modules flag:

// index.mjs
import json from './package.json';
$ node --experimental-json-modules index.mjs

Node.js 尚未实现 Import Assertions,和浏览器环境嗅探资源的 MIME 不同,Node.js 导入模块是依据文件后缀,比如 .js.json.node

浏览器端 Chrome Canary 和 Edge Canary 已经实现对 JSON Modules 实验性的支持。

import json from './package.json' assert { type: 'json' };
console.log(json);

WHATWG CSS Modules

这里的 CSS Modules 加上了定语 WHATWG 用来和社区的 CSS Modules 规范做区分,它是 Web Components 的一部分,由 WICG 工作组制定,和社区规范没有任何关系。关于术语 "CSS Modules" 命名混乱的问题在 GitHub 上有过水深火热的讨论 Change 'CSS Modules' name to avoid webdev confusion #843。为了方便区分,本文称呼为 "WHATWG CSS Modules"(或者 "W3C CSS Modules"?)。

WHATWG CSS Modules 的语法和 CSS Modules 类似:

import styles from './styles.css' assert { type: 'css' };

但是导入的不再是普通对象,而是 CSSStyleSheet

let sheet = new CSSStyleSheet();
sheet.replaceSync('div { color: green }');
console.log(sheet);
// {
//   ownerRule: null,
//   rules: CSSRuleList {0: CSSStyleRule, length: 1},
//   cssRules: {0: CSSStyleRule, length: 1},
//   disabled: false,
//   href: null,
//   media: {length: 0, mediaText: ""}, // MediaList
//   ownerNode: null,
//   parentStyleSheet: null,
//   title: "",
//   type: "text/css"
// }

除了打印的属性,CSSStyleSheet 还包括以下修改样式规则的方法:

  • CSSStyleSheet.insertRule(rule: string, index?: number): number 插入新的样式规则
  • CSSStyleSheet.deleteRule(index: number): void 删除指定的样式规则
  • CSSStyleSheet.replace(rule: string): Promise<CSSStyleSheet> 异步替换样式规则
  • CSSStyleSheet.replaceSync(rule: string): void 同步替换样式规则

先看一段加载 CSS 的代码,需要显式传入字符串字面量,代码如下:

let sheet = new CSSStyleSheet();
sheet.replaceSync('div { color: green }');
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

写成 CSS Modules 形式后更为简洁,目前 Chrome Canary 和 Edge Canary 已经实现,代码如下:

/* styles.css */
div { color: green }
import styles from './styles.css' assert { type: 'css' };
document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];

在 HTML 标准中 CSS Module 也被描述为 CSS Module Script,和 JS 一样属于 Module script:

Module scripts can be classified into two types:

  • A module script is a JavaScript module script if its record is a Source Text Module Record.

  • A module script is a CSS module script if its record is a Synthetic Module Record, and it was created via the create a CSS module script algorithm. CSS module scripts represent a parsed CSS stylesheet.

以上节选自 https://html.spec.whatwg.org/multipage/webappapis.html#module-script

HTML Modules(HTML 模块)

和 CSS Modules 类似,HTML Modules 提案允许开发者从外部 HTML 模块中导出 DOM 元素,免去使用字符串字面量和 内联 <template> 元素。

import importedDoc from './import.html' assert { type: 'html' };
let theTemplate = importedDoc.querySelector('template');
import { content } from './import.html' assert { type: 'html' };
document.body.appendChild(content);

该提案目前尚未有浏览器实现。

总结

JSON/CSS/HTML Modules 提高了 JSON/CSS/HTML 和 JS 间的互操作性(Interoperability),随着相关提案的定稿,和浏览器、Node.js 对规范的实现,前端开发必然会更加便利。

相关链接