前端开发者学堂 - fedev.cn

React中CSS Modules的使用

发布于 大漠

最近项目开始换React的工程,感觉好多东西都得重新开始,特别在撸CSS的时候。说实话和Vue的工程相比,体感差完了。在Vue的工程中除了Modules之外还可以使用CSS的作用域scoped的概念。用久了Vue的同学,在这方面的感觉爽死了,但是突然切到React的工程体系之下,这方面的感觉突然不要不要。拿个实例来说吧(可能我做得不到位),组件的CSS是全局的,有时覆盖起来,除了蛋疼之外,而费时,费成本。

React项目中写CSS的姿势

比如最简单的一个按钮组件,居然要这么撸:

虽然为每个组件创建一个单一的.scss文件,并在入口引入相应的样式文件,发现React中的CSS没有域的概念,是全域的。

很多时候需要去覆盖组件初始样式,不得不重新定义样式类或提高选择器权重来处理。着实的蛋疼。除此之外,项目是多人开发,各种各样的类名都有,未统一起来,从覆盖上也增加了不少工作量。另外,还会碰到一些常见的CSS问题,比如:

  • 全局污染
  • 命名混乱
  • 依赖关系复杂
  • 无法共享变量
  • 代码冗余,难维护

所以最近在重新考虑如何在React项目中编写CSS。以便找到一条更适合自己甚至团队编写、维护CSS的方式:

  • 行内样式(在JS中写CSS,最终样式编译到标签元素的style内)
  • CSS-in-JS,较为流行的有styled-componentsstyled-jsxreact-style
  • CSS功能模块(Functional CSS),比如tailwindcsstachyons等(怎么看都有点类似早期的OOCSS)
  • CSS Modules

初步对比了React中编写CSS的几种方式,我个人更趋向于CSS Modules,不过在React项目中配置CSS Modules要比在PostCSS或者Vue中配置复杂的多(主要还是自己对Webpack太弱)。这次在配置的时候踩了一些坑,特意梳理一下,以备后用。

我要的目标

习惯了Vue的开发,开始撸React还真不习惯。而我想要的目标是:

React中编写CSS能不能像Vue一样,有作用域的概念,组件的CSS只作用于相应的组件,并不会影响其他组件。

为什么选择CSS Modules

时至今日都在提模块化管理,而前面提到的在React中处理样式的方案都有模块化的身影。而CSS模块化的解决方案主要分为:

  • 彻底抛弃CSS,使用JavaScript或JSON来写,比如这两年聊得多的CSS-in-JS。其优点是能给CSS提供JavaScript同样强大的模块化能力;缺点是不能使用CSS处理器以及较难处理伪类选择器的样式
  • 依旧使用CSS,但借助JavaScript来管理样式的依赖关系,比如我们今天要聊的CSS Modules。其最大的特点就是最大化的结合了CSS生态和JavaScript模块化能力

不管是哪种CSS模块化,其主要目的是解决:

  • CSS样式的导入和导出:灵活的按需导入以便最大化的复用代码;导出时能隐藏内部作用域,以免造成不必要的全局污染
  • 解决CSS的编程能力:虽然CSS的处理器(比如Sass、LESS、Stylus和PostCSS)赋予了CSS一些编程能力,但还有有鸡肋之处,它们依旧无法解决模块化最重要的问题

而在React中编写CSS时,有关于这些方面所涉及到的问题表现的更为真切,比如上面提到的:

全局污染

众所周知,CSS是没有作用域名的概念(虽然CSS自定义属性的出现,解决了一些作用域的问题),因此写的样式都是一个全局的。很多时候要去覆盖这些样式,也因此会造成样式可能被错误覆盖。搞不好,你会看到好多样式中会有!important这样的关键词,甚至更为离谱的是,在行内样式中还会有!important身影。

另一个更为复杂的是CSS选择器权重,更易于让CSSer犯错,从而也提高了样式代码覆盖的成本。仅管Web Components中的Shadow DOM能彻底解决这个问题,但它的做法有点极端,样式彻底局部化,造成样式重写难度,从而损失了灵活性。

命名混乱

由于全局污染的问题,加上多人协同开发,最易于造成的问题就是样式冲突。为了避免样式冲突,对于元素的命名就有着更高的要求,虽然很多CSS的方法论(比如BEM、OOCSS、Atomic、ITCSS等)可以让我们在编写CSS时尽可能的避免命名的冲突(样式混乱),但并没有彻底解决问题(特别是在一些新生团队),在写CSS的时选择器会非常的复杂(复杂到有超六、七层的嵌套,而最佳的是不超三层),而且命名风格还很难统一。

工程越大,人员越多,命名越乱,选择器越复杂,样式越难覆盖 —— 死循环

依赖管理不彻底

编写组件最理想的状态 —— 应该相互独立。在引入一个组件时,应该只引入组件自己所需的样式。但现在的做法是除了要引入JavaScript之外,还要引入它的CSS(而CSS处理器以很难做到每个组件编译出单独的CSS)。而在独立的页面中引入所有CSS又会造成不必要的浪费(这也是我不喜欢而没选择Functional CSS原因之一)。

虽然JavaScript模块化已经非常成熟了。比如借助Webpack的css-loader的能力,可以帮助我们管理CSS依赖。这也是目前较好的方案之一。

无法共享变量

复杂组件要使用JavaScript和CSS来共同处理样式(特别是一些强交互的组件),就会造成有些变量在JavaScript和CSS中冗余。而CSS的处理器是无法提供跨JavaScript和CSS共享变量的这种能力。

值得庆幸的是,CSS的自定义属性可以让我们在JavaScript和CSS共享变量的能力(注意,其实不是变量,是CSS自定义属性)。

代码压缩不彻底

很多压缩工具对于较常的class类名压缩却无能为力。

事实上,上面提到的这几点都是CSS中一直以来存在的,而又难以解决的。不过借助JavaScript的能力来管理CSS的话,问题就会变得简单的多。这也是CSS-in-JS流行的主要原因,也就出现前面提到的现象,以对象的方式在JavaScript中撸CSS,从而也让不少的同学难以接受这种方式。但CSS Modules的出现,既可以借助JavaScript能力来管理CSS,也方便了CSSer撸代码的习惯,可以说一举两得。这也是为什么选择CSS Modules主要原因。

另外一点,CSS经过这么多年的发展,从SMACSSOOCSS,再到BEM,可谓是不断的在优化和改进。而CSS Modules与实践单一职责原则的Web组件非常匹配。

我更为好奇的是CSS Modules在设计前端系统中的可能性。CSS Modules基本上是CSS文件,默认情况下类名的作用域名是局部的(本地的)。在任何语言中,全局作用域都被认为是一种不好的实践,而CSS却又是这样的一种模式,只不过这么多年来,大家都无耐的接受了这样的一个现实,也已经学会了在CSS中如何使用它。有了CSS Modules(和其他一些模式),我们就可以跳出CSS的全局作用域,构建模块化系统。

很多同学一直觉得CSS非常简单。事实上,CSS一开始的确很简单,但随着项目的增长,CSS会变得越来越复杂,越来越难以编写和维护,即使是专业的开发人员(CSS大神)也会发现很难在复杂的大型系统中维护和组织样式。(事实上,我也非常的害怕,特别是和一些不太了解CSS的同学一起开发项目)。

CSS Modules的出现,其主要目的就是解决CSS的问题

通过封装CSS的组件作为闭包,从而遵循组件单一的职责

如何在工程体系中构建CSS Modules

既然CSS Modules有这么大的优势,那么我们就需要在工程中构建CSS Modules。那么问题来了,如何构建CSS Modules呢?这是当下我们要去解决的问题。

使用Webpack来构建CSS Modules,这也是目前主流方式之一。

而在不同的工程中构建CSS Modules方式是不一样的,比如PostCSS、Vue、React等不同的工程体系中,构建的方式都不一样。除了PostCSS之外借助PostCSS相关插件,其他的工程体系(不管是Vue还是React),都可以借助Webpack来构建。而使用Webpack在React项目中构建CSS Modules又不是一件难事。

对于我这样不太了解Webpack工作机制的人来说,还是一件难事。最起码我觉得比Vue环境下难得了。这个时候我需要一位Webpack高级配置工程师和我一起来构建CSS Modules。

抛开所有JavaScript框架而言,不管是在哪处框架底下,都可以通过Webpack下的css-loader在配置文件中的加载器选项来启用它。当然,如果你还依赖Sass或PostCSS这样的开发套件的话,那么会相对的增加配置难度。不过,只要一步一步来,一切都不是太大的问题。接下来的内容就来看如何在React的开发环境上构建CSS Modules。

接下来的内容主要来看如何在React环境下配置CSS Modules以及如何使用CSS Modules。

CSS Modules在create-react-app休系下的的使用

React社区中有一个构建React开发体系的工具Create React App,俗称create-react-app,有点类似于Vue社区中的Vue-CLI

在这里不会阐述Create React App如何使用,更多的是会聊聊CSS Modules在Create React App构建的工程体系下如何工作。为了更好的用示例向大家演示CSS Modules的使用,我在Github上创建了一个仓库,在不同的分支下能看到相应的Demo效果

使用Create React APP创建项目

你可以直接从Github上克隆我创建的示例:

git clone git@github.com:airen/css-modules.git
cd css-modules
cd react-modules

安装工程所需要的包:

npm i
npm start

这样工程就可以跑起来了。或者你使用:

npx create-react app <project-name> // 我这里创建的项目名称react-modules
cd project-name
npm start

你将看到的初始效果如下:

上面的这一切都并不重要,重要的是下面的内容 —— React中CSS Modules的使用

注意,我写使用案例时的基本环境是node: v10.9.0npm: v6.9.0Create React APP(v2)。不同环境,估计效果略有差异,最终以你本地电脑运行结果为准。

使用Create React App 第二版本构建的React项目,在使用CSS Modules时,不需要做任何的配置。只需要创建.css.sass.sass文件时有相应的要求,即**使用 [name].module.css 文件命名约定支持 CSS Modules 和常规 CSS 。 CSS Modules 允许通过自动创建 [filename]\_[classname]\_\_[hash] 格式的唯一 classname 来确定 CSS 的作用域。同样的,如是要是.sass.scss的话,文件名格式应该是[name].module.sass[name].module.scss

或许你会好奇,不需要配置就具备CSS Modules运行环境吗?事实上的确如此。当然,Create React APP环境默认配置了一些功能,如果这些功能达不到你工程所需的要求,那就需要手动进行配置。只不过Create React APP的配置文件隐藏的较深,需要执行不可逆转的命令:

npm run eject

执行完上面的命令之后,会新增两个目录script/config/

config/目录下可以看到两个配置文件webpack.config.jswebpackDevServer.config.js。如果你需要配置所需的功能,可以在webpack.config.js中添加配置。如果你需要将项目打包输出的话,还得配置webpack.config.prod.js。具体如何配置,这里不说了。因为太复杂了。在后面的内容,我们会聊聊Webpack中怎么配置CSS Modules(纯Webpack环境之下,即不依赖Create React APP构建的项目工程)。

CSS Modules的基本使用

前面提到过了,Create React APP默认具备了CSS Modules的功能:

在接下来的内容把重点放在CSS Modules的使用上。不管是在哪种环境之下使用,CSS Modules的使用都不会有太大差异,只会稍微的细节上的差异。

CSS Modules中类名的使用

将分支切换到demo1查看示例代码。

类名的使用是CSS中最基础的部分,那么CSS Modules中类名又是如何使用呢?第一个示例就向大家演示CSS Modules中类名的使用。

首先创建了一个基本组件Button

|--src/
|----Button/
|------Button.js            // 组件模板在这个文件中构建
|------Button.module.css    // CSS代码在这个文件中书写

前面提到过了,Create React APP中使用默认的CSS Modules功能,创建.css文件时需要以[name].module.css格式创建,正如上面示例中的Button.module.css所示。

这个组件非常简单,就是一个按钮:

// Button.js
import React from 'react';
import styles from './Button.module.css';

class Button extends React.Component {
    render() {
        return (
            <div className={styles.button} role="button">Click Me</div>
        )
    }
}

export default Button;

Button.module.css中的代码也非常的简单:

// Button.module.css
.button {
    --primary: #fe90af;
    --color: #fff;

    background: var(--primary);
    color: var(--color);

    padding: 5px 10px;
    border-radius: 4px;
    margin: 5px;
}

调用Button组件之后,编译出来的HTML会像下面这样:

<div class="Button_button__1o_YA" role="button">Click Me</div>

对应的CSS的选择器.button编译成了Button_button__1o_YA,而CSS样式编译成:

.Button_button__1o_YA {
    --primary: #fe90af;
    --color: #fff;
    background: var(--primary);
    color: var(--color);
    padding: 5px 10px;
    border-radius: 4px;
    margin: 5px;
}

效果如下:

CSS Modules将本地名.button编译成全局名.Button_button__1o_YA。在组件中(Button)可以使用像.button的名称来声明类名,而不必担心类名的冲突。如果你不信,可以写一个简单的小示例,假设在App.js中使用一个带.button类名的元素,看看是否会受组件Button中的.buttonn的影响:

<div className="button">我是一个带button类名的元素</div>

渲染出来的结果告诉我们,组件中的类名不会影响别的组件中的同类名。这已经达到我们想要的两个目的 —— 解决CSS的命名冲突不会污染全局

顺便说一下,.Button_button__1o_YA中的__1o_YA是一个随机的hash值,以确保具有相同名称的多个CSS Modules的唯一性。

在Webpack的配置文件webpack.config.dev.js中可以,配置CSS Modules编译器如何重写类名,并且hash值是可选的。

在继续往下介绍CSS Modules中类的使用前先打断一下,我们在引用Button.module.css是以JavaScript对象的方式引入:

import styles from './Button.module.css';

其中styles变量是一个JavaScript对象,如果将styles打印出来,你会看到像下面这样的结果(就Button组件的代码为例):

console.log(styles)

你或许习惯了import './App.css';这种姿势引入CSS文件,但使用CSS Modules时,还是需要稍作改变。不过有技术手段达到您想要的方式,稍后会聊到这方面的知识。

不少同学平时写类名喜欢采用BEM的方式,或者喜欢使用带中折号---方式。为了验证我们所说的,把App.css更换成CSS Modules可以识别的文件格式App.module.css,并在文件中使用带中折号声明的样式,在调用的时候会报错:

.App-header {}

<header className={styles.App-header}></header>

这个时候在编译的时候将会报错:

稍后的方法中我们会介绍,怎么处理这种方式,让其生效。

默认情况之下,使用中折号声明的类名在CSS Modules中是不生效的。这个时候需要将其更换成驼峰的书写格式,即将.App-header换成.AppHeader

.AppHeader { }

<header className={styles.AppHeader}></header>

这个时候CSS Modules能正常的编译:

虽然不能使用带有中折号-或双中折号--命名的类名,但可以接受下划线命名的类名(___):

.App_logo { //... }

.App__header { //... }

<header className={styles.App__header}>
    <img src={logo} className={styles.App_logo} alt="logo" />
</header>

另外需要注意一点,如果使用import styles from '[name].module.css'方式引入CSS(即开启了CSS Modules方式),在模板中(JSX)中未使用styles.[classname],那么编译的时候会被忽略(哪怕是使用className引用),比如:

<div className="App"></div>

这样调用,类名.App并未生效:

值得注意的是,如果你在CSS文件中编写了一些CSS,而在模板中没有使用它们(或错误的使用它们),Webpack将跳过生成输出任何未使用的CSS,从而能减少代码大小。

CSS Modules中多类名的使用

请把分支切换到demo2查看示例代码。

很多时候,在写代码的时候,会使用多个类名来控制元素的样式。同样拿Button组件来举例。我们可以借助ES6的相关特性来给元素调用多个类名:

<div className={`${styles.button} ${styles.buttonPrimary}`} role="button">Button</div>

前面提到过了,styles是一个JavaScript的对象,所以使用上面那种方式引用多个类名,可以正常的工作。上面的代码编译之后的结果是:

<!-- 编译出来的HTML -->
<div class="Button_button__1o_YA Button_buttonPrimary__2NC-O" role="button">Button</div>

// 编译出来的CSS
.Button_button__1o_YA {
    --primary: #f36;
    --color: #fff;

    padding: 5px 10px;
    font-size: 1rem;
    margin: 5px;
    border: 1px solid currentColor;
    border-radius: 5px;
    cursor: pointer;
    transition: all .28s ease;
}

.Button_buttonPrimary__2NC-O {
    background: var(--primary);
    color: var(--color);
}

.Button_buttonPrimary__2NC-O:hover {
    box-shadow: 0 0 3px 5px var(--primary);
}

上面示例演示的效果非常的简单,.buttonPrimary是一个主色按钮,在<div>上同时引用.button.buttonPrimary两个类名的主要作用是.buttonPrimary同时具备按钮的基本样式.button

如果你不习惯上面那种设置多类名的方式的话,可以使用CSS Modules中的另一个特性 —— 组合特性composes。那么上面的示例,我们可以修改成像下面这种方式:

// Button.module.css

.button { 
    //...
}

.buttonPrimary {
    composes: button;
    background: var(--primary);
    color: var(--color);
}

.buttonPrimary:hover {
    box-shadow: 0 0 3px 5px var(--primary);
}

<!-- Button.js -->
<div className={styles.buttonPrimary} role="button">Button</div>

编译出来的结果:

<!-- HTML -->
<div class="Button_buttonPrimary__2NC-O Button_button__1o_YA" role="button">Button</div>

<!-- style -->
.Button_button__1o_YA {
    --primary: #f36;
    --color: #fff;

    padding: 5px 10px;
    font-size: 1rem;
    margin: 5px;
    border: 1px solid currentColor;
    border-radius: 5px;
    cursor: pointer;
    transition: all .28s ease;
}

.Button_buttonPrimary__2NC-O {
    background: var(--primary);
    color: var(--color);
}

.Button_buttonPrimary__2NC-O:hover {
    box-shadow: 0 0 3px 5px var(--primary);
}

最终得到的效果是一样的。

CSS Modules最大的特色之一就是“反正全局CSS,每个CSS代码都是应该用于特定的组件中,并且与项目的其他部分是隔离的”。也正因为这个特性,刚接触CSS Modules的同学可能会担心,如果多个组件中要使用相同的CSS时是不是要写多个CSS(做着相同的事情)。事实上大家一点都不用担心,CSS Modules的composes特性可以用来跨文件引用相同的样式(上面演示的是同一文件引用类样式)。在上面的示例上稍作调整。比如我们在App.module.css中要引用Button.module.css中的.button中的样式代码,我们就可以像下面这样来写:

<!-- App.module.css -->
.composeClass {
    composes: button from './Button/Button.module.css';
    composes: buttonPrimary from './Button/Button.module.css';
}

<!-- Button.module.css -->
.button {}

.buttonPrimary {}

.buttonPrimary:hover {}

<!-- App.js -->
<div className={styles.composeClass}>合并别的文件中的类</div>

编译出来的结果:

<!-- HTML -->
<div class="App_composeClass__23fyv Button_button__1o_YA Button_buttonPrimary__2NC-O">合并别的文件中的类</div>

<!-- Style -->
.App_composeClass__23fyv {

}

.Button_buttonPrimary__2NC-O {
    background: var(--primary);
    color: var(--color);
}

.Button_button__1o_YA {
    --primary: #f36;
    --color: #fff;
    padding: 5px 10px;
    font-size: 1rem;
    margin: 5px;
    border: 1px solid currentColor;
    border-radius: 5px;
    cursor: pointer;
    transition: all .28s ease;
}

最终的效果是一样的:

CSS Modules中的composes特性有点类似于Sass中的@extend,可以扩展@mixins%placeholder中声明的样式。

如此一来,可以把各个组件共用的样式放置在一个独立的文件中,比如common.css。然后在需要的地方调用。来看一个小示例:

// common.css
.primary {
    --primary: #f36;
}
.white {
    --color: #fff;
}
.p10 {
    padding: 10px;
}

// Button.module.css
.buttonPrimary {
    composes: primary from '../common.css';
    composes: white from '../common.css';
    background: var(--primary);
    color: var(--color);
}

// App.module.css
.composeClass {
    composes: primary from './common.css';
    composes: white from './common.css';
    composes: button from './Button/Button.module.css';
}

<!-- Button.js -->
<div className={styles.buttonPrimary} role="button">Button</div>

<!-- App.js -->
<div className={styles.composeClass}>合并别的文件中的类</div>

编译出来的结果:

<!-- HTML -->
<div class="Button_buttonPrimary__2NC-O Button_button__1o_YA" role="button">Button</div>
<div class="App_composeClass__23fyv common_primary__3GgzT common_white__1wVkU Button_button__1o_YA">合并别的文件中的类</div>

<!-- Style -->
<style type="text/css">
    .common_primary__3GgzT {
        --primary: #f36;
    }
    .common_white__1wVkU {
        --color: #fff;
    }
    .common_p10__1huS7 {
        padding: 10px;
    }
</style>

<style type="text/css">
.Button_button__1o_YA {
    --primary: #f36;
    --color: #fff;

    padding: 5px 10px;
    font-size: 1rem;
    margin: 5px;
    border: 1px solid currentColor;
    border-radius: 5px;
    cursor: pointer;
    transition: all .28s ease;
}

.Button_buttonPrimary__2NC-O {
    background: var(--primary);
    color: var(--color);
}

.Button_buttonPrimary__2NC-O:hover {
    box-shadow: 0 0 3px 5px var(--primary);
}
</style>
<style type="text/css">
.App_App__16ZpL {
    text-align: center;
}

.App_AppLogo__2NrNP {
    animation: App_App-logo-spin__1e7sv infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
}

.App_AppHeader__2Hhu3 {
    background-color: #282c34;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
}

.App_AppHink__3RXs- {
    color: #61dafb;
}

@keyframes App_App-logo-spin__1e7sv {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}


.App_composeClass__23fyv {
}
</style>

在使用CSS Modules时,composes是非常强大的,可以让你跳出CSS Modules局部作用域名的限制,在组件中(局部作用域)调用共用样式(全局作用域)。这一点非常有用,可以让我们很好地在CSS Module中管理共用样式。比如你想使用Bootstrap中的CSS代码,或者你想使用tailwindcss(Functional CSS典型的作品之一)时,就可以采用该特性。

继续加到CSS Modules中多类名的使用中来。虽然composes可以达到

className={`${styles.primary} ${styles.white}`}

类似的功能。但对于习惯于class="primay white"的开发同学来说,或多或少有所不习惯。其实除了上面这两种方法之外,还可以借助classnames模块,让多类名的使用变得更为轻便。

如果需要使用classnames的话,需要先执行下面的命令来安装该功能模块:

npm install classnames --save

并在需要使用该功能的.js中像下面这样引入:

// App.js
import cn from 'classnames';

那么就可以像下面这样在元素上同时使用多个类名:

<!-- App.js的模板中 -->
<div className={cn(styles.border, styles.box)}>classnames模块</div>

// App.module.css
.border {
    border: 2px solid #f25;
}

.box {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 10px;
    color: #fff;
    border-radius: 4px;
    margin: 5px;
}

编译出来的结果如下:

<!-- HTML -->
<div class="App_border__3Ekwc App_box__aqO3i">classnames模块</div>

<!-- Style -->
.App_box__aqO3i {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 10px;
    color: #fff;
    border-radius: 4px;
    margin: 5px;
}

.App_border__3Ekwc {
    border: 2px solid #f25;
}

有关于classnames更详细的介绍和使用可以查阅官方文档

CSS Modules中的组合选择器的使用

请将分支切换到demo3查看示例代码。

CSS中组合选择器的使用是非常常见的。那么在CSS Modules中的组合选择器怎么使用呢?咱们先来看后代选择器(也常称嵌套选择器)。

// App.module.css
.button {
    padding: .5rem;
    margin-top: .5rem;
    border: 1px solid #2F79AD;
    border-radius: 4px;
    background-color: #6DB9EE;
}

.fun .button {
    font-weight: bold;
    background: linear-gradient(
        90deg,
        #ff0000, #ffff00,
        #00ff00, #00ffff,
        #ff00ff, #ff0000
    );
}

<!-- App.js -->
<button className={styles.button}>Regular Button</button>
        
<div className={styles.fun}>
    <button className={styles.button}>FUN BUTTON</button>
</div>

编译出来的结果如下:

<!-- HTML -->
<button class="App_button__13pio">Regular Button</button>

<div class="App_fun__1wmPx">
    <button class="App_button__13pio">FUN BUTTON</button>
</div>

<!-- Style -->
<style type="text/css">
    /* .... */

    .App_button__13pio {
        padding: .5rem;
        margin-top: .5rem;
        border: 1px solid #2F79AD;
        border-radius: 4px;
        background-color: #6DB9EE;
    }

    .App_fun__1wmPx .App_button__13pio {
        font-weight: bold;
        background: linear-gradient(
            90deg,
            #ff0000, #ffff00,
            #00ff00, #00ffff,
            #ff00ff, #ff0000
        );
    }
</style>

效果如下:

再来看一个子选择器的示例:

// App.module.css
.box > .button {
    background: linear-gradient(to right, #f36, #f90);
    color: #fff;
}

// App.js
<div className={styles.box}>
    <button className={styles.button}>BOX BUTTON</button>
</div>

编译出来的结果如下:

<!-- HTML -->
<div class="App_box__aqO3i">
    <button class="App_button__13pio">BOX BUTTON</button>
</div>

<!-- Style -->
<style type="text/css">
    /* ... */
    .App_button__13pio {
        padding: .5rem;
        margin-top: .5rem;
        border: 1px solid #2F79AD;
        border-radius: 4px;
        background-color: #6DB9EE;
    }

    .App_box__aqO3i > .App_button__13pio {
        background: linear-gradient(to right, #f36, #f90);
        color: #fff;
    }
</style>

至于组合选择器,其实和前面提到的多类名的使用有点类似。只是稍微在细节上有所不同。比如基于上面的.button再加一个.disabled的样式,而且使用的是组合选择器:

// App.module.css

.button.disabled {
    background-color: #aaa;
    border-color: #999;
}

// App.js
<button className={`${styles.button} ${styles.disabled}`}>Disabled Button</button>

编译出来的结果我想你也能猜得到一二:

<!-- HTML -->
<button class="App_button__13pio App_disabled__sQeTY">Disabled Button</button>

<!-- Style -->
<style type="text/css">
    /* ... */
    
    .App_button__13pio {
        padding: .5rem;
        margin-top: .5rem;
        border: 1px solid #2F79AD;
        border-radius: 4px;
        background-color: #6DB9EE;
    }

    .App_button__13pio.App_disabled__sQeTY {
        background-color: #aaa;
        border-color: #999;
    }
</style>

至于其他的组合选择器的示例就不再演示了。感兴趣的可以自己尝试一下。

CSS Modules中选择器的作用域

请把分支切换到demo4查看示例代码。

通过上面内容,应该都了解到CSS Modules中的选择器默认都是局部作用域名的。其实在CSS Modules中还有两个标识符可以用来区别局部作用域名和全局作用域。

  • :local:局部作用域名,相当于组件中的本地选择器
  • :global:全局作用域名

我们来看一个小示例,假设我们在App.module.css中的某个选择器前面使用:global来标识该选择器是全局的:

// App.module.css
.App {
    text-align: center;
}

:global .App-logo {
    animation: App-logo-spin infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
}

// App.js
<div className={styles.App}>
    <header className={styles.AppHeader}>
        <img src={logo} className="App-logo" alt="logo" />
    </header>
</div>

编译出来的结果如下:

<!-- HTML -->
<div class="App_App__16ZpL">
    <header class="App_AppHeader__2Hhu3">
        <img src="/static/media/logo.5d5d9eef.svg" class="App-logo" alt="logo">
    </header>
</div>

<!-- Style -->
<style type="text/css">
    .App_App__16ZpL {
        text-align: center;
    }

    .App-logo {
        animation: App-logo-spin infinite 20s linear;
        height: 40vmin;
        pointer-events: none;
    }
    /* ... */
</style>

不知道大家是否有留意到,采用:global声明的全局选择器,和我们平时声明类名无差异,而且也不需要遵循CSS Modules的类名命名规则,并且在模板中调用的时候,也不需要使用styles.xxx这样的方式,可以直接在className中引用类名。比如上例中的className="App-logo"。而且还可以跨组件的使用,比如我们在Button组件的Button.module.css中使用:global声明了一个全局的.button样式:

// Button.module.css
:global .button {
    padding: .5rem;
    margin-top: .5rem;
    border: 1px solid #2F79AD;
    border-radius: 4px;
    background-color: #6DB9EE
}

同时我们在App组件中也有一个Button,同时共用了Button组件中的.button类:

// App.js
<div className={`${styles.primary} button`} role="button">Primary Button</div>

// App.module.css
.primary {
    background: linear-gradient(to right, #f35, #d0f);
    color: #fff;
}

编译出来的结果如下:

<!-- HTML -->
<div class="App_primary__2eHgs button" role="button">Primary Button</div>

<!-- Style -->
.button {
    padding: .5rem;
    margin-top: .5rem;
    border: 1px solid #2F79AD;
    border-radius: 4px;
    background-color: #6DB9EE;
}

.App_primary__2eHgs {
    background: linear-gradient(to right, #f35, #d0f);
    color: #fff;
}

从编译出来的结果可以看到,跨组件中使用:global标识符声明的类名可以全局中使用。

这种场景也是非常有用的。比如使用JavaScript生成的类,它是一个全局作用域的,而不是局部作用域。比如你需要给当前的菜单项添加一个.active类名时(由于使用JavaScript或React自己身的方法生成的类都是全局作用域的),:global声明的类就非常有用了,比如:

:globa .active {
    color: #fff;
    border-bottom: 1px solid #f3450a;
}
CSS Modules中的@规则的使用

请把分支切换到demo5查看示例代码。

在CSS中写样的的时候,我们会遇到使用@规则的样式,比如媒体查询@media条件CSS判断@supports以及动画关键帧@keyframes等。那么在CSS Modules中会有什么不同吗?我们来看两个有关于@规则的使用,一个是媒体查询@media,另一个是@keyframes

比如,在App.module.css中使用@keyframes创建了一个App-logo-spin的动画:

@keyframes App-logo-spin {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

并且该动画运用于.AppLogo上:

.AppLogo {
    animation: App-logo-spin infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
}

最后编译出的代码如下:

<!-- HTML -->
<img src="/static/media/logo.5d5d9eef.svg" class="App_AppLogo__2NrNP" alt="logo">

<!-- Style -->
<style type="text/css">

    .App_AppLogo__2NrNP {
        animation: App_App-logo-spin__1e7sv infinite 20s linear;
        height: 40vmin;
        pointer-events: none;
    }

    @keyframes App_App-logo-spin__1e7sv {
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
    }
</style>

动画名称App-logo-spin会自动添加相应的hash值,变成了App_App-logo-spin__1e7sv,并且运用于相应的元素之上。

再来看一个@media的运用:

// App.module.css

@media (max-width: 600px) {
    .AppLogo {
        opacity: .6;
    }
}

编译出来的代码如下:

@media (max-width: 600px){
    .App_AppLogo__2NrNP {
        opacity: .6;
    }
}

.App_AppLogo__2NrNP {
    animation: App_App-logo-spin__1e7sv infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
}
CSS Modules中标签元素和属性选择器的使用

在CSS Modles中,类名是被限定在定义它的组件中使用(除非使用了:global标识的类),但对于HTML的标签元素和属性选择器是不受影响的。比如,在Button组件中声明了下面的样式代码:

// Button.module.css
[href^="https:"]{
    color: salmon;
    border-bottom: 3px double currentColor;
    padding-bottom: 5px;
}

a{
    text-decoration: none;
}

虽然属性选择器和标签元素是在Button组件中声明的,但其还是成为全局的样式,被运用到了App组件中的带有href="https://"开头的<a>标签上。来看看编译出来的代码:

<!-- HTML -->
<a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a>

<!-- Style -->
<style type="text/css">
    [href^="https:"]{
        color: salmon;
        border-bottom: 3px double currentColor;
        padding-bottom: 5px;
    }

    a{
        text-decoration: none;
    }
</style>

效果如下:

如果你不想让标签元素和属性选择器对应的样式被作用于全局的话,可能使用局部作用域名的类名来稍加限制,比如:

.panelBody [href^="https:"]{
    color: salmon;
    border-bottom: 3px double currentColor;
    padding-bottom: 5px;
}

这样只会作用于.panelBody容器下带有href="https:"和元素。

另外,在CSS Modules中使用id声明的样式不会被运用到元素上,比如:

// Button.module.css
#button {
    padding: .5rem;
    margin-top: .5rem;
    border: 1px solid #2F79AD;
    border-radius: 4px;
    background-color: #6DB9EE;
}

// Button.js
<div role="button" id="button">Button</div>

虽然JSX模板中的div显式声明了id名称,并且和样式中的#id相同,但样式并没有运用到相应的元素上。如果我们把styles对象打印出来,看到有值输出:

顺便再来看一下伪元素或伪类选择器在CSS Modules中的使用。比如:

// Button.module.css
.btn::after{
    content: '外部链接';
}

如果组件中显式调用了.btn类,伪元素::after也会相应的编译出来:

CSS Modules和全局样式共存

CSS Modules中声明的样式仅运用于相应的组件范围内,帮助我们解决了CSS中很多痛苦的问题。而事实上,我们很多时候可能会引用于第三方库的样式,比如前面提到的Bootstrap的样式。甚至很多时候希望要一些共用样式,比如基础样式normalize.css。如果需要让CSS Modules和全局样式共存的话,就需要借助Webpack的能力,在配置上做相应的设置。比如:

rules: [
{
    test: /\.css$/,
    include: '/src/app',
    use: [
        {
            loader: 'style-loader',
        },
        {
            loader: 'css-loader',
            options: {
                sourceMap: true,
                modules: true,
                localIdentName: '[local]___[hash:base64:5]'
            }
        }
    ],
},
{
    test: /\.css$/,
    include: '/src/styles',
    use: [
        {
            loader: 'style-loader',
        },
        {
            loader: 'css-loader',
            options: {
                sourceMap: true
            }
        }
    ],
}]

比如上面所示的配置,在/src/app/目录下的CSS将会遵循CSS Modules的能力,将相应的类名编译成随机的hash值,而在/src/styles/目录下是一些全局样式文件,不会被编译。

CSS Modules中样式覆盖

请把分支切换到demo7查看示例代码。

CSS Modules虽然随机生成了带有hash值的类名,在多人协作的时候再也不用担心类名的冲突。但随之也来了新的问题,我们始终无法预知最终生成的类名是什么。而且同一个组件有可能会在不同的地方调用,在样式如果有细节上的差异,就难免需要组件原有样式进行覆盖。那么在CSS Modules环境之下,又如何来覆盖原有的样式呢?

比如,我们在Button组件中在.button中定义了一个样式:

// Button.module.css
.button {
    padding: .5rem;
    margin-top: .5rem;
    border: 1px solid #2F79AD;
    border-radius: 4px;
    background-color: #6DB9EE;
}

虽然在App组件中调用了Button组件,并且也使用了className调用一个新的类,比如primary

<Button className={styles.primary}></Button>

同时在App.module.css重新定义了样式:

.primary {
    background: linear-gradient(to right, #f36, #90a);
    color: #fff;
}

你是不是和我一样认为能覆盖呢?事实上并非如此,未能如愿。主要是className={styles.primary}并未编译到Button组件的按钮标签中:

<div class="Button_button__1o_YA" role="button">Button</div>

这问题出于组件设计不够好,无法透传别的类名。暂时先不表,后面再来聊这个透传类名的事情。

那么我们有没有别的办法可以进行覆盖呢? 想过使用组合选择器,可是和前面一样,找不到区别按钮的标识符。那再换过一种方式,在调用组件的时候,显式声明一个data-*的自定义属性,比如:

// App.js
<Button data-primary-button="primary"></Button>

// App.module.css
[data-primary-button="primary"] {
    background: linear-gradient(to right, #f36, #90a);
    color: #fff;
}

事实上还是未生效。再想个曲线救国,牺牲HTML结构:

// App.js
<div data-primary-button="primary">
    <Button />
</div>

<div className={styles.primary}>
    <Button />
</div>
        
// App.module.css
.primary > div,
[data-primary-button="primary"] > div{
    background: linear-gradient(to right, #f36, #90a);
    color: #fff;
}

样式覆盖成功了:

重新创建一个Button1组件,定义一个data-type的自定义属性,并且能动态传值:

// Button1.js

import React from 'react';
import styles from './Button1.module.css';

console.log(styles);

class Button1 extends React.Component {
    constructor(props) {
        super(props);
        this.state = {}
    }
    render() {
        const {type} = this.props;
        console.log(this.type)
        return (
            <div className={styles.button} data-type={`${type}`} role="button" >Button</div>
        )
    }
}

export default Button1;

App组件中引用Button1组件,并且通过动态传的type值来覆盖样式:

// App.js
<Button1 type="success" />

同时在App.module.css中设置需要覆盖按钮的样式:

[data-type="success"] {
    background: linear-gradient(to bottom, #f09, #adf);
}

注意属性选择器在CSS Modules是全局的,其样式将会覆盖Button1组件中的.button样式:

刚撸React,上面透传参数的案例有可能写得不到位,欢迎路过的大婶能指正其中不对之处。

相对而言,CSS Modules中要覆盖组件中的样式会比较蛋疼。在设计组件的时候,千万要注意这个细节,并全需要留好口子。当然,或许还有其他的方式,只是我自己还没有领略到其中的奥秘之处。

CSS Modules中CSS和JavaScript共享变量

CSS Modules另一个优势是能够从CSS文件中导出变量用于JavaScript中。在CSS Modules中可以导出Sass、LESS中的变量或者任何CSS的属性。比如:

// var.scss

$bule: #45fdf3;
$base-font-size: 14px;

:export {
    brandColor: $blue;
    baseFontSize: $base-font-size;
}

在JavaScript文件中可以像下面这样获取到相应的变量值:

// App.js

import styles from './var.scss';

console.log(styles.brandColor);     // -> #45fdf3
console.log(styles.baseFontSize);   // -> 14px

其实我们可以借助CSS的自定义属性相关的特性,会让事情变得更为简单。感兴趣的同学不妨一试。

CSS Modules使用小技巧

花了一定的篇幅向大家演示了如何来写CSS Modules,现在简单的小结一些写CSS Modules应该注意的一些小细节。

  • 尽量使用class来定义样式
  • 在CSS Modules中id声明的样式会被忽略,正好应了那句,少在代码中使用id
  • CSS Modules最大特色是作用于本地(局部域),只用单个类来定义样式最佳
  • 尽量避免组合选择器的运用,在CSS Modules中也没必要这么使用,有利于提高选择器性能
  • 可以借助:global和属性选择器的小技巧来声明全局样式
  • 借助Webpack的能力,让全局样式和局部样式共存(文件结构需要组织好)
  • 通过CSS Modules的composes特性来实现样式复用,可以将共用样式单独放置在一个文件中,通过composes来复用
  • 使用class命名时,尽量考虑BEM的模式(尽量避免中折---,应该尽量选择___连接)
  • 尽量借助PostCSS能力,辅助你快速编写CSS代码
  • 尽量借助CSS的自定义属性来替代CSS处理器的变量,虽然CSS Modules中引用的变量可以CSS和JavaScript共用
  • 在设计组件时,需要预留覆盖组件样式的入口,个人建议采用data-xxx这样的自定义属性来做为覆盖样式的入口选择器

上面这几点只是我自己使用CSS Modules的体验,仅是建议。既然是建议,大家可以根据自己的习惯来做相应的调整。另外在使用CSS Modules时尽量避免:

  • 在同一个元素中声明多个类:其实没必要,CSS Modules的composes可以复用本地或其他组件中相同的类的样式,好比Sass的@extend可以扩展@mixin%placeholder样式
  • 不在同一个样式文件中使用相同的类名:不管是CSS Modules还是CSS中,我们都应该尽量避免在同一个样式文件中使用相同的类名(CSS的选择器权重和顺序决定选用哪个样式)。哪果你在同一相样式中使用了相同的类名,在编译时并不会出错,编译出来的类名是相同的,只不过哪个使用,决取于其位置所在,越靠后越会被选用(假设权重相同)
  • 尽量不要在样式中使用标签或属性选择器声明样式:在CSS Modules中,使用标签元素和属性选择器声明的样式是全局的
  • 尽量避免使用:global标识声明样式:如果需要使用:global标识符声明样式,应该将这该样式放置到公用样式文件中,以供全局使用,这样做利于代码维护
  • 尽量避免id选择器声明样式:虽然CSS Modules会对样式中的id选择器转换成相应的hash值,但并不会运用到带有指定id的元素上(标签元素id属性的值不不会像class一样被编译与样式表中编译后的值相匹配)

这些只是自己在React项目中初次使用CSS Modules的体感所记,如果有不对之处还请大神拍正。如果您有这方面更多的使用经验和更好的建议,欢迎在下面的评论中与我们一起分享。

改变Create React App体系下默认使用CSS Modules的模式

前面提到了,使用create-react-app创建的React项目,默认就具备了CSS Modules的能力。不同的是在创建样式文件时对命名有一定的要求,需要以[name].module.css(或[name].module.scss[name].module.sass[name].module.less等),而且在.js中引用样式文件需要像下面这样来引用:

import styles from './Button.module.css'

如果你去阅读create-react-app构建体系中的Webpack相关配置(webpack.config.js)时不难发现,默认只对cssModuleRegexsassModuleRegex中开启了CSS Modules。

如果要查看Webpack相关的配置,需要先执行npm run eject命令,在config/目录下可以找到对应的webpack.config.js文件。请接着把分支切换到demo8查看源码。

执行完npm run eject命令之后,在config/目录可以找到webpack.config.js文件,就可以查看代码了(这个时候我又要感叹了,昵玛Webpack的配置真复杂,我需要一位Webpack高级配置专家帮我),截取其中的一部分代码:

// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

配置中对.css.module.css.scss|sass以及.module.scss|sass等样式文件做了一个正则匹配。关键代码是默认之下只在cssModuleRegexsassModuleRegex开启了CSS Modules的能力:

既然如此,是不是依葫芦画瓢,也可以在cssRegexsassRegexgetStyleLoaders()中添加下面两行的配置呢:

modules: true,
getLocalIdent: getCSSModuleLocalIdent,

想不如动,直接添加保存:

重新一下工程,测试一下是否会对.css中的选择器做hash转换:

编译出来的结果如下:

是不是离我们的习惯又更进一步了。接着继续往下撸,让我们在CSS Modules环境下撸CSS更接近我们平时的手撸姿势。

React CSS Modules带来的优势

React CSS Modules是一个npm包,其实现了自动化映射CSS Modules。每个CSS类都赋予一个带有全局唯一名字的本地标识符。CSS Modules实现了模块化和复用性。同时CSS Modules是一个可以被多种方法实现的规范。React CSS Modules利用Webpack的css-loader所提从的功能启用了现有的CSS Modules的集成。

简单地说,CSS Modules 和 React CSS Modules都是通过Webpack的css-loader来做编译的,不同的是React CSS Modules在现有的CSS Modules做了一些集成和优化。如果大家有动手写过CSS Modules的代码,不难发现现有的CSS Modules给我们编写CSS时带来几个不便之处:

  • 类名的命名有较强的限制,比如说需要采用驼峰式命名,或采用下划线_(或__)连接的命名
  • 无论何时构建一个className都必须使用styles对象
  • 混合类的使用带来诸多的不便
  • 全局类不够灵活
  • 引用一个未定义的CSS模块时解析结果为undefined,但并无相关警告或报错提示

而React CSS Modules改进了上述的问题,并提供了更好的实现方式:

  • 类的命名方式不再受限制(可以使用你平时较为喜欢的命名类方式)
  • 不必每次使用一个CSS模块时还要引用styles对象
  • 全局CSS和CSS Modules之间有一个明显的区别,比如 <div className="button" styleName="button-primary">
  • styleName引用一个未定义的CSS模块时,会得到一个警告信息(需要开启handleNotFoundStyleName选项的配置)
  • 可以为每个元素(ReactElement)只使用单独的CSS Module,也可以使用多个CSS Module(取决由配置项allowMultiple决定)

接下来,还是基于Create React APP创建的项目环境下来向大家演示React CSS Modules的使用。

先把分支切换到demo9查看示例源码。

首先在项目根目录下执行:

npm i react-css-modules --save

那么你的组件可以会变成下面这种方式使用:

// App.js
import React from 'react';
import CSSModules from "react-css-modules";
import logo from './logo.svg';
import styles from './App.module.css';

console.log(styles);

function App() {
    return (
        <div className="App">
            <header styleName="App-header">
                <img src={logo} className="App-logo" alt="logo" />
                <p>Edit <code>src/App.js</code> and save to reload.</p>
                <a
                    className="App-link" styleName="underline"
                    href="https://reactjs.org"
                    target="_blank"
                    rel="noopener noreferrer"
                >
                    Learn React
                </a>
            </header>
        </div>
    );
}

export default CSSModules(App, styles);

// 或者
// export default CSSModules(App, styles, options)

注意,在组件中需要调用react-css-modules,像下面这样:

import CSSModules from "react-css-modules";

并且export组件的时候像下面这样:

export default CSSModules(App, styles);

或者带有参数的方式导出组件:

export default CSSModules(App, styles, options)

在React CSS Modules的官方文档中有options设置的详细描述。比如:

  • allowMultiple选项设置为true时,可以在styleName中同时设置多个类名,比如styleName="a b",如果设置为false只能在styleName设置一个类名
  • handleNotFoundStyleName,选项设置调用未声明的模块报不报错等

使用了React CSS Modules之后,我们在JSX模板中时,可以同时使用classNamestyleName。其中className是一个全局的样式,而styleName是一个局部的样式。比如我们前面提到的Button组件,.button是公用样式,那么就可以使用className="button"来引用,而button-primary是组件样式,那就可以通过styleName="button-primary"来引用:

// Button/Button.js
import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './Button.module.css'

const Button = () => {
    return (
        <div className="button" styleName="button-primary" role="button">Primary Butotn</div>
    )
}

export default CSSModules(Button, styles, {allowMultiple: true, handleNotFoundStyleName: 'throw'})

在介绍CSS Modules的时候,我们提到过,在CSS Modules中要覆盖样式是件麻烦的事情,那么在React CSS Modules中,事件会相对来说简单一点。我们可以使用styles属性来覆盖组默认的styles对象。比如我们想要在App.js中覆盖组件Button的样式,可以像下面这样来做:

// button-cover-css.module.css
.button-primary {
    background: linear-gradient(to bottom, #09a, #f09);
    color: #fff;
}

// App.js
import buttonCoverStyles from './button-cover-css.module.css;
<Button styles={ buttonCoverStyles }>

包装过的组件继承了styles属性,该属性描述了CSS Mdodules和CSS类之间的映射关系。比如,styleName='foo'className={this.props.styles.foo}是等价的。

使用React CSS Modules还有一个方便之处。在某些情况下,我们希望给组件同时导入多份CSS样式文件,毕竟在写组件时还是会用到一公用样式的嘛,这些公用样式又不想在每个组件的方重撸一回。那么我们就可以像下面这样使用:

// Panel.js
import React from "react";
import CSSModules from "react-css-modules";
import style from './panel.css'
import sharedStyle from './shared.scss'

const Panel = () => {
    return (
        <div styleName="panel panel-default">
            <div styleName="panel-body">A Basic Panel</div>
        </div>
        )
}
export default CSSModules(Panel,{...style, ...shared},{});

// Shared.css
.panel{
    background-color: #eee;
    border-radius: 4px;
    padding: 20px 30px;
}

// Panel.css
.panel-default{
    composes: panel from './panel.css";
    max-width: 640px;
    margin-bottom: 24px;
}

上面只是演示了React CSS Modules中的一小部分特性,如果你对React CSS Modules更详细的内容感兴趣的话,可以花点时间阅读官方提供的文档

如果你对React CSS Modules的文档不是很了解,也没关系,他并不会太影响你在React中怎么使用CSS Modules。只需要知道怎么安装,使用调用React CSS Modules即可,其使用具有CSS Modules所有特性(因为他是CSS Modules的集合),只是在CSS Modules上做了一些使用上的优化。

你可能发现了,上面的示例中importstyles时,文件格式还是.module.css。如果你还是喜欢.css的方式,请按上一节的方式,在Create React App的工程体系下开启相关的配置。

使用 babel-plugin-react-css-modules实现CSS Modules

如果你打开过React CSS Modules的官网,可以看到作者在最前面就推荐大家使用babel-plugin-react-css-modules来替代它。而两者主要差异是:

babel-plugin-react-css-module是预先发生,可以免去运行时,处理styleName对应问题,提升生产环境下的性能

不过babel-plugin-react-css-modules的配置真是件非常痛苦的事情。要比react-css-modules复杂的多。跟着官方文档操作一波也未能如愿,网上搜索了大量的相关文章也并没有实际上解决配置问题(主要还是自己太弱,继续需要一位Webpack配置导师来指点我)。另外到目前为止,babel-plugin-react-css-modules还有很多Issues(44个)未修复。是不是有点感觉在坑爹呀。

如果你不所被坑,那就请继续往下阅读,看看如何使用babel-plugin-react-css-module实现CSS Modules。

请把分支切换到demo10查看源码。

目前我们还是基于Create React App的构建体系上来操作的。请先执行npm run eject命令再继续后面的配置和操作。

在命令终端执行下面命令:

npm i babel-plugin-react-css-modules --save

此处有个细节,安装的时候,带的后缀是--save,而不是--save-dev

webpack.config.js中开启.css|.scss|.sass的CSS Modules的能力。同时在你项目的根目录下创建.babelrc文件,并在该文件中添加有关于babel-plugin-react-css-modules相关的配置:

// .babelrc
{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ],
    "plugins": [
        "@babel/plugin-transform-react-jsx-source",
        [
            "babel-plugin-react-css-modules",
            {
                "context": "./",
                "generateScopedName": "[local]___[hash:base64:5]",
                "autoResolveMultipleImports": true,
                "webpackHotModuleReloading": true,
                "exclude": "node_modules",
                "handleMissingStyleName": "warn"
            }
        ]
    ]
}

结应的webpack.config.js也需要做相应的调整。因为babel-plugin-creat-css-modules中的generateScopedName规则要和css-loader中的localIdentName规则保持一致。

// webpack.config.js 部分代码

const getCSSModuleLocalIdent = "[local]___[hash:base64:5]"; // 需要和.babelrc中generateScopedName保持一样

oneOf: [
    // ...
    {
        test: cssRegex,
        exclude: cssModuleRegex,
        use: getStyleLoaders({
            importLoaders: 1,
            sourceMap: isEnvProduction && shouldUseSourceMap,
            modules: true,
            // getLocalIdent: getCSSModuleLocalIdent,
            localIdentName: getCSSModuleLocalIdent,
        }),
        sideEffects: true,
    },
        
    {
        test: cssModuleRegex,
        use: getStyleLoaders({
            importLoaders: 1,
            sourceMap: isEnvProduction && shouldUseSourceMap,
            modules: true,
            // getLocalIdent: getCSSModuleLocalIdent,
            localIdentName: getCSSModuleLocalIdent,
        }),
    },
       
    {
        test: sassRegex,
        exclude: sassModuleRegex,
        use: getStyleLoaders({
            importLoaders: 2,
            sourceMap: isEnvProduction && shouldUseSourceMap,
            modules: true,
            // getLocalIdent: getCSSModuleLocalIdent,
            localIdentName: getCSSModuleLocalIdent,
        },'sass-loader'),
        sideEffects: true,
    },
       
    {
        test: sassModuleRegex,
        use: getStyleLoaders({
            importLoaders: 2,
            sourceMap: isEnvProduction && shouldUseSourceMap,
            modules: true,
            // getLocalIdent: getCSSModuleLocalIdent,
            localIdentName: getCSSModuleLocalIdent,
        },'sass-loader'),
    },   
],

如果package.json中有关于babelrc的相关配置的话,需要把它去除掉,不然会和新创建的.babelrc相冲突。保存上面所有的修改,重新运行你的项目。如果你能看到React的Logo,表示你已经离成功更近一步了。

使用起来也非常的简单,和CSS Modules以及React CSS Modules只是细节上的差异,详细的操作可以阅读官方提供的示例,这里只用一个简单的示例演示一下babel-plugin-react-css-modules下的CSS Modules的基本使用:

// App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
return (
    <div styleName="App">
    <header styleName="App-header">
        <img src={logo} styleName="App-logo" alt="logo" />
        <p>
        Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
        styleName="App-link"
        href="https://reactjs.org"
        target="_blank"
        rel="noopener noreferrer"
        >
        Learn React
        </a>
    </header>
    </div>
);
}

export default App;

使用该插件操作CSS Modules时,在引入CSS文件时,不再需要将其赋值给一个对象,可以直接import导入需要的样式文件:

// babel-plugin-react-css-modules
import './App.css';

// CSS Modules 或 React CSS Modules下
import style from './App.css'

在JSX中使用styleName替代了className。如果使用className引入的为名,虽然样式中的代码会编译,但在JSX中className不会做hash编译,比如下面这个小例示:

// App.css

.active {
color: red;
padding-bottom: 5px;
border-bottom: 3px double currentColor
}

// App.js
<a styleName="App-link" className="active">Learn React</a>

编译出来的结果:

<!-- Style -->
.App-link___13fTZ {
    color: #61dafb;
}

.active___1y60p {
color: red;
padding-bottom: 5px;
border-bottom: 3px double currentColor
}

<!-- HTML -->
<a href="https://reactjs.org" target="_blank" rel="noopener noreferrer" class="active App-link___13fTZ">Learn React</a>

如果在组件中同时引入两个不同的CSS文件,并且也有相同的类名时,想分清楚使用的是哪个文件中的类名时,可以像下面这样操作:

// App.js
import appStyle from './App.css';
import fooStyle from './foo.css';

<div styleName="appStyle.active">active class in App.css</div>
<div styleName="fooStyle.active">active class in foo.css</div>

编译出来的结果:

<!-- HTML -->
<div class="active___1y60p">active class in App.css</div>
<div class="active___R5w4I">active class in foo.css</div>

<!-- Style -->
// foo.css
.active___R5w4I {
    color: red;
}

// App.css
.active___1y60p {
    color: green;
    padding-bottom: 5px;
    border-bottom: 3px double currentColor;
}

为了更好的区分类名来自哪个文件,我们在做hash配置的时候,可以把[name]参数加上,比如"[name]__[local]___[hash:base64:5]"

更多的示例不再演示了,感兴趣的同学可以点击官网查阅。另外推荐两篇有关于babel-plugin-react-css-modules相关的两篇文章:

小结

该文主要踩了一遍React下面CSS Modules的使用,虽然大环境下是基于Create React App构建体系展开的,但有关于CSS Modules在业务中如何使用,以及需要的注意的细节都有阐述过,另外为了更好的改善大家在React项目中编写、维护和扩展CSS的能力,还介绍了react-css-modulesbabel-plugin-react-css-module怎么来增强React环境下CSS Modules的能力。

那么自这篇文章之后,有关于CSS Modules在不同环境下的运用就基本上全了(PostCSS下CSS ModulesVue环境下的CSS Modules),接下来,再来踩一回纯Webpack下的CSS Modules如何配置,以及有关于其他增加CSS能力和提高开发效率的工具,在Webpack体系下如何构建,感兴趣的同学可以关注后续的相关更新。

扩展阅读