Glen Maddern: CSS 模块

CSS 模块

原文:http://glenmaddern.com/articles/css-modules

欢迎来到未来

  • 2015-08-19

假如你想要弄清楚在最近 CSS 思维发展中的拐点 ,很有可能你就会挑选 Christopher Chedeau 在去年十一月份 “CSS in JS” 的讲话。这是一个分水岭,如同经历过高度碰撞后的粒子般急速前进,从原本的方向中确立了不同的思想分支。例如,React Style, jsxstyleRadium 是目前用来设计 React 样式及其与之相关的项目的方法中最新,最聪明和可行的方法。如果创新是用来探索临近可能 adjacent possible 的其中一种情形,那么 Christopher 则会靠近更多的可能性。

Christopher Chedeau's 7 problems with CSS at scale

这一页幻灯片让很多人有一种似曾相识的感觉。

这里全都是从一方面以上影响大部分 CSS 代码库的合法性问题, Christopher 指出只要你愿意把样式放进 JavaScript,这些都能得到很好的解决,尽管如此但是它也拥有自己的复杂之处和特质。看看我之前提到的项目中处理 :hover 状态的一系列办法,而在 CSS 里面已经早就解决了。

CSS Modules team 觉得我们可以跟问题死磕,保持我们所喜欢的 CSS 并在 styles-in-JS 社区的佳作上继续创作。所以,我们在看好我们自身的方法并坚定捍卫 CSS 优点的同时,也由衷的感谢在其他方向不断有所突破的人。谢谢!

让我来告诉你们为什么 CSS 模块化是未来。

This is how intensely we’ve been thinking about 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 */
<button class="Button Button--in-progress">Processing...</button>

这看起来确实挺好的,我们有这四种变体,然而 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 模块编译的方式变得可能 - 通过使用 requireimport 从 JavaScript 中加载这些文件:

/* components/submit-button.js */
import styles from './submit-button.css';

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

实际上命名空间是自动生成并且唯一的。 CSS 模块让已经为你考虑好了,并且编译文件为 ICSS阅读我之前的博客 的格式,介绍了 CSS 和 JS 是如何沟通的。所以,当你运行应用的时候,会看到下面的东西:

<button class="components_submit_button__normal__abc5436">
  Processing...
</button>

如果你在 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 <button className={className}>{text}</button>
  }
}

在使用你自己样式的时候,你可以不必担心产生一个全局安全的 CSS 变量名,这样会让你更专注于组件而非样式。并且一旦摆脱了这种持续的上下文切换,你会对于曾经你的忍受感到惊讶。

第二步 组成是一切

早些时候我提过,每一个类名应该包含一个按钮不同状态的所有样式,而在 BEM 样式里面你必须假设它不止有一个类名:

/* BEM Style */
innerHTML = `<button class="Button Button--in-progress">`

/* CSS Modules */
innerHTML = `<button class="${styles.inProgress}">`

等等,但是你要怎样代表所有状态共有的样式?答案是 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 */
}

你可以在你的标签 <button calss="Button--error"> 只用一个类名来获得你想要的公有 & 特殊的样式。这是一个非常强大的概念,但是实践起来却有一些你需要注意的边缘的案例 & 陷阱。感谢 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 里面获得不同的类


<button class="components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

这就是 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 {}
<button class="shared_colors__primary__fca929
               components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

实际上,在它到达浏览器的那一刻,我们本地名 “normal” 并没有它自己的样式。这是好事!因为这意味着我们可以使用一个新的具有本地意义的对象(一个称为 “normal” 的实体)而无需添加新一行 CSS。我们越是能做到这一点,蔓延在我们网站的视觉不一致以及到达浏览器之后的臃肿就会越少。

<small data-reactid=".0.1.1.1x.0">除此之外:这些空的类名可以很容易的被检测并且被类似 csso 之类的检查器删除。 </small>

第四步 单一合理的模块

组成是十分给力的,因为它让你去描述一个元素而非它组成的样式。这是另一种不同的概念上的实体到样式的实体的映射。让我们来看一看一个 朴素老旧的 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 规模较小,我们并不能知道所有的问题,所以希望能够听到你的想法。

Style happy, friends!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容

  • 编写css是前端工作中,一项普通而又频繁的劳动,由于css并不是一门语言,所以在程序设计上显得有些简陋。对于小型项...
    Jack_Lo阅读 5,587评论 15 39
  • 再谈CSS 预处理器2016-09-09 Justineo JavaScript转自:http://efe.bai...
    抓住时间的尾巴吧阅读 1,462评论 0 2
  • 在现在的前端开发中,前后端分离、模块化开发、版本控制、文件合并与压缩、mock数据等等一些原本后端的思想开始...
    Charlot阅读 5,385评论 1 32
  • 今天参观了一下万科的万物仓,发现还是挺有意思的,整个环境整洁干净,配备空调和除湿系统,门口有保安24小时保护,给人...
    蔡明洲阅读 2,372评论 0 0
  • 我有一位同龄的好伙伴,名叫阿宝。十八岁那年,他第一次远离家乡,去外省一个城市打工,遭遇不测,从此再也没能回来。 那...
    向往森林阅读 135评论 0 1