CSS 模块
原文:http://glenmaddern.com/articles/css-modules
欢迎来到未来
- 2015-08-19
假如你想要弄清楚在最近 CSS 思维发展中的拐点 ,很有可能你就会挑选 Christopher Chedeau 在去年十一月份 “CSS in JS” 的讲话。这是一个分水岭,如同经历过高度碰撞后的粒子般急速前进,从原本的方向中确立了不同的思想分支。例如,React Style, jsxstyle 和 Radium 是目前用来设计 React 样式及其与之相关的项目的方法中最新,最聪明和可行的方法。如果创新是用来探索临近可能 adjacent possible 的其中一种情形,那么 Christopher 则会靠近更多的可能性。
这一页幻灯片让很多人有一种似曾相识的感觉。
这里全都是从一方面以上影响大部分 CSS 代码库的合法性问题, Christopher 指出只要你愿意把样式放进 JavaScript,这些都能得到很好的解决,尽管如此但是它也拥有自己的复杂之处和特质。看看我之前提到的项目中处理 :hover
状态的一系列办法,而在 CSS 里面已经早就解决了。
CSS Modules team 觉得我们可以跟问题死磕,保持我们所喜欢的 CSS 并在 styles-in-JS 社区的佳作上继续创作。所以,我们在看好我们自身的方法并坚定捍卫 CSS 优点的同时,也由衷的感谢在其他方向不断有所突破的人。谢谢!
让我来告诉你们为什么 CSS 模块化是未来。
我们是这么描述 CSS 模块的
第一步 本地默认
在 CSS 模块中,每一个文件都是单独编译的,所以你可以用一些简单的类名选择器和通用名称,而不必担心污染全局变量、比方说,我们正在构建一个简单的提交按钮具有下列 4 种状态:正常、不可用、错误、处理中。
开始CSS模块之前
我们也许会这样写代码,使用 普通古老的 CSS 和 HTML 的 Suit/BEM 风格的类名:
/* components/submit-button.css */
.Button { /* all styles for Normal */ }
.Button--disabled { /* overrides for Disabled */ }
.Button--error { /* overrides for Error */ }
.Button--in-progress { /* overrides for In Progress */
这看起来确实挺好的,我们有这四种变体,然而 BEM 风格的命名意味着我们不用没有了可以嵌套的选择器。我们用大写字母开头的 Button
避免前置的样式或我们放进的依赖。并且我们采用 --modifier
的类型所以我们可以清楚这个变体是需要基础类名来应用的。
总之,这是合理明确并且可维护的代码,但是这需要对围绕命名规范有可怕的认知理解。然而,这是我们在标准的 CSS 所能做的最好的了。
利用 CSS 模块
CSS模块以为这你从不需要担心你的命名空间变得普遍,就用在任何觉得有意义的地方就可以了。
/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */
注意到我们并不到处使用 "button" 这个词。为什么呢?这个文件早就命名为 "submit-button.css",在其他的的语言里,你并不需要去对拥有命名空间的本地变量进行预处理,CSS 当然也是。
这就让 CSS 模块编译的方式变得可能 - 通过使用 require
和 import
从 JavaScript 中加载这些文件:
/* components/submit-button.js */
import styles from './submit-button.css';
buttonElem.outerHTML = ``
实际上命名空间是自动生成并且唯一的。 CSS 模块让已经为你考虑好了,并且编译文件为 ICSS阅读我之前的博客 的格式,介绍了 CSS 和 JS 是如何沟通的。所以,当你运行应用的时候,会看到下面的东西:
如果你在 DOM 看到这些东西,说明已经成功了!
你是大猩猩,CSS 模块是鲨鱼
命名约定
再回来思考我们按钮的例子:
/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */
注意到所有的类型都是独立的,与其一部分变成“基本类”,剩下的部分变成“覆盖类”。在 CSS 模块里,任何一个类名都需要对应改变量的所有样式(不止是短期的用处)。在 JavaScript 中,使用这些样式会大有不同:
/* Don't do this */
`class=${[styles.normal, styles['in-progress']].join(" ")}`
/* Using a single name makes a big difference */
`class=${styles['in-progress']}`
/* camelCase makes it even better */
`class=${styles.inProgress}`
一个 React 的例子
React 阵营本身和 CSS 模块并没有什么联系。但是 React 却提供了一个使用 CSS 模块绝佳的经历,所以展示一个比较复杂的例子是值得的:
/* components/submit-button.jsx */
import { Component } from 'react';
import styles from './submit-button.css';
export default class SubmitButton extends Component {
render() {
let className, text = "Submit"
if (this.props.store.submissionInProgress) {
className = styles.inProgress
text = "Processing..."
} else if (this.props.store.errorOccurred) {
className = styles.error
} else if (!this.props.form.valid) {
className = styles.disabled
} else {
className = styles.normal
}
return
}
}
在使用你自己样式的时候,你可以不必担心产生一个全局安全的 CSS 变量名,这样会让你更专注于组件而非样式。并且一旦摆脱了这种持续的上下文切换,你会对于曾经你的忍受感到惊讶。
第二步 组成是一切
早些时候我提过,每一个类名应该包含一个按钮不同状态的所有样式,而在 BEM 样式里面你必须假设它不止有一个类名:
/* BEM Style */
innerHTML = `
等等,但是你要怎样代表所有状态共有的样式?答案是 CSS 模块最为给力的功能,组成 (composition):
.common {
/* all the common styles you want */
}
.normal {
composes: common;
/* anything that only applies to Normal */
}
.disabled {
composes: common;
/* anything that only applies to Disabled */
}
.error {
composes: common;
/* anything that only applies to Error */
}
.inProgress {
composes: common;
/* anything that only applies to In Progress */
}
composes
关键词说明 .normal
包括了所有来自 .common
样式,很像 Sass 里面的 @extend
关键词。但是 Sass 会重写你的 CSS 选择器达到这个目的, 而 CSS 模块会改变导出到 JavaScript 的类名。
在 Sass
让我们举一个 BEM 的例子,并且应用一些 Sass 的 @extend
:
.Button--common { /* font-sizes, padding, border-radius */ }
.Button--normal {
@extends .Button--common;
/* blue color, light blue background */
}
.Button--error {
@extends .Button--common;
/* red color, light red background */
}
编译到 CSS:
.Button--common, .Button--normal, .Button--error {
/* font-sizes, padding, border-radius */
}
.Button--normal {
/* blue color, light blue background */
}
.Button--error {
/* red color, light red background */
}
你可以在你的标签
只用一个类名来获得你想要的公有 & 特殊的样式。这是一个非常强大的概念,但是实践起来却有一些你需要注意的边缘的案例 & 陷阱。感谢 Hugo Giraudel 这里有一个很好的问题总结和链接。
包含 CSS 模块
composes
关键词在概念上类似于 @extends
,但是执行起来是不相同。为了证明,先看一个例子:
.common { /* font-sizes, padding, border-radius */ }
.normal { composes: common; /* blue color, light blue background */ }
.error { composes: common; /* red color, light red background */ }
到达浏览器之后看起来会像下面这个样子:
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 { /* blue color, light blue background */ }
.components_submit_button__error__1638bcd { /* red color, light red background */ }
在你的 JS 代码里面, import styles from "./submit-button.css
返回:
styles: {
common: "components_submit_button__common__abc5436",
normal: "components_submit_button__common__abc5436 components_submit_button__normal__def6547",
error: "components_submit_button__common__abc5436 components_submit_button__error__1638bcd"
}
所以我们仍然可以在我们的代码里面使用styles.normal
或者 styles.error
, 我们会在渲染完的 DOM 里面获得不同的类
Submit
这就是 composes
的力量,你可以组合不同的独立样式组,而无需改变你的标签并且重写你的 CSS 选择器。
第三步 在不同的文件共享
使用 Sass 或者 Less,每一个你 @import
的文件会在同一个全局工作区被处理。这就决定了你在一个文件如何定义变量和混合块并在你所有的组件文件使用。这很实用,然而一旦你的变量名与其他的变量名产生了冲突(由于它们共享了同样的命名空间),你会不可避免的重构一个 variables.scss
或者 settings.scss
,并且对于你来说,哪个模块依赖哪个变量变得不可见。而且你的 settings
文件会变得笨重。
这里有一个更好的方法(实际上 Ben Smithett 的post about using Sass & Webpack together 在 CSS 模块项目有一个直接的影响,我推荐你去读一读)但是你同样会受限于 Sass 的全局命名。
CSS 模块在单一的文件运行一次,所以并没有全局空间的污染。就像在 JavaScript 中我们可以 import
或者 require
我们的依赖, CSS 模块也可以让我们从其他的文件 compose
:
/* colors.css */
.primary {
color: #720;
}
.secondary {
color: #777;
}
/* other helper classes... */
/* submit-button.css */
.common { /* font-sizes, padding, border-radius */ }
.normal {
composes: common;
composes: primary from "../shared/colors.css";
}
使用组成(composes),我们得以使用命名普通的文件 color.css
并且使用它本地的名字来引用其中的一个类。由于组成改变了 exported 的类名而非 CSS 本身, composes
的声明本身在到达浏览器之前就会从 CSS 本身删除了:
/* colors.css */
.shared_colors__primary__fca929 {
color: #720;
}
.shared_colors__secondary__acf292 {
color: #777;
}
/* submit-button.css */
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 {}
Submit
实际上,在它到达浏览器的那一刻,我们本地名 “normal” 并没有它自己的样式。这是好事!因为这意味着我们可以使用一个新的具有本地意义的对象(一个称为 “normal” 的实体)而无需添加新一行 CSS。我们越是能做到这一点,蔓延在我们网站的视觉不一致以及到达浏览器之后的臃肿就会越少。
除此之外:这些空的类名可以很容易的被检测并且被类似 csso 之类的检查器删除。
第四步 单一合理的模块
组成是十分给力的,因为它让你去描述一个元素而非它组成的样式。这是另一种不同的概念上的实体到样式的实体的映射。让我们来看一看一个 朴素老旧的 CSS 的简单例子:
.some_element {
font-size: 1.5rem;
color: rgba(0,0,0,0);
padding: 0.5rem;
box-shadow: 0 0 4px -2px;
}
这个元素以及样式,简单,然而却有一个问题:colour, font-size, box-shadow, the padding,这些所有的东西都是有详尽的细节规范的,即使我们想要在其他地方重用这些样式。让我们在 Sass 重构一次:
$large-font-size: 1.5rem;
$dark-text: rgba(0,0,0,0);
$padding-normal: 0.5rem;
@mixin subtle-shadow {
box-shadow: 0 0 4px -2px;
}
.some_element {
@include subtle-shadow;
font-size: $large-font-size;
color: $dark-text;
padding: $padding-normal;
}
这是一个进步,但我们只提取了大多数行的一半。$large-font-size
是排版和 $padding-normal
是布局的事实不过是通过其名称,而不是在任何地方执行表示。当有一个类似于 box-shadow
的声明的值并不会让它成为一个变量,我们不得不用一个 @mixin
或 @extends
来表示。
使用 CSS 模块
通过使用组成,我们可以在可复用的部分声明自己的组件。
.element {
composes: large from "./typography.css";
composes: dark-text from "./colors.css";
composes: padding-all-medium from "./layout.css";
composes: subtle-shadow from "./effect.css";
}
这种格式自然而然的会产生大量含有单一目的的文件,通过文件系统划定不同的风格而不是命名空间。加入你想要把不同的类型放在一个文件里,可以试一下下面的简写:
/* this short hand: */
.element {
composes: padding-large margin-small from "./layout.css";
}
/* is equivalent to: */
.element {
composes: padding-large from "./layout.css";
composes: margin-small from "./layout.css";
}
这样使得通过极端粒度的类名为你的网站上每一个视觉效果添加别名提供了可能性:
.article {
composes: flex vertical centered from "./layout.css";
}
.masthead {
composes: serif bold 48pt centered from "./typography.css";
composes: paragraph-margin-below from "./layout.css";
}
.body {
composes: max720 paragraph-margin-below from "layout.css";
composes: sans light paragraph-line-height from "./typography.css";
}
这是一项我十分感兴趣去探索的技术。在我看来,它结合了 Tachyons 原子 CSS 技术,Semantic UI 的可读性和独立性等等最好的方面。
但我们现在只是在 CSS 模块故事的开始,我们十分欢迎你在目前或者接下来的项目尝试并与我们共创未来。
开始吧!
通过 CSS 模块,我们希望我们可以帮你和你的团队保留你们现有尽可能多的 CSS 知识与产品,变得更为舒服和专业。我们已经把语法添加到最低限度,努力确保有例子是接近你已经工作的方式。我们在 Webpack, JSPM 以及 Browserify 都有示范项目,如果您使用的其中之一,我们总是在了望 CSS 模块可以生效的新环境:服务器端支持的 NodeJS 正在 happening 而 Rails 正在初始。
为了然事情更为简单,我在这里建了个例子而你无需安装任何东西:
只要你准备好了,可以去 CSS Modules 仓库看一看,如果你有问题,请提交 issue 来讨论。CSS Modules team 规模较小,我们并不能知道所有的问题,所以希望能够听到你的想法。