CSS模块

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

如果让你选出一个近期CSS发展的转折点,你很可能会选2014年11月NationJS大会上Christopher Chedeau关于“CSS in JS”的分享。那个时刻是个分水岭,一系列不同的想法像经历过高能碰撞一样在各自方向上螺旋上升。例如,React Style, jsxstyleRadium 是目前在React中写样式的三个最新,最好,最可行的方法并且所有的参考资料都在它们的项目Readme中。如果发明是对于相邻可能性的一种探索,那么Christopher的责任是让许许多多的可能性更加靠近。

CSS模块

这些都是在某一方面影响大多数大型CSS代码库的问题。Christopher指出如果将样式移到JavaScript中,这些问题都能被很好得到解决,这确实没错,但是这样做增加了复杂性和特殊性。只要看一下我之前提到的项目中涌来处理:hover状态的方法的范围,有些东西在CSS中很早就被解决了。

CSS模块小组认为我们可以正面解决问题-在CSS中保留我们喜欢的一切,在styles-in-JS社区优秀工作的基础之上继续改进。因此,即便我们坚持自己的方法同时又坚定得维护CSS之美,我们仍然欠那些推动我们到这一步的朋友们一个感谢。谢谢,朋友们!

下面让我来向你们介绍什么是CSS模块,以及它为什么会成为未来。

CSS模块

第一步. 默认局部环境

在CSS模块中,每个文件独立编译因此你可以使用简单的通用类选择器-你不需要担心污染全局空间。例如现在我们要创建一个提交按钮并且它含有以下4种状态。

CSS模块

使用CSS模块之前

我们也许会用Suit/BEM等命名方式加上传统的CSS和HTML,最终结果如下:

/* 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可以互相通信。因此,当运行app时,你会看到以下内容:

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

如果在DOM中看到这些内容,表明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 */

注意,类都是独立的,而不是以一个作为”基础类”其他作为”覆盖类”。在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例子

CSS模块并不是React特有。但是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类名会产生全局冲突,这样就可以专注于结构,而不是样式。一旦摆脱一直以来的上下文切换,你会惊讶以前怎么能忍受它。

但这仅仅是个开始。当你开始思考如何组织样式时,CSS模块是不二之选。

步骤二.组件即一切

早先我提到每个类都应该包含每个状态的按钮的所有样式,对比BEM假设你有不止一个状态:

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

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

等一下,那么如何表示所有状态的共有样式呢?答案是CSS模块中最具有威力的武器,组件

.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里的@extends。但是Sass通过重写CSS选择器来实现,而CSS模块通过选择哪个类输出到JavaScript进行了改变

Sass

让我们拿出上面BEM的例子并添加一些Sass中的@extends

.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 class="Button--error">中只用一个类名并且得到了你想要的公共和特定样式。这是一个很厉害的概念,但是你必须知道实现起来有一些特殊情况和陷阱。关于这些问题的一篇很棒的总结以及后续阅读的链接在这里,感谢Hugo Giraudel.

使用CSS模块之后

compose关键字概念上类似于@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.normalstyles.error但是在渲染后的DOM上会有多个类名

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

这就是composes厉害的地方,在不用改变标记或重写CSS选择器的情况下你就可以合并多组独立的样式。

第三步. 文件共享

使用Sass或LESS,每个@import文件在同一个全局工作空间中被处理。这就是为什么你可以在一个文件中定义变量和混合宏然后在所有组件文件中使用它们。这很实用,可是一旦变量名互相冲突(因为这是另一个全局命名空间),你就不可避免得要重构variables.scsssettings.scss,并且你不知道哪些组件依赖哪些变量。这时候配置文件显得弄巧成拙

其实有更好的方法论(事实上Ben Smithett发表的 文章关于一起使用Sass和Webpack直接影响了CSS模块项目,我鼓励大家去阅读一下)然而你任然会受限于Sass的全局特性。

CSS模块每次运行一个文件,所以根本没有全局上下文可以污染。另外类似于JavaScript中我们可以importrequire依赖,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";
}

使用组件,我们可以访问普通的文件例如color.css并且引用想要使用局部名称的那个类。由于组件改变的是输出的类,而不是CSS本身,组件声明会在浏览器渲染前从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"本身没有样式。这非常好!这意味着我们不用新写一行CSS就可以添加新的局部-有意义的对象(实体叫做”normal”)。有了这样的能力,页面上视觉不一致就会越来越少并且用户浏览器的内容会越发精简。

另外:空类可以用类似于csso的工具选中和移除。

步骤四.单一功能模块

模块非常有用因为你可以描述一个元素是什么,而不是由什么样式组成。这是把概念上的实体(元素)映射到样式实体(规则)的另一种方式。让我们看一个传统CSS的简单例子:

.some_element {
  font-size: 1.5rem;
  color: rgba(0,0,0,0);
  padding: 0.5rem;
  box-shadow: 0 0 4px -2px;
}

这个元素,这些样式。很简单。然而,有个问题:color,font-size,box-shadow,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模块,我们希望可以帮助你和你的团队保持现有有关CSS的知识和现有项目,但编码更加舒服和效率更高。我们把额外语法减到最少并且试图确保这些例子接近于你正在进行的项目。我们有WebpackJSPMBrowserify的实例如果你正在用其中之一的话,并且我们一直在关注其他新的CSS模块可以运行的环境:我们正在支持服务端的NodeJS并且即将支持Rails。

但是为了让事情更简单,我在 Plunker 上制作了一个例子你可以实际体验,你不需要安装任何东西:

CSS模块

当你准备好了,访问CSS模块的主仓库。如果有任何疑问,请提issue进行讨论。CSS模块小组还很小因此我们没办法发现所有的用例,我们希望从你那里得到一些。

本文根据@Glen Maddern的《CSS Modules》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://glenmaddern.com/articles/css-modules

Blueed

现居上海。正在学习前端的道路上,欢迎交流。个人博客:Blueed.me,微博:@Ivan_z3

如需转载,烦请注明出处:http://www.w3cplus.com/css/css-modules.html

返回顶部