CSS Modules

Posted by Vison on September 29, 2017

讲一下写这篇文章的背景,之前工作一直在围绕ReactNative Android原生以及后台,对React Web端接触的较少。对React和Redux以及ES6语法倒是较 为熟悉,但是对React Web端开发还是有些盲区。加上最近刚入职新公司,接下来一段时间可能会重点放在React Web端开发,所以最近进行了一些扫盲,对 Web端开发进行了一些实验和思考,在这个过程中发现对CSS Modules可能不是很理解。做了一些试验,对CSS Modules有了一些新的感悟吧,所以对这些想 法做一些整理。

什么是 CSS Modules?

CSS Modules?是CSS吗?那你就错了,CSS Modules不是CSS,它只是将CSS中加入局部作用域和模块依赖。使用 JS 来管理样式依赖,使得CSS能够应用 到组件中去,并且将CSS的全局作用域改成局部作用域,这样组件样式之间就会进行隔离。样式文件形成局部作用域,和其他文件之间有相同的classname也 不会互相影响。所以,CSS Modules只是一种CSS在组件编程中的一种解决方案。CSS Modules 能最大化地结合现有 CSS 和 JS 模块化能力。发布时依旧 编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular等任何框架中使用。当然CSS 模块化的解决方案有很多,但 CSS Modules是目前最好的 CSS 模块化解决方案之一。

如何产生局部作用域?

大家都知道CSS规则是全局的,CSS .class 选择器选取带有指定类 (class) 的元素,如果样式中出现两个相同命名的class选择器,他们将会互相影响。

<!DOCTYPE html>
<html>
    <head>
        <style>
        .intro{
            background-color:yellow;
            text-align:center;
        }
        </style>
        <style>
        .intro{
            background-color:red;
        }
        </style>
    </head>
    <body>
        <div class="intro">
            <p>我是唐老鸭。</p>
        </div>
    </body>
</html>

大家运行上面例子后可以发现,p标签中文字为红色居中,这就说明了CSS规则是全局的,class命名相同的话会有样式的冲突,即使打包在不同的style中 也会互相影响。那么问题来了,如何让CSS规则产生局部作用域呢?其实我们只要打包时将class命名给改掉,用一个独一无二的class的名字进行替换就好 了啊,这样就不会与其他class命名冲突了。

解决办法

上述例子是为了引发大家对CSS Modules的思考,下面就会对React中CSS Modules的解决方案进行分析。写了一个React组件如下:

import React from 'react';
import { connect } from 'react-redux';
import style from './index.css';

class Home extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
    };
  }

  render() {
    return (
      <div className={style.title}>
        首页
      </div>
    );
  }
}

export default connect()(Home);

组件中国年引用的样式文件index.css如下:

.title {
  size: 20px;
  text-align: center;
}

该组件在项目中使用Webpack build以后界面元素如下所示:

.byzpuW3R6MEgvP-zXxPHu {
    size: 20px;
    text-align: center;
}

<div class="byzpuW3R6MEgvP-zXxPHu">
    首页
</div>

大家可以发现,.title在被Hash过以后变成了.byzpuW3R6MEgvP-zXxPHu,这样就能保证该class是唯一的。而style.title的引用也被编译成了Hash后 的字符串,这样以来就能保证只有引用该路径下的CSS中的title才会使用该样式,就达到了隔离的目的。

如何实现的呢?

上面例子中Hash .class是因为项目中使用了Webpack,不止Webpack,CSS Modules 也提供其他各种插件,Browserify、JSPM、NodeJS等。 我们使用的是 Webpack 的css-loader插件,比较容易使用,下面是一个Webpack 使用 css-loader的一个例子。

module.exports = {
  entry: __dirname + '/index.js',
  output: {
    publicPath: '/',
    filename: './bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'stage-0', 'react']
        }
      },
      {
        test: /\.css$/,
        loader: "style-loader!css-loader?modules&localIdentName=[name]__[local]-[hash:base64:5]"
      },
    ]
  }
};

全局作用域怎么办?

一直在讲CSS如何实现局部作用域,搞到最后发现css文件都变成局部的了,那全局可怎么办啊 ::>_<:: 。不用担心,其实CSS Modules早就考虑到这个问题了。 其实使用了 CSS Modules 后,就相当于给每个 class 名外加加了一个 :local,以此来实现样式的局部化,如果你想切换到全局模式,使用对应的 :global。 使用:global(.className)的语法,声明一个全局规则,使用这种方式声明的class都不会被编译成哈希字符串。 另外要说明一下,CSS Modules 只会转换 class 名相关样式, id 选择器、伪类、标签选择器将不被转换,原封不动的出现在编译后的 css 中。

.link {
  color: green;
}

/* 以上与下面等价 */
:local(.normal) {
  color: green;
}


/* 可定义多个全局样式 */
:global {
  .link {
    color: green;
  }
  .box {
    color: yellow;
  }
}

使用Compose 来组合样式

对于样式复用,CSS Modules 只提供了唯一的方式来处理:composes 组合

/* components/Button.css */
.base { /* 所有通用的样式 */ }

.normal {
  composes: base;
  /* normal 其它样式 */
}

.disabled {
  composes: base;
  /* disabled 其它样式 */
}
import styles from './Button.css';

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

生成的 HTML 变为

<button class="button--base-fec26 button--normal-abc53">Submit</button>

由于在 .normal 中 composes 了 .base,编译后会 normal 会变成两个 class。

composes 还可以组合外部文件中的样式。

/* settings.css */
.primary-color {
  color: #f40;
}

/* components/Button.css */
.base { /* 所有通用的样式 */ }

.primary {
  composes: base;
  composes: primary-color from './settings.css';
  /* primary 其它样式 */
}

CSS Modules 使用技巧

CSS Modules 使用建议遵循如下原则:

  • 不使用选择器,只使用 class 名来定义样式;
  • 不层叠、不嵌套多个 class,只使用一个 class 把所有样式定义好;
  • style 文件中使用了 id 选择器,伪类,标签选择器等;
  • 不要对一个元素使用多个 class;
  • 使用 composes 组合来实现复用;

作为一个CSS本来就不怎么熟练的开发者来说,前两项对我倒没声明大问题,但是CSS大神们可能就接受不了了。明明很简单就能定义出来,为什么要这么麻烦。 这种做法可能会抛弃掉CSS中最精华的部分,但是这样会让CSS Modules更加简单易用,而且有一个缺点项目中class数量将成倍上升。