Skip to content

CSS Modules详解及在React中的实践 #11

Description

@zxiaohong

CSS Modules 详解及应用

在最近的React项目中,遇到了CSS处理的问题:

  1. 由于是多人开发,各自对样式类的命名规则不统一
  2. 样式全局有效,产生了样式覆盖,不得不重新定义样式类或者使用 !important;

平时的项目开发中,还会有一些类似的CSS问题:

  • 全局污染
  • 命名混乱
  • 依赖引入复杂
  • 无法共享变量
  • 代码冗余

使用CSS模块化可以很好的解决上述问题。目前主要分为两类:

一类是彻底抛弃 CSS,使用 JS 或 JSON 来写样式。Radium, jsxstyle ,react-style 属于这一类。优点是能给 CSS 提供 JS 同样强大的模块化能力;缺点是不能利用成熟的 CSS 预处理器(或后处理器) Sass/Less/PostCSS, :hover 和 :active 伪类处理起来复杂。

另一类是依旧使用 CSS,但使用 JS 来管理样式依赖,代表是CSS Modules。CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力。发布时依旧编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。

相关概念

overview

CSS Modules是什么

  • CSS Modules 是使所有的类名animation 动画名称默认为局部样式的CSS文件,它既不是官方标准也不是浏览器特性,而是在构建过程中,对CSS类名限定作用域的一种方法。

原理

  • CSS Modules 产生局部作用域的原理是将每个类名编译成独一无二的哈希串

CSS Modules 仅作用于class和id选择器 对其他选择器无效

使用CSS Modules 有什么优势

  • 所有样式都是 local 的,解决了命名冲突和全局污染问题
  • class 名生成规则配置灵活,可以此来压缩 class 名
  • 可以与CSS预处理器、后处理器配合使用
  • 只需引用组件的 JS 就能搞定组件所有的 JS 和 CSS
  • 依然是 CSS,几乎 0 学习成本...

启用方法

CSS Modules 通过webpack配置引入项目,不依赖于任何框架,只要使用webpack配置后就可以用于React/Vue/Angular/jQuery 项目中.

  1. 在webpack.config.js的module中添加如下配置:
module.exports = {
  entry: __dirname + '/index.js',
  output: {
    publicPath: '/',
    filename: './bundle.js'
  },
  module: {
    rules: [
      ...
      {
      test: /\.css$/,
      use:[
          {loader:'style-loader'},
          {
            loader:'css-loader',
            option:{
                modules:true
            }
          }
        ]
      },
    ]
  }
}

上面代码中,关键的是在css-loader的option里配置option:{modules:true},表示打开 CSS Modules 功能。

使用这种配置方式,css-loader默认将类名编译为唯一的hash串,但不利于class类名的语义化,如:

/*header.css*/
.root {
    text-align: center;
}
.header {
    background-color: #536587;
    height: 150px;
    padding: 20px;
    color: white;
}
.title {
    font-size: 1.5em;
}
/*header.js*/

import React from 'react';
import styles from './header.css';

const Header =()=>{
  return (
    <div className={styles.root}>
        <header className={styles.header}>
          <h1 className={styles.title}>Welcome to React</h1>
        </header>
    </div>
  )
}
export default Header;

<h1/>将被编译为:<h1 class="_1yHZnBWcll0vdb7BCk5Ufm">Welcome to React</h1>

同时 style.css文件将被编译为

    ._1yHZnBWcll0vdb7BCk5Ufm {
        font-size: 1.5em;
    }

因此,需要在配置webpack时多做一点,定制编译生成的哈希类名:

rules:[
  {
    test:/.css$/,
    use:[
      {loader:'style-loader'},
      {loader:'css-loader',
        option:{modules:true,localIdentName:'[path][name]__[local]-[hase:base64:5]'}
        ]
  }
]

配置localIdentName之后 上面的类名将生成 如下格式:

localidentname

CSS Modules的使用

  • 局部变量和全局变量

    • 局部变量 默认 :local 构建时按照 localIdentName 做规则处理
    • 全局变量 需要声明全局变量时,使用:global,全局变量,编译后类名不变
.normal {
 color: green;
}

/* 以上与下面等价 */
:local(.normal) {
 color: green; 
}
/* 定义全局样式 */
:global(.btn) {
 color: red;
}
/* 定义多个全局样式 */
:global {
 .link {
   color: green;
 }
 .box {
   color: yellow;
 }
}
  • Compose 组合Class

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

复用内部样式
.userInfoBox {
 position: relative;
 padding: 0 24px 0 32px;
 height: 40px;
 color: white;
 line-height: 40px;
 background: #1f1f1f;
 transition: background .2s ease 0s;
}

.docDownloadBox {
 composes: userInfoBox;
 margin-right: 10px;
 border-right: 1px solid #444;
}

使用docDownloadBox类的HTML

<a className={styles.docDownloadBox}>
  文档&下载
</a>

编译为两个class
composes

复用外部样式
/* settings.css */
.primary-color {
  color: #f40;
}

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

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

对于大多数项目,有了 composes 后已经不再需要 Sass/Less/PostCSS。但如果你想用的话,由于 composes 不是标准的 CSS 语法,编译时会报错。就只能使用预处理器自己的语法来做样式复用了。

  • 层叠多个class

配合classnames使用

npm install --save classnames

直接使用
/* components/submit-button.js */
import React, { Component } from 'react';
import classNames from 'classnames';
import styles from './submit-button.css';

export default class SubmitButton extends Component {
render () {
  let text = this.props.store.submissionInProgress ? 'Processing...' : 'Submit';
  let className = classNames({
    [`${styles.base}`]: true,
    [`${styles.inProgress}`]: this.props.store.submissionInProgress,
    [`${styles.error}`]: this.props.store.errorOccurred,
    [`${styles.disabled}`]: this.props.form.valid,
  });
  return <button className={className}>{text}</button>;
}
};
使用 classnames/bind
/* components/submit-button.js */
import React, { Component } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';

let cx = classNames.bind(styles);

export default class SubmitButton extends Component {
  render () {
    let text = this.props.store.submissionInProgress ? 'Processing...' : 'Submit';
    let className = cx({
      base: true,
      inProgress: this.props.store.submissionInProgress,
      error: this.props.store.errorOccurred,
      disabled: this.props.form.valid,
    });
    return <button className={className}>{text}</button>;
  }
};
  • 定义和使用变量

CSS Moduels 本身没有变量的概念,如果需要使用变量,要借助预处理器/后处理器。

定义变量

安装 PostCSS 和 postcss-icss-values

  npm install --save postcss-loader postcss-icss-values

把postcss-loader加入webpack.config.js。

{
  test: /\.css$/,
  use: [
    require.resolve('style-loader'),
    {
      loader: require.resolve('css-loader'),
      options: {
        importLoaders: 1,
        modules:true,
        localIdentName:'[name]--[local]--[hash:base64:5]'
      } 
    },
    {
      loader: require.resolve('postcss-loader'),
      options: {
        ident: 'postcss',
        plugins: () => [
          require('postcss-flexbugs-fixes'),
          autoprefixer({
            browsers: [
              '>1%',
              'last 4 versions',
              'Firefox ESR',
              'not ie < 9', // React doesn't support IE8 anyway
            ],
            flexbox: 'no-2009',
          }),
          require('postcss-icss-values')
        ],
      },
    },
  ],
},

然后,定义变量,如variable.css

@value primary  #0c77f8;
@value green    #aaf200;

变量和值之间 写不写冒号都可以,但如果和sass配合使用,不要写分号
一次只定义一个变量

在其他css文件中使用变量,

App.css中引入变量

@value variables: "./variable.css";
@value primary from variables;

//或者直接 @value primary from "variables.css"

.title {
  background-color: primary;
}
css和js共享变量
//variable.css
@value primary #0c77f8;
//App.js
import { primary } from './variable.css';
console.log(primary); // -> #0c77f8

css变量在js中只读

  • 模块化样式和全局样式共存

webpack.conf.js中添加exclude/include配置,定义不同的解析规则即可

// webpack.config.js 局部
{
  test: /\.css$/,
  exclude:/common.css/,
  use: [
    require.resolve('style-loader'),
    {
      loader: require.resolve('css-loader'),
      options: {
        importLoaders: 1,
        modules:true,
        localIdentName:'[name]--[local]--[hash:base64:5]'
      } 
    },
    {
      loader: require.resolve('postcss-loader'),
      options: {
        ident: 'postcss',
        plugins: () => [
          require('postcss-flexbugs-fixes'),
          autoprefixer({
            browsers: [
              '>1%',
              'last 4 versions',
              'Firefox ESR',
              'not ie < 9', // React doesn't support IE8 anyway
            ],
            flexbox: 'no-2009',
          }),
          require('postcss-icss-values')
        ],
      },
    },
  ],
},
{
  test:/\.css$/,
  include:/common.css/,
  use:['style-loader','css-loader','postcss-loader']
},

设置为全局规则的css类,不会被模块化处理。

  • 使用babel-plugin-react-css-modules

babel-plugin-react-css-modules 可以实现使用styleName属性自动加载CSS模块。只需要把className换成styleName即可获得CSS局部作用域的能力,babel插件来自动进行语法树解析并最终生成className。改动成本极小,不会增加JSX的复杂度,也不会给项目带来额外的负担。

import React from 'react';
import './table.css';

export default class Table extends React.Component {
  render () {
    return <div styleName='table'>
      <div styleName='row'>
        <div styleName='cell'>A0</div>
        <div styleName='cell'>B0</div>
      </div>
    </div>;
  }
export default Table;
    

使用babel-plugin-react-css-modules:

  1. 不必使用驼峰命名;
  2. 不用使用styles Object
  3. 可以和全局变量自由组合;

<div className='global-css' styleName='local-module'></div>

参考文献:

  1. CSS Modules 详解及 React 中实践
  2. CSS Modules 用法教程 - 阮一峰的网络日志
  3. CSS Modules 入门及 React 中实践 | AlloyTeam
  4. GitHub - css-modules/css-modules: Documentation about css-modules
  5. GitHub - css-modules/postcss-icss-values: Pass arbitrary constants between your module files
  6. GitHub - gajus/babel-plugin-react-css-modules: Transforms styleName to className using compile time CSS module resolution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions